1. Bingact.createElement()

Circlewee·2022년 6월 9일
2

WYLSBingsu

목록 보기
2/8
post-thumbnail

0. 서론

글을 작성하기에 앞서 React공식 문서와 구글링을 통한 자료수집으로 공부를 해봤다. 공부를 하면서 느낀 점은 정말 대단한 사람들이 대단한 작품을 만들었다는 생각이 들었다.
기초 개념이 공감갔고 왜 이렇게 구현했을까에 대한 호기심도 어느 정도 해결할 수 있었지만, 이것들을 깨닫는다고 해서 제로에서부터 구현하는 것은 어림도 없다는 것도 깨달을 수 있었다.

그래서 코드 구현 자체는 Reference와 별다를 것이 없을 것이다.
다만 내가 이것을 파고들어 공부하면서 깨닫는 것들을 같이 기록하고 어떤 생각으로 개발자들이 만들었을지를 느낄 수 있다는 것만으로도 큰 공부가 될것이라고 생각한다.
그리고 작성하는 코드들을 최대한 이해하려하고 주석이나 아래 comment의 형태로 남겨보려고 한다.

1. createElement

먼저 리액트는 JSX 문법을 사용한다. (필수 아님)

// 1
const element1 = <h1>Hello World!</h1>;
// 2
const element2 = React.createElement('h1', null, 'Hello World!');

두 코드 모두 React에서 element를 생성하는 코드이다.
1번이 jsx문법을 사용한 코드이고, 2번이 React의 createElement함수를 사용한 코드이다.
1번으로 작성한 코드를 babel로 컴파일하게 되면 2번과 같은 코드로 바뀌게 되므로 같은 element를 생성하는 코드라고할 수 있다.
여기서 알 수 있는 것은 createElement()를 구현하면 React스러운 방식으로 element를 생성할 수 있고 더 나아가 @babel/preset-react로 트랜스파일하면서 JSX문법을 JS코드로 바꿀 때 우리가 만든 Bingact를 사용할 수 있으므로 구현해야 한다는 것이다.

1.1 코드

// Bingact.js
function createElement(type, props, ...children) {
	return {
      type,
      props: {
      	...props,
        children: children.map((child) => 
        	typeof child === 'object' ? child : createTextElement(child)
        ),
      },
    };
}

function createTextElement(text) {
  return {
    type: 'TEXT_ELEMENT', // 임의로 지정하는 type
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

function render() {} // 구현해야하는 부분

const Bingact = {
  createElement,
  render,
}

export default Bingact;
// app.js
import Bingact from 'Bingact';

const element = Bingact.createElement(
  'div', 
  {id: 'parent', 
  Bingact.createElement('h1', {id: 'child'}, 'Hello World!')
);
  • type(div, h1, p등)을 받아 담고, props로 element의 attribute를 지정한다. 여기서 DOM의 기본 개념을 이용해 자식 element들도 props에 같이 넣어 주면서 우리에게 익숙한 트리구조를 만든다.
    그리고 이것을 Object로 반환한다.
  • 여기서 children.map()을 사용하는 이유는 만들려고 하는 element의 자식 element가 createElement로 생성된 객체일수도 있기 때문이다. 그리고 자식들도 React에서는 props로 전달 받을 수 있기 때문에 props 키에 추가하는 것이다.
    (여기서 포인트는 text가 존재하면 Text element를 최하위 자식으로 보는 것이다. HTML 태그의 기본 개념)

2. render

위에서 생성한 text element와 createElement를 활용하기 위해선 화면에 출력하는 render()가 필요하다.

function render(element, container) {
  const dom =
    element.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(element.type);

  const isProperty = (key) => key !== 'children';

  Object.keys(element.props)
    .filter(isProperty)
    .forEach((name) => {
      dom[name] = element.props[name];
    });

  element.props.children.forEach((child) => render(child, dom));

  container.appendChild(dom);
}
  • 먼저 parameter로 element와 element를 담을 container(부모)를 받는다.
    element의 type이 TEXT_ELEMENT면 text node를 생성하고 아니면 해당하는 type의 element를 생성해 dom변수에 담는다.
  • 이후 Object.keys 메소드를 이용해 element.props의 key값들을 배열로 만들고, filter메소드를 이용해 childrend이 아닌 모든 key값들을 다시 배열로 만든다. 그리고 다시 그 배열을 순회하면서 새로운 dom의 attribute로 설정한다.
  • 그리곤 자식 element들을 순회하면서 render함수를 재귀호출하며 부모 element에 appendChild한다.

이렇게 구현을 하고나니 templete string을 사용하지 않고 자식을 추가하는 방식으로 마지막에 호출하는 부분에서만 DOM에 접근할 수 있게 되었다!

3. render사용

const container = document.getElementById('root');
Bingact.render(element, container);

단순하다. #root인 element를 가져오고 위에서 JSX문법 또는 Bingact.createElement()로 생성한 element를 추가하는 코드이다.
다만 여기서 JSX문법을 사용하려면 몇가지 설정해야하는 것이 있다.

3.1 설정

// babel.config.json
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-react",
  ],
  ...
}

해당 설정을 적용하고 shell에 npx babel "경로" --presets=@babel/preset-react를 입력하면 아직 안된다. 기본 설정은 React를 사용하는 것이기 때문에 우리는 코드에서 우리가 만든 Bingact를 사용한다고 알려줘야한다.

import Bingact from 'Bingact';

/** @jsx Bingact.createElement */
const element = Bingact.createElement(
  ...
)

요런식으로 /** @jsx Bingact.createElement */를 작성하면 babel이 이 파일을 트랜스파일링 할때 알아서 우리가 만든 Bingact의 createElement를 사용하게 된다.

3.2 절대 경로 설정

지금은 라이브러리를 구현하는 데 신경쓰느라 경로도 복잡하지 않지만 만약 이것을 이용해 실제로 개발한다고하면 어떻게 될까?라는 생각이 들어 절대 경로도 설정해주었다.

// babel.config.json
{
  ...
  "plugins": [
    [
      "module-resolver",
      {
        "alias": {
          "Bingact": "./src/Bingact"
        }
      }
    ]
  ]
}

방법은 간단하다. 위에서 jsx코드의 트랜스파일링을 실행하기위해 작성한 babel config파일에서 alias를 작성하는 데 왼쪽은 절대경로로 사용할 이름, 오른쪽이 생략하기 위한 경로라고 생각하고 설정해주면 된다. 만약 왼쪽을 "@@@@####"으로 바꾼다면 우리는

import Bingact from '@@@@####';

의 형태로 사용할 수 있는 것이다.
이후 npm i -D babel-plugin-module-resolver를 실행하게 된다면 우리가 만든 라이브러리를 절대경로로 사용할 수 있게 되는 것이다!

4. 마무리

함수형 컴포넌트를 만들자는 목표를 세우고 createElement와 그것을 render하는 함수까지 구현해보았다. 쉽지 않은 내용이지만 막상 JS의 메소드들과 언어적 특성을 사용해 구현하는 것을 알 수 있었던 것 같다. 공부할 수 있던 것을 간략하게 정리해 보자면

  • element의 속성을 setAttribute()메소드를 이용하는 것이 아닌 Object의 성질을 이용해서 key-value로 접근할 수 있다는 것을 깨달았다.
  • tag사이에 들어간 텍스트가 단순 텍스트가 아닌 텍스트 element인 것을 얼핏 알고 있었지만 그 성질을 따로 고려해 구현해야한다는 것을 알 수 있었다.
  • 처음에 React를 독학하면서 createElement의 형태로 element를 만들 수 있다는 것을 알고 있었지만 이후에 사용할일은 없었기 때문에 거의 잊어버리고 있었다. 그런데 babel이 트랜스파일링하는 과정에서 사용한다는 것을 알 수 있었다.
  • props도 결국에는 element에 custom 속성을 추가하는 것이다.

해결해야할 부분이 남아있다.

<div>
	<Element />
	{Element()}
</div>

위의 Element 코드가 우리가 흔히 사용하는 방법이지만 아직 동작하지 않았다. 현재는 아래와 같은 방식으로 JSX코드를 반환하는 함수를 직접 실행해야만 적용이 되고 있었다.

Reference
나만의 리액트 라이브러리 만들기
build your own react in 90 lines of javascript

profile
공부할 게 너무 많아요

0개의 댓글