[React] - babel 사용이유와 가상돔

Lee Jeong Min·2021년 12월 2일
0
post-thumbnail

babel polyfill의 필요성

babel 자체는 ES6 문법으로 작성한 코드를 ES5 환경에서 동작할 수 있도록 문법을 변환시켜주지만 ES5에 존재하지 않는 문법은 변환시키지 못한다.(ex Promise, Map, Set...) 따라서 Babel 폴리필의 추가가 필요하다.

const numbers = [101, 201, 301];

// Array.prototype.find method
let findedTwoZeroOne = numbers.find(n => n === 201);

console.log({findedTwoZeroOne});

// Promise
function delay(timeout = 1000) {
  return new Promise((resolve) => {
    window.setTimeout(() => resolve(), timeout);
  });
}

delay().then(() => {
  console.log('call out');
});

find, Promise의 경우 코드가 트랜스파일링 되지 않고 그대로 남아있기 때문에 IE에서 지원하지 않을 가능성이 높다. 따라서 이를 위해 polyfill을 사용하는데, 최근엔 core-js 사용을 권하기 떄문에 core-js를 사용한다.

참고: https://babeljs.io/docs/en/babel-polyfill#docsNav

React 컴포넌트 모듈 관리

기존 컴포넌트들을 분리하여 각 파일에 컴포넌트들을 작성하게되면 다음과 같은 오류를 마주친다.

이는 eslint에서 발생하는 JSX오류로 JSX는 REACT 스코프 범위 내에 있어야 하기 때문에 아래와 같이 코드를 작성해준다.

Headline.js

const { React } = window;

export function AppHeadline() {
  return <h1>Declarative programming</h1>;
}

그러나 Description.js의 아포스트로피가 들어가는 곳이 있는데, 리액트 컴포넌트는 HTML이 아니기 때문에 아포스트로피 이스케이프처리(')를 작성해야한다.


현재 자바스크립트로는 타입을 검사해줄 방법이 없기 때문에 아래와 같은 오류가 뜬다.

이 오류를 지금 eslint에서 꺼주기 위해 eslint 설정파일에서 'react/prop-types': 'off'를 넣어준다.


@babel/standalone 모듈 로드의 문제점

컴포넌트들로 분리된 것들을 index.html에서 바로 불러오려고 하면 다음과 같은 에러가 발생한다.

이는 js파일에서 jsx가 표준이 아니기 때문에 다음과 같은 오류가 생기는 것이다.

따라서 <script type="text/jsx" ... </script> 로 변경을 하게 되면 다음과 같은 에러를 마주친다.

이 에러는 브라우저가 require를 인식하지 못하기 때문에 발생하는 것인데 바벨로 트랜스 컴파일 되어 아래와 같은 코드를 브라우저에서 확인할 수 있다.

결국 브라우저가 require를 인식하지 못하기 때문에 esm 모듈 인식을 위해 .babelrc 파일로 들어가 설정한다.

.babelrc

    "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false
      }
    ]
  ]

또한 스크립트 태그도 아래와 같이 data-type="module"을 넣어준다.

 <script type="text/babel" data-type="module" src="./src/main.js"></script>

참고: https://babeljs.io/docs/en/babel-standalone#usage

이렇게 설정을 해주면 방금전 오류는 사라지게 되지만 경로상의 문제가 발생한다.
결국 이 문제는 babel/standalone자체의 문제이기 때문에 근본적인 다른 해결책이 필요하다

문제점

  • main.js 파일은 babel을 사용해 모듈을 호출하고 컴파일 할 수 있는 것을 확인할 수 있지만
  • main.js 파일에서 호출하는 다른 모듈의 호출 경로 문제를 확인할 수 있다.
  • 다른 모듈 파일의 호출경로를 올바르게 수정한다 할지라도 Babel에 의해 컴파일 되지 않는다.

이를 근본적으로 해결하기 위해 Babel cli를 사용한다.

Babel Cli

앞서 살펴본 문제를 해결하기 위해 개발에 사용된 모든 JSX 코드를 React.createElement API로 Babel을 사용해 처리해야한다.

이를 위해 다음의 패키지들을 설치해준다.

npm i -D @babel/{cli,plugin-transform-react-jsx}

플러그인 설치했으므로 플러그인 항목에 넣어준다.

.babelrc

"plugins": ["@babel/plugin-transform-react-jsx"]

설정을 다 한 후, 패키지 제이슨에 들어가 바벨 컴파일을 위한 설정을 해준다.

"scripts": {
  "start": "cross-env OPEN='/client' npm run dev",
  "dev": "run-p watch:compile serve",
  "serve": "node ./server/index.mjs",
  "lint": "eslint ./ --ignore-path .gitignore",
  "format": "prettier --write ./ --ignore-path .gitignore",
  "compile": "babel ./client/src -d ./client/dist",
  "test": "jest",
  "watch": "run-p watch:**",
  "watch:lint": "esw ./ --watch --color --ignore-path .gitignore",
  "watch:format": "onchange ./ -- npm run format {{changed}}",
  "watch:compile": "npm run compile -- --watch",
  "watch:test": "npm t -- --watchAll",
  "clear": "rimraf -rf ./client/dist"
},

package.json에 wacth:compile이라는 바벨 컴파일을 위한 스크립트를 넣어두고, dist파일에 있는 src를 main에서 갖다쓰게 하면 바벨로 컴파일된 소스코드를 이용해 렌더링을 하게 된다.

/client/index.html

<script type="module" src="./dist/main.js"></script>

이후 정상적으로 동작하는 것을 확인할 수 있다.

module 프로그래밍 실습

이후 React 컴포넌트로 모듈 프로그래밍 실습을 진행하기 위해 피그마에 디자인된 시안들을 확인하였는데 윈도우와 달리 맥북의 경우, 레티나 디스플레이에서 선명하게 보여지게 하기 위해서는 2배수 이미지가 필요하다.

따라서 피그마에서 사진을 내보낼때 2배수를 사용한다.

일반적인 사진의 경우 jpg가 최적화 되어있으며, 투명도가 필요한 경우는 png 파일, 단순한 모양, 로고, 심볼의 경우 svg를 사용하여 파일을 내보낸다.

<svg title={title} width="54" height="16" viewBox="0 0 54 16" fill="none">

svg 접근성을 위한 방법으로는 title이나 svg 로고를 감싸는 곳에서 aria-label과 같은 요소를 넣어주는 방법이 있다.

폰트 포맷

otf와 ttf 형태가 있는데 일반적으로는 OTF(맥용), TTF(윈도우용)이라고 하며 곡선의 표현방식에 차이가 있다고 한다. OTF가 곡선식의 차수가 높을수록 좀 더 자유롭게 곡선 표현이 가능하고 해상도의 단위가 높아도 깔끔하게 표현이 가능하다고 한다.

리액트에서 기본값 주는법

React DefaultProps

참고: https://reactjs.org/docs/react-without-es6.html#declaring-default-props

Logo.defaultProps = {
  textColor: '#242CF8',
  dotColor: '#FF6B00',
};

defaultProps를 이용하여 기본값을 설정할 수 있고, react는 렌더링할떄 virtualDOM의 값이 null이면 렌더링 할때 표시하지 않기 때문에 title={title} 처럼 작성하여도 (title에 값이 들어오지 않는 경우에는 표시가 되지 않고 값이 존재하는 경우에만 표시) 무방하다.

가상돔

가상돔이란 실제 DOM에 직접 조작하는 것이 아니라, 변경 요청이 발생할 때 가상의 DOM의 이전/이후 구조를 비교해 변경된 부분만 실제 DOM에 업데이트 한다. 실제 DOM을 조작하지 않아 UI 반응 속도를 높일 수 있다고 한다.

  • 잦은 DOM 조작은 비용이 많이 들고 속도가 느려진다.
  • React는 가상 DOM을 사용하여 성능(속도)을 향상시키는 방식을 채택하였다.
  • 가상 DOM의 컴포넌트를 지속적으로 관찰하여 상태 변경을 감지하려 시도한다.
  • 가상 DOM의 이전 이후 비교(diff)는 재조정 알고리즘(전에는 reconciliation, 현재는 fiber)을 사용하여 효율적으로 처리한다.
  • 비교 결과 차이가 발생하면 실제 DOM에 반영해 UI를 업데이트 한다.

가상돔 실습

가상돔 실습을 간단하게 해보았는데 파일 구조는 다음과 같다.

모듈설명
h.js가상(virtual) DOM 노드 생성
createElement.jsDOM 노드 생성 (children 포함)
diff.js이전/이후 가상 DOM 노드 비교 후, 변경 여부 반환
props.js가상 DOM 노드의 props 업데이트(변경,추출,검토,제거 등)
bindEvents.js가상 DOM 노드의 이벤트 props 연결
updateElement.js실제 DOM 노드 컨테이너에 마운트(mount) → 렌더링(rendering)

h.js

export function h(type, props = {}, ...children) {
  return { type, props, children };
}

React.createElement()는 type, props, ...children을 인자로 받는다. 마찬가지로 가상 DOM 구조를 손쉽게 생성하기 위해 hyperscript 모듈인 h를 다음과 같이 작성해준다.

JSX 프라그마

React는 가상 DOM을 보다 이해하기 쉽고, 사용하기 편리하도록 JSX를 사용한다. JSX 문법은 Babel과 같은 컴파일러를 통해 가상 DOM을 생성하는 함수 코드로 변경된다.

@babel/plugin-tranform-react-jsx 플러그인을 사용하여 JSX 문법을 특정 함수로 처리하여 컴파일 할 수 있는데 가상 DOM을 생성하는 h() 함수를 JSX 문법을 해석하는 함수로 설정할 수 있다.

JSX 문법을 해석해 특정 함수로 변경하도록 설정하려면 @jsx 프라그마(컴파일러에 지시하는것)을 사용한다.

/** @jsx h */

const vDomNode = (
  <ul className="what-is-virtual-dom">
      <li>가상 DOM은 실제 DOM을 추상화 하여 표현한 것을 말합니다.</li>
      <li>가상 DOM 트리에서 무언가 변경되면 새로운 가상 DOM 트리가 생성됩니다.</li>
  </ul>
);
h(
  'ul', 
  { className: 'what-is-virtual-dom' }, 
  h('li', null, '가상 DOM은 실제 DOM을 추상화 하여 표현한 것을 말합니다.'),
  h('li', null, '가상 DOM 트리에서 무언가 변경되면 새로운 가상 DOM 트리가 생성됩니다.'),
)

따라서 JSX문법으로 작성을 해도 바벨 플러그인 transform 덕분에 손쉽게 가상노드들을 생성할 수 있다. 아래는 바벨 홈페이지에서 컴파일러에 의해 처리된 결과를 보여준다.

createElement.js

import { setProps } from './props.js';
import { bindEvents } from './bindEvents.js';

export function createElement(vNode) {
  if (typeof vNode === 'string') {
    return document.createTextNode(vNode);
  }

  const elementNode = document.createElement(vNode.type);

  setProps(elementNode, vNode.props);
  bindEvents(elementNode, vNode.props);

  vNode.children
    .map(createElement)
    .forEach(elementNode.appendChild.bind(elementNode));

  return elementNode;
}

createElement는 vNode가 텍스트 노드인지 확인하여 텍스트 노드인 경우에는 바로 반환한다. 그 외의 경우 setProps를 이용하여 리액트 컴포넌트의 props를 설정해주고 bindEvents로 이벤트를 설정해준다.
그 이후, 자식 노드들을 돌면서 재귀적으로 호출하여 현재 노드들에 붙여준다.

updateElement.js

import { createElement } from './createElement.js';
import { updateProps } from './props.js';
import { diff } from './diff.js';

export function updateElement(parentNode, newVNode, oldVNode, index = 0) {
  if (!oldVNode) {
    parentNode.appendChild(createElement(newVNode));
  } else if (!newVNode) {
    parentNode.removeChild(parentNode.childNodes[index]);
  } else if (diff(newVNode, oldVNode)) {
    parentNode.replaceChild(
      createElement(newVNode),
      parentNode.childNodes[index]
    );
  } else if (newVNode.type) {
    updateProps(parentNode.childNodes[index], newVNode.props, oldVNode.props);

    let i = 0;
    let newChildrenCount = newVNode.children.length;
    let oldChildrenCount = oldVNode.children.length;

    for (; i < newChildrenCount || i < oldChildrenCount; ++i) {
      updateElement(
        parentNode.childNodes[index],
        newVNode.children[i],
        oldVNode.children[i],
        i
      );
    }
  }
}

만약 위 함수에서 oldVNode 즉 이전 가상 DOM 노드가 없는 경우에는 바로 붙여 주기만 하면된다. 또한 새 가상 DOM노드가 전달되지 않은 경우에는 DOM에서 제거되었다는 의미이므로 parentNode에서 index에 해당하는 자식 노드를 제거해준다.
세 번째로는 가상 DOM노드들을 비교해 변경이 감지되면 diff 모듈을 사용하여 새로운 노드로 교체해주고, DOM요소의 props의 변경사항을 업데이트 사항이 있다면 업데이트 해준다. 최종적으로 자손 노드를 재귀호출을 통해 업데이트 시켜준다.

diff.js

export function diff(newVNode, oldVNode) {
  return (
    typeof newVNode !== typeof oldVNode ||
    (typeof newVNode === 'string' && newVNode !== oldVNode) ||
    newVNode.type !== oldVNode.type ||
    newVNode.props.forceUpdate
  );
}

전달된 새 가상 DOM과 이전 가상 DOM을 비교해 노드가 변경되었는지 확인한다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글