이 이야기는 단계별 리액트 DIY 시리즈의 일부입니다.
시작하기 전에, 사용할 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);
속성(attributes) 대신 엘리먼트 속성(element properties)을 설정한다는 점에 유의하세요. 즉 유효한 속성(properties)만 허용된다는 의미입니다.
랜더링 할 필요가 있는 것이 무엇인지 설명하기 위해 일반 JS 오브젝트를 사용할 것 입니다. 그것을 Didact 엘리먼트들이라 부를 것 입니다. 이 엘리먼트는 type
과 props
, 두 개의 필수 속성(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: {} }
]
}
};
아래는 DOM 엘리먼트입니다.
<div id="container">
<input value="foo" type="text">
<a href="/bar"></a>
<span></span>
</div>
Didact 엘리먼트들은 리액트 엘리먼트와 매우 유사합니다. 하지만 일반적으로 여러분이 리액트를 사용할 때, 리액트 엘리먼트를 JS 객체로 만들지는 않을 겁니다. 아마도 여러분은 JSX 혹은 createElement
를 사용하겠죠. Didact에서도 동일하지만, 다음 시리즈의 포스트를 위해 엘리먼트 생성 코드는 남겨두도록 하죠.
다음 단계는 엘리먼트와 그 자식을 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);
}
여전히 속성(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);
}
render 함수가 아직 지원하지 않는 한가지는 텍스트 노드입니다. 먼저 텍스트 엘리먼트의 모양을 정의해야 합니다, 예를 들어, Foo 엘리먼트는 리액트에서 아래와 같이 작성할 수 있습니다.
const reactElement = {
type: "span",
props: {
children: ["Foo"]
}
};
자식은 다른 엘리먼트 객체가 아니라 단순히 문자열이라는 점에 유의하세요. 이는 우리가 정의한 Didact 엘리먼트들(:children)에 위배되니, 엘리먼트들의 배열과 모든 엘리먼트는 type
과 props
를 가져야합니다. 이러한 규칙을 따른다면, 추후에 조건 문(if
)을 적게 사용할 수 있을 것 입니다. 따라서 아래와 Didact 텍스트 엘리먼트의 type
은 "TEXT ELEMENT"
고, 실제 텍스트는 nodeValue
속성에 포함합니다.
const textElement = {
type: "span",
props: {
children: [
{
type: "TEXT ELEMENT",
props: { nodeValue: "Foo" }
}
]
}
};
텍스트 엘리먼트가 어떻게 렌더링 되는지 정의했습니다. 다른 엘리먼트와의 차이점은 텍스트 엘리먼트는 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);
}
엘리먼트를 DOM에 자식으로 렌더링 할 수 있게 해주는 render
함수를 만들었습니다. 다음으로 필요한 것은 엘리먼트를 만드는 쉬운 방법입니다. 다음 포스팅에서는 JSX를 Didact와 함께 사용할 수 있도록 만들어보겠습니다.
지금까지 작성한 코드를 돌려보길 원하면 codepen을 확인하거나, github 레포에서 차이점을 확인할 수도 있습니다
다음 포스트: 엘리먼트 생성과 JSX
읽어 주셔서 감사합니다.
저는 Hexacta에서 여러가지를 만들고있습니다.
아이디어가 있거나 저희를 도울 수 있다면 연락주세요.