[JS] JS에 JSX 적용해보기

jiseong·2022년 5월 4일
3

T I Learned

목록 보기
237/291
post-custom-banner

React에서 흔하게 사용되는 JSX문법은 바벨에 의해 다음과 같이 변경이 된다. HTML 문서에서 작성하던 방식으로 작성한 코드가 중첩 구조를 가진 React.createElement 함수로 변경이 되고 React.createElement에 의해 Node가 생성되고 부착되는 방식이다.

그런데 여기서 최상단에/** @jsx 사용할함수명 */을 작성해주면 React.createElement 대신에 사용할함수명으로 바뀌는 것을 볼 수 있다.

이런 특성을 이용하면 react의 도움없이도 순수 자바스크립트에서 JSX를 사용할 수 있다.

트랜스파일

가장 먼저 HTML 문서에서 작성하던 방식으로 작성한 코드를 커스텀함수로 트랜스파일 해주기 위해 바벨 플러그인을 설치해줘야 한다.

$ npm install --save-dev @babel/plugin-transform-react-jsx

그리고 웹팩설정으로 플러그인에 추가해주면 된다.

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

커스텀 함수

플러그인을 추가해주면 바벨에 의해서 JSX문법이 커스텀함수로 변환되지만 아직 변환된 코드를 활용해서 Node를 생성하고 화면에 나타낼 수 있는 과정을 구현하지 않았기 때문에 커스텀함수를 작성해줘야 한다.

커스텀 함수 생성

바벨의 의해 변환되어 함수의 매개변수로 들어오는값들은 태그명, 속성들, 자식들이다.

const test = (
  <div class="container">
    <p>hell</p>
  </div>
);

// 바벨에 의해 변환된 값
const test = 커스텀함수("div", {class: "container"}, 커스텀함수("p", null, "hell"));

그래서 이 순서에 맞게 커스텀함수를 구현해주면되는데 우선 간단하게 ul, li 태그로 이루어진 값을 처리하는 커스텀함수를 작성해보려고 한다.

  markup() {
    return (
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
    );
  }

가장 먼저 커스텀함수의 첫번째 매개변수인 태그명을 활용하여 Node를 생성하고

const node = document.createElement(name);

두번째 매개변수를 활용하여 각 Node의 attributes인 class, href등을 추가하는 과정을 작성한다.

Object.entries(attributes || {}).forEach(([key, value]) => {
  node.setAttribute(key, value);
});

그 다음으로 맨 마지막 매개변수를 활용해서 자식노드를 생성해주어야 하는데 다음과 같이 상황에 따라 text, Array등등 다양한 형태가 올수도 있기때문에 자식노드를 처리하는 과정을 따로 분리시켰다.

// 자식 노드들 처리
children.forEach((childNode) => addChild(node, childNode));

다양한 형태의 자식노드들

<div>text</div>

<div>{false && <p>hidden</p>}</div>

<ul>
  {books.map((book) => (
    <li>{book.name}</li>
  ))}
</ul>

지금으로서는 ul 태그, li 태그, 문자 이렇게 3가지 종류만 있기 때문에 문자일 때 처리하는 코드, 그리고 하위에서 만들어진 노드를 처리하는 코드만 작성해주면 된다.

jsx(jsx(jsx(jsx())))방식으로 중첩되는 구조이기 때문에 먼저 생성된 노드들을 처리하기 위해 typeof childNode === "object"조건을 걸어준 것이다.

생성된 Element, Node들은 object 타입을 가진다.

function addChild(parent: DocumentFragment | HTMLElement, childNode: any): any {
  // object 형식일 때 이미 하위에서 node로 만들어진 것
  if (typeof childNode === "object") {
    return parent.appendChild(childNode);
  }

  // string, number
  parent.appendChild(document.createTextNode(childNode));
}

여기까지 작성한 코드는 다음과 같다.

function jsx(name: string, attributes: TAttribute, ...children: any[]) {
  const node = document.createElement(name);

  Object.entries(attributes || {}).forEach(([key, value]) => {
    node.setAttribute(key, value);
  });

  // 자식 노드들 처리
  children.forEach((childNode) => addChild(node, childNode));

  return node;
}

function addChild(parent: DocumentFragment | HTMLElement, childNode: any): any {
  // object 형식일 때 이미 하위에서 node로 만들어진 것
  if (typeof childNode === "object") {
    return parent.appendChild(childNode);
  }
  // string, number
  parent.appendChild(document.createTextNode(childNode));
}

커스텀 함수 사용

이제 커스텀 함수를 사용할 컴포넌트에서 import하고 추가적으로 바벨의 변환과정에 커스텀 함수를 사용해야하기 때문에 /** @jsx 사용할 커스텀함수명 */을 작성해주면 된다.

/** @jsx jsx */
import jsx from "./core/jsx-runtime";

class App {
  constructor({
    container,
    props,
  }: {
    container: HTMLElement | null;
    props?: object | undefined;
  }) {
    this.container = container;
    this.props = props;
    this.render();
  }

  render() {
    this.container?.replaceChildren(this.markup());
  }

  markup() {
    return (
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
    );
  }
}

그러면 React에서 사용했던 JSX문법을 자바스크립트에서도 사용할 수 있게 된다.

아직 처리해야할 예외상황들이 존재하기 때문에 완벽하지는 않지만 비교적 쉽게? 자바스크립트에서 JSX문법을 사용할 수 있고 기존에 createElement를 생성하고 붙이고 했던 반복적인 코드들이 HTML과 비슷한 코드로 바뀌어 보기쉽고 익숙하기 때문에 한번 사용해보면 유용하다는 것을 느낄 수 있을 것이다.

jsx문법을 사용하기 전
const $dropBox = this.createDom('div', {
  className: 'dropdown',
});
const $profile = this.createDom('div', {
  className: 'profile__image',
});
const $img = this.createDom('img', {
  src: state.myInfo?.imageURL || defaultProfileImage,
  alt: 'profile',
});
const $ul = this.createDom('ul', {
  className: 'dropdown__content',
});

$profile.appendChild($img);
$dropBox.appendChild($profile);
$dropBox.appendChild($ul);
this.$dom.appendChild($dropBox);
jsx문법을 사용한 후
  markup() {
    return (
      <div class="dropdown">
        <div class="profile" onClick={this.toggleDropdown}>
          <img
            src={state.myInfo?.imageURL || defaultProfileImage}
            alt="profile"
            class="profile__image"
          />
        </div>
        <ul class="dropdown__content" onClick={this.handleDropdownContent}>
          {this.props.list.map(li => (
            <li class="dropdown__item">
              <a href={li.href} class={li.className}>
                {li.text}
              </a>
            </li>
          ))}
        </ul>
      </div>
    );
  }

작성된 코드는 여기서(Github주소)에서 확인할 수 있습니다.

추가 팁

반복적으로 컴포넌트 상단에 선언문을 작성해주는것이 귀찮다면 ProvidePlugin과 @babel/plugin-transform-react-jsx의 옵션을 활용하면 된다.

ProvidePlugin에는 작성한 커스텀함수를 불러오고 명칭을 jsx로 한다. (꼭 커스텀함수를 모듈로 내보낼 때 export default 커스텀함수로 작성해야한다.)

그리고 @babel/plugin-transform-react-jsx의 옵션으로 pragma에 별칭인 jsx를 작성해주면 된다.

(타입스크립트일때 빨간줄이 뜬다면 tsconig.json에서 jsx 옵션을 'react'가 아닌 'preserve'으로 설정해주면 된다.)

// 웹팩 설정파일
module.exports = {
  // 생략...
  module: {
    rules: [
      {
        // 생략....
            plugins: [
              [
                "@babel/plugin-transform-react-jsx",
                {
                  runtime: "classic",
                  pragma: "jsx",
                },
              ],
            ],
      },
    ],
  },
  plugins: [
    new ProvidePlugin({
      jsx: [
        path.resolve(path.join(__dirname, "src/core/jsx-runtime.ts")),
        "default",
      ],
    }),
    // 생략...
  ],
};

이전

/** @jsx jsx */
import jsx from "./core/jsx-runtime";

이후

// 필요없음

Reference

post-custom-banner

0개의 댓글