이 이야기는 단계별 리액트 DIY 시리즈의 일부입니다.

DOM 리뷰

시작하기 전에, 사용할 DOM API를 살펴봅시다.

// Get an element by id
const domRoot = document.getElementById("root");
// Create a new element given a tag name
const domInput = document.createElement("input");
// Set properties
domInput["type"] = "text";
domInput["value"] = "Hi world";
domInput["className"] = "my-class";
// Listen to events
domInput.addEventListener("change", e => alert(e.target.value));
// Create a text node
const domText = document.createTextNode("");
// Set text node content
domText["nodeValue"] = "Foo";
// Append an element
domRoot.appendChild(domInput);
// Append a text node (same as previous)
domRoot.appendChild(domText);

gist, codepen

속성(attributes) 대신 엘리먼트 속성(element properties)을 설정한다는 점에 유의하세요. 즉 유효한 속성(properties)만 허용된다는 의미입니다.

Didact 엘리먼트들

랜더링 할 필요가 있는 것이 무엇인지 설명하기 위해 일반 JS 오브젝트를 사용할 것 입니다. 그것을 Didact 엘리먼트들이라 부를 것 입니다. 이 엘리먼트는 typeprops, 두 개의 필수 속성(properties)을 가집니다. type은 문자열이거나 함수가 될 수 있지만, 추후 포스트에서 컴포넌트를 소개하기 전까지는 문자열 만을 사용할 겁니다. props는 비어있을 수 있지만 null은 아닌 오브젝트입니다. props는 Didact 엘리먼트들의 배열이 될 수 있는 children 속성(property)을 가질 수 있습니다.

Didact 엘리먼트를 많이 사용하게 될 것이므로, 지금부터는 줄여서 엘리먼트라 부를 것 입니다. HTML 엘리먼트와 혼동하지 않도록, 이것들은 DOM 엘리먼트, 혹은 변수 이름을 지을 때 그냥 dom 엘리먼트라 하도록 하죠. (preact가 그랬듯이)

예를 들어, 아래는 엘리먼트 입니다.

const element = {
  type: "div",
  props: {
    id: "container",
    children: [
      { type: "input", props: { value: "foo", type: "text" } },
      { type: "a", props: { href: "/bar" } },
      { type: "span", props: {} }
    ]
  }
};

gist

아래는 DOM 엘리먼트입니다.

<div id="container">
  <input value="foo" type="text">
  <a href="/bar"></a>
  <span></span>
</div>

gist

Didact 엘리먼트들은 리액트 엘리먼트와 매우 유사합니다. 하지만 일반적으로 여러분이 리액트를 사용할 때, 리액트 엘리먼트를 JS 객체로 만들지는 않을 겁니다. 아마도 여러분은 JSX 혹은 createElement를 사용하겠죠. Didact에서도 동일하지만, 다음 시리즈의 포스트를 위해 엘리먼트 생성 코드는 남겨두도록 하죠.

DOM 엘리먼트 랜더링

다음 단계는 엘리먼트와 그 자식을 dom에 랜더링하는 것 입니다. 우리는 하나의 엘리먼트와 dom 컨테이너를 받는 render 함수(ReactDOM.render 와 동일한)를 사용합니다. 이 함수는 엘리먼트에 정의된 대로 DOM 하위 트리를 만들어 컨테이너에 추가할 것 입니다.

function render(element, parentDom) {
  const { type, props } = element;
  const dom = document.createElement(type);
  const childElements = props.children || [];
  childElements.forEach(childElement => render(childElement, dom));
  parentDom.appendChild(dom);
}

gist

여전히 속성(properties)과 이벤트 리스너가 없습니다. 그럼 props 속성 이름들을 Object.keys 함수로 반복하여 적절히 설정해 보겠습니다.

function render(element, parentDom) {
  const { type, props } = element;
  const dom = document.createElement(type);

  const isListener = name => name.startsWith("on");
  Object.keys(props).filter(isListener).forEach(name => {
    const eventType = name.toLowerCase().substring(2);
    dom.addEventListener(eventType, props[name]);
  });

  const isAttribute = name => !isListener(name) && name != "children";
  Object.keys(props).filter(isAttribute).forEach(name => {
    dom[name] = props[name];
  });

  const childElements = props.children || [];
  childElements.forEach(childElement => render(childElement, dom));

  parentDom.appendChild(dom);
}

gist

DOM 텍스트 노드 랜더링

render 함수가 아직 지원하지 않는 한가지는 텍스트 노드입니다. 먼저 텍스트 엘리먼트의 모양을 정의해야 합니다, 예를 들어, <span>Foo</span> 엘리먼트는 리액트에서 아래와 같이 작성할 수 있습니다.

const reactElement = {
  type: "span",
  props: {
    children: ["Foo"]
  }
};

gist

자식은 다른 엘리먼트 객체가 아니라 단순히 문자열이라는 점에 유의하세요. 이는 우리가 정의한 Didact 엘리먼트들(:children)에 위배되니, 엘리먼트들의 배열과 모든 엘리먼트는 typeprops를 가져야합니다. 이러한 규칙을 따른다면, 추후에 조건 문(if)을 적게 사용할 수 있을 것 입니다. 따라서 아래와 Didact 텍스트 엘리먼트의 type"TEXT ELEMENT"고, 실제 텍스트는 nodeValue 속성에 포함합니다.

const textElement = {
  type: "span",
  props: {
    children: [
      {
        type: "TEXT ELEMENT",
        props: { nodeValue: "Foo" }
      }
    ]
  }
};

gist

텍스트 엘리먼트가 어떻게 렌더링 되는지 정의했습니다. 다른 엘리먼트와의 차이점은 텍스트 엘리먼트는 createElement를 사용하는 대신 createTextNode를 사용해야한다는 것입니다. 즉, nodeValue는 다른 속성들(properties)과 같은 방식으로 설정됩니다.

function render(element, parentDom) {
  const { type, props } = element;

  // Create DOM element
  const isTextElement = type === "TEXT ELEMENT";
  const dom = isTextElement
    ? document.createTextNode("")
    : document.createElement(type);

  // Add event listeners
  const isListener = name => name.startsWith("on");
  Object.keys(props).filter(isListener).forEach(name => {
    const eventType = name.toLowerCase().substring(2);
    dom.addEventListener(eventType, props[name]);
  });

  // Set properties
  const isAttribute = name => !isListener(name) && name != "children";
  Object.keys(props).filter(isAttribute).forEach(name => {
    dom[name] = props[name];
  });

  // Render children
  const childElements = props.children || [];
  childElements.forEach(childElement => render(childElement, dom));

  // Append to parent
  parentDom.appendChild(dom);
}

gist

요약

엘리먼트를 DOM에 자식으로 렌더링 할 수 있게 해주는 render 함수를 만들었습니다. 다음으로 필요한 것은 엘리먼트를 만드는 쉬운 방법입니다. 다음 포스팅에서는 JSX를 Didact와 함께 사용할 수 있도록 만들어보겠습니다.

지금까지 작성한 코드를 돌려보길 원하면 codepen을 확인하거나, github 레포에서 차이점을 확인할 수도 있습니다

다음 포스트: 엘리먼트 생성과 JSX

읽어 주셔서 감사합니다.


저는 Hexacta에서 여러가지를 만들고있습니다.
아이디어가 있거나 저희를 도울 수 있다면 연락주세요.


출처, 소스코드