React

Symbol은 자바스크립트의 원시(Primitive)타입으로 ES6에서 새롭게 추가되었습니다.

원시타입은 객체도 아니고 메서드도 아닌 타입을 의미합니다.

 

참고) 기본 자료형 (Primitive) 인 여섯가지 데이터 타입

  • Boolean
  • Null
  • Undefined
  • Number
  • String
  • Symbol (ECMAScript 6 에 추가됨)

Symbol의 특징

Symbol은 객체 속성(object property)을 만들 수 있는 원시 타입입니다.

 

Symbol 타입은 주로 객체의 고유한 프로퍼티의 값으로 사용하는 목적으로 쓰입니다.

다음 예시를 볼까요??

var symbolProperty = Symbol("key"); // Symbol(key)
var ob = {};

ob[symbolProperty] = "value";

console.log(ob); // {Symbol(key): "value"}

console.log(ob[symbolProperty]); // "value"
console.log(typeof symbolProperty); // "symbol"

 

이 때 Symbol은 생성할 때마다 독립적인 값이 되기때문에, 같은 string 으로 정의해도 같은 값이 아닙니다.

var symbolProperty1 = Symbol("key"); // Symbol(key)
var symbolProperty2 = Symbol("key"); // Symbol(key)
var ob = {};

ob[symbolProperty1] = "value1";
ob[symbolProperty2] = "value2";

console.log(ob); // {Symbol(key): "value1", Symbol(key): "value2"}

console.log(symbolProperty1 === symbolProperty2); // false

 

위 코드를 보면 symbolProperty1, symbolProperty2 는 같은 'key'로 Symbol을 생성했지만 서로 다름을 알 수 있습니다.

 

또한 Symbol을 생성했을 때 value (value of) 는 원시형 값이 아닙니다. 따라서 toString() 등으로 문자등과 합칠 수 없습니다.

"text" + Symbol("string"); // Error
// Uncaught SyntaxError: Invalid or unexpected token

 

Symbol의 생성

Symbol은 다음과 같은 세가지 방법으로 생성할 수 있습니다.

Symbol();
Symbol.for(); // Symbol과 달리 전역으로 존재하는 global symbol table 참조
Symbol.iterator; // iterator 객체를 정의하기 위해 쓰인다.

obj[Symbol.iterator] = function* {}

Symbol.for를 더 자세히 알아볼까요?

var ob = {};
var a = Symbol.for("key");
var b = Symbol.for("key");

ob[a] = 20;

console.log(ob[b] === 20);

이렇게 Symbol.for 로 생성한 Symbol은 같은 'key'로 만든 Symbol과 같다는 것을 알 수 있습니다.

 

Symbol의 private한 성질

Symbol 속성은 열거형 속성이 아니기 때문에 for of 이나 Object.keys 때 찾을 수 없습니다.

Symbol 속성을 찾을 때는 Object.getOwnPropertySymbols 로 찾아야 합니다.

또한 JSON.stringify() 에서도 무시됩니다.

var ob = {
  [Symbol("a")]: 10,
  [Symbol("b")]: 20,
};

Object.getOwnPropertySymbols(ob);
// [Symbol(a), Symbol(b)]

Object.keys(ob);
// []

for (var i in ob) {
  console.log(i);
}
// 반환값 없음

JSON.stringify(ob);
// "{}"

 

React 와 Symbol

JSX 문법으로 태그를 생성할 때 실제로는 함수가 호출됩니다.

<marquee bgcolor="#ffa7c4">hi</marquee>;

React.createElement(
  /* type */ "marquee",
  /* props */ { bgcolor: "#ffa7c4" },
  /* children */ "hi"
);

 

그리고 위 함수는 다음과 같은 객체를 반환합니다.

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'), // 🧐 응? 이건 뭐지?
}

 

typeof는 대체 무엇이며, 왜 Symbol()을 값으로 가지고 있는 걸까요?

 

HTML injection

일반적으로 생성하고 DOM을 주입하기 위해 주로 아래와 같은 방법을 사용하곤 합니다.

const messageEl = document.getElementById("message");
messageEl.innerHTML = "<p>" + message.text + "</p>";

다만 message.text가 다음과 같을 경우 골치아파집니다.

<img src onerror="stealYourPassword()" />

이 때문에 React 같은 모던 라이브러리에선 문자열 텍스트에 대한 이스케이핑이 기본으로 지원됩니다.

 

만약 message.text에 HTML 태그나 여타 다른 수상한 태그 문자열이 들어오면, React는 이를 실제 HTML 태그로 변환하지 않습니다.

 

  1. React는 먼저 입력값을 이스케이프한 뒤 DOM에 주입시킵니다.
  2. 결과적으로 HTML 태그가 나오는 대신 단순한 마크업 코드만 표시됩니다.

 

만약 HTML을 React element 안에 넣어야하는 상황이라면,

dangerouslySetInnerHTML={{ __html: message.text }}

를 사용하면 됩니다.

 

의도적으로 injection을 염두해두고 만든 것이 느껴지죠?

 

그러나 이러한 이스케이핑 방법은 완전히 안전하지 않습니다. 대부분의 공격은 속성(attributes)을 통해 이루어집니다.

만약 서버에서 받은 message.text의 정보가 JSON인 경우 어떻게할까요??

만약 당신의 서버에 구멍이 생겨, (원래는 문자열로 입력을 받아야 하는데) 유저가 임의의 JSON 객체를 서버에 저장할 수 있는 문제가 발생했다고 하자. 클라이언트 쪽 코드에선 당연히 해당 정보를 문자열로 받게끔 설계되어 있을테니 문제가 발생하게 된다

// 서버에 구멍이 생겨 JSON이 저장되었다고 가정하자.
let expectedTextButGotJSON = {
  type: "div",
  props: {
    dangerouslySetInnerHTML: {
      __html: "/* put your exploit here */",
    },
  },
  // ...
};
let message = { text: expectedTextButGotJSON };

// React 0.13에서 이는 위험할 수 있다.
<p>{message.text}</p>;

이 문제를 해결하기 위해 '모든 React element에 Symbol 태그'를 달았습니다.

 

다시한번 React.createElement의 반환되는 객체를 살펴볼까요?

{
  type: 'marquee',
  props: {
    bgcolor: '#ffa7c4',
    children: 'hi',
  },
  key: null,
  ref: null,
  $$typeof: Symbol.for('react.element'),
}

Symbol의 성질 상 JSON에는 Symbol를 넣을 수 없습니다.

 

즉, 설사 서버에 보안 구멍이 생겨 텍스트 대신 JSON을 반환한다 하더라도, 그 JSON에는 Symbol.for('react.element') 코드를 포함시킬 수 없습니다.

React는 element.$$typeof 를 체크하여, 해당 키가 없거나 무효하면 React element 생성을 거부합니다.

 

즉 Symbol로 생성된 해당 키가 없는 경우에는 React element 로 생성되지 않는것이죠

(injection 등으로 해커의 공격을 위한 Element의 생성을 방지합니다)

 

번외 - ES6 Symbol을 지원하지 않는 브라우저는요

그럼 Symbol을 지원하지 않는 브라우저들은 어떨까요?

아쉽게도 이들은 방금 위에서 언급한 혜택을 받을 수 없습니다.

일관성을 위해 element에는 언제나 $$typeof 필드가 포함되어 있으나, Symbol를 지원하지 않는 환경에선 $$typeof 값에 Symbol 대신 number가 들어가게 됩니다.

0xeac7; // 잘 보면 “React”처럼 보이니까요

참고 자료

Javascript와 Symbol Symbol

왜-React-Element에는-typeof-프로퍼티가-있을까

 

CRA를 사용하지 않고 React 프로젝트 생성하기

CRA (create-react-app)는 매우 편리한 리액트 프로젝트 빌드 도구입니다.

Webpack, Babel 등 설정하기가 까다롭고 시간이 들어가는 작업을 한번에 해주니까요.

그러나 Babel 혹은 Webpack의 설정을 건드려야 할 경우, eject를 통해 숨겨져 있던 설정 파일들을 끄집어 내야 하는 번거로움이 존재합니다.

리액트 프로젝트를 생성할 때 Webpack과 Babel을 어떻게 구성해보는지 알아보기 위해서
한번 직접 리액트 프로젝트를 구성해 봤습니다.

다만 제가 구성한 방법으로는 Debugging에 몇몇 문제가 있어 실제 제작 프로젝트에는
CRA를 이용해 boiler plate를 생성하려고 합니다.

설치

이 프로젝트에서는 yarn을 사용합니다.

최초로 yarn init명령을 실행해 package.json 파일을 생성합니다.

yarn init -y

리액트의 핵심인 모듈들을 설치해줍니다.

이 때 build 이후에 module은 사용하지 않으므로 (개발환경에서만 사용하므로) devDependency로 설치합니다.

yarn add -D react react-dom

그리고 모듈 번들러인 Webpack을 설치합니다.

yarn add -D webpack webpack-cli webpack-dev-server

웹팩에 번들링에 필요한 바벨을 설치합니다.

yarn add -D babel-loader css-loader style-loader file-loader
  • babel-loader : JSX 및 ES6+ 문법을 트랜스파일링
  • css-loader : CSS 파일을 자바스크립트가 이해할 수 있도록 변환
  • style-loader : 변환된 CSS 파일을 style 태그로 감싸서 삽입
  • file-loader : 이미지 및 폰트 등의 파일 로딩

마지막으로 웹팩으로 번들링 할 때 필요한 플러그인들을 설치합니다.

  • html-webpack-plugin : HTML 파일에 번들링된 자바스크립트 파일을 삽입해주고 번들링된 결과가 저장되는 폴더에 옮겨줌
  • clean-webpack-plugin : 번들링을 할 때마다 이전 번들링 결과를 제거함
yarn add -D html-webpack-plugin clean-webpack-plugin

폴더 구조

이 보일러 플레이트에서 사용할 폴더들은 다음과 같습니다.

  • dist : 빌드된 파일들이 생성
  • src : 컴포넌트, 유틸 등 소스 파일들이 위치함
  • public : 빌드할 때 참고할 html 등 정적 파일

여기서 dist 폴더의 경우 build 명령을 수행할 때 자동으로 만들어 주므로 생성하지 않아도 괜찮습니다.

mkdir src public dist

바벨 설정

바벨 설정 파일인 babel.config.js 를 작성합니다.

module.exports = function (api) {
  api.cache(true);

  const presets = ["@babel/preset-env", "@babel/preset-react"];

  const plugins = [];

  return {
    presets,
    plugins,
  };
};

웹팩 설정

웹팩 설정 파일인 webpack.config.js 를 작성합니다.

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = {
  mode: "development",
  entry: {
    main: "./src/index.js",
  },
  resolve: {
    extensions: [".js", ".jsx"],
  },
  devtool: "eval-cheap-source-map", // source-map을 설정하는 부분
  devServer: {
    contentBase: path.join(__dirname, "dist"), // 이 경로에 있는 파일이 변경될 때 번들을 다시 컴파일
    compress: true, // Enable gzip compression for everything served
    port: 8080, // 각자의 portNumber 작성
    hot: true, // 모듈의 변화된 부분만 자동으로 리로딩하는 HMR(Hot Module Replacement)
    overlay: true, // 에러가 발생했을 때 브라우저에 띄울 것인지
    writeToDisk: true, // 메모리 뿐만 아니라 직접 파일로 만들 것인지
    open: true, // Tells dev-server to open the browser after server had been started. Set it to true to open your default browser.
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/, // 컴포넌트 파일을 읽어오는 규칙입니다.
        exclude: "/node_modules/",
        loader: "babel-loader",
      },
      {
        test: /\.css$/, // 스타일 속성 파일을 읽어오는 규칙입니다.
        use: [{ loader: "style-loader" }, { loader: "css-loader" }],
      },
      {
        test: /\.jfif$/,
        loader: "file-loader",
        options: {
          name: "[name].[ext]",
        },
      },
    ],
  },
  output: {
    path: path.resolve(__dirname, "dist"),
    filename: "bundle.js",
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      // index.html에 output에서 만들어진 bundle.js를 적용하여, dist에 새로운 html 파일 생성
      template: `./public/index.html`,
    }),
  ],
};

몇가지 중요한 속성을 살펴보겠습니다.

  • entry : 모듈의 의존성이 시작되는 부분으로 이름을 지정할 수 있고 여러개를 만들 수 있음.
  • resolve : 웹팩이 모듈을 처리하는 방식 정의하는 것으로 확장자를 생략하고도 인식하게 함.
  • devtool : 참고링크 source-map을 설정하는 부분으로 에러가 발생했을 때 번들링된 파일에서 어느 부분에 에러가 났는지를 쉽게 확인할 수 있게 해주는 도구.
  • devServer : webpack-dev-server의 옵션을 설정해주는 부분. 자세한 설명은 주석으로 작성했습니다.

package.json 에 script 추가

webpack-dev-server 에 --progress 옵션을 주는 경우, console에 결과가 나타납니다.

(Output running progress to console)

{
  "scripts": {
    "start": "webpack-dev-server --progress --mode development",

    "build": "webpack --progress --mode production"
  }
}

리액트 컴포넌트 생성

public 폴더에 다음과 같은 index.html 파일을 생성합니다.

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>리액트 프로젝트 시작</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

src 폴더에 entry point 파일과 (index.js), 컴포넌트 파일을 생성합니다. (App.jsx)

// index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(<App />, document.querySelector("#root"));
// App.jsx
import React from "react";

const App = () => {
  return <div>hello world!</div>;
};

export default App;

실행

개발환경의 경우 다음 명령어를 통해 실행해 볼 수 있습니다.

yarn start

빌드

다음 명령어를 통해 빌드해 볼 수 있습니다.

yarn build

타입스크립트 개발 환경 구성해보기

우선 타입스크립트와, 타입 정의 파일들을 설치해야합니다.

yarn add -D typescript @types/react @types/react-dom

타입스크립트 명령어를 사용하면 typescript 설정 파일을 생성할 수 있습니다.

npx typescript --init

tsconfig.json 파일이 자동으로 생성됩니다.

여기서 리액트 jsx 코드를 사용하기 위해서는 compilorOptions의 jsx 속성에 "react" 값을 추가합니다.

{
  "compilorOptions": {
    "jsx": "react"
  }
}

Webpack으로 빌드하기 위해 ts-loader를 설치합니다.

yarn add -D ts-loader

그리고 Webpack 설정 파일에 다음 내용들을 추가합니다.

module.exports = {
  // 엔트리 포인트
  entry: "./src/index.tsx",

  // 빌드 결과물을 dist/main.js에 위치
  output: {
    filename: "main.js",
    path: __dirname + "/dist",
  },

  resolve: {
    // 파일 확장자 처리
    extensions: [".ts", ".tsx", ".js"],
  },

  module: {
    rules: [
      // .ts나 .tsx 확장자를 ts-loader가 트랜스파일
      { test: /\.tsx?$/, loader: "ts-loader" },
    ],
  },
};

'공부' 카테고리의 다른 글

Git Hooks + commitlint를 이용한 커밋 메시지 검사  (0) 2020.08.16

+ Recent posts