티스토리 뷰
const element = (
<div id="container">
<input type="text" value="name" />
<a href="/index">link to index</a>
<span className="description" onClick={() => console.log("click")}>
click me...
</span>
</div>
)
React에서 JSX로 위와 같이 작성하면 Babel을 통해 만들어진 결과물을 볼 수 있다. Babel 공식 문서에는 결과물에 대한 예제가 다음과 같이 나와 있다.
// input
const profile = (
<div>
<img src="avatar.png" className="profile" />
<h3>{[user.firstName, user.lastName].join(" ")}</h3>
</div>
);
// output
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
const profile = _jsxs("div", {
children: [
_jsx("img", {
src: "avatar.png",
className: "profile"
}),
_jsx("h3", {
children: [user.firstName, user.lastName].join(" "),
}),
],
});
이것을 이용해서 React에서 제공하는 `createElement()`, `render()`와 동일한 기능을 하는 함수를 만들어봤다. 물론 실제와는 다르지만 React의 동작 방식을 이해하기 위해 시도해보기로 했다.
기본적으로 output은 type과 props다.
type은 Element의 tag이고, props는 해당 Element의 id, value, href, children과 같은 요소들이다. 위의 JSX output이 다음과 같다고 가정하고(완전 단순화) 해당 output을 만들어내는 createElement 함수와 createElement 함수의 output을 DOM에 그리는 render 함수를 구현했다.
createNode
위의 JSX를 그리기 위해 createNode 함수를 사용한다면 ouput은 다음과 같다.
const element = {
type: "div",
props: {
id: "container",
children: [
{
type: "input",
props: {
value: "name",
type: "text",
},
},
{
type: "a",
props: {
href: "/index",
children: "link to index",
},
],
},
{
type: "span",
props: {
className: "description",
children: "click me...",
},
},
],
},
};
DOM은 트리 구조다. 그러니까 Element에 type이 있고, props가 있고, props에는 children이 있다. 이 children은 다시 type과 props를 가지고, 그 props에는 children이 있고... 이런 구조다.
React 공식 문서에는 JSX 없이 사용하는 React 라는 파트가 있다. React를 사용할 때 JSX는 필수가 아니며, 빌드 환경에서 컴파일 설정을 하고 싶지 않을 때 JSX 없이 React를 사용하는 것이 훨씬 편리할 수 있다고 설명한다.
// JSX
class Hello extends React.Component {
render() {
return <div>Hello {this.props.toWhat}</div>;
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Hello toWhat="World" />);
// no JSX
class Hello extends React.Component {
render() {
return React.createElement('div', null, `Hello ${this.props.toWhat}`);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(React.createElement(Hello, {toWhat: 'World'}, null));
`React.createElement` 함수의 파라미터로 div, null, `Hello ${this.props.toWhat}`을 넘겨주고 있는데, 이는 각각 type, props, children이다. 위 예제의 child는 text node다. child는 위와 같이 text node이거나 그냥 node다. text node는 일반적인 node와 달리 `document.createTextNode()`로 생성해야 한다. 여기에서 분기가 생겨서 조금 고생했다. 일반적인 노드는 `document.createElement()`로 생성한다.
`React.createElement` 역할을 할 `createNode()` 함수는 다음과 같이 작성했다.
function createNode(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
return typeof child === 'string'
? createTextNode(child)
: child;
}),
},
};
}
function createTextNode(text) {
return {
type: 'TEXT_NODE',
props: {
nodeValue: text,
children: [],
},
};
}
`{ type, props, ...child }` 형태를 만들어줄 수 있도록 구현했다. 다만 child가 string인 경우에는 text node인데, Babel output을 보면 text node를 따로 type, props가 있는 형태로 처리하지 않고 그냥 child에 string을 담는다. node를 생성하는 데에는 별 문제가 없었는데, 이후에 render 함수를 구현할 때는 이 부분을 분기하는 것이 어려웠다.
어떻게 분기해야 할지 고민하다가 TEXT_NODE를 type으로 갖는 똑같은 형태의 객체를 생성하고, render 함수 내부에서 type이 TEXT_NODE인 경우 `document.createTextNode`를 호출, nodeValue를 이용하는 쪽으로 구현했다.
최종적으로 맨 위의 JSX와 같은 결과물을 내려면 다음과 같이 사용하면 된다.
// 주어진 html
const element = (
<div id="container">
<input type="text" value="name" />
<a href="/index">link to index</a>
<span className="description" onClick={() => console.log("click")}>
click me...
</span>
</div>
)
// createNode
const element = createNode(
'div',
{ id: 'container' },
createNode('input', { value: 'name', type: 'text' }),
createNode('a', { href: '/index' }, 'link to index'),
createNode(
'span',
{ className: 'description', onClick: () => console.log('click') },
'click me...',
),
);
render
document.addEventListener('DOMContentLoaded', () => {
const rootElement = document.querySelector('#root');
render(element, rootElement);
});
최상위 요소인 root가 parentNode가 되고, 위에서 작성한 element를 그 안에 그려주는 식으로 동작한다. render 함수는 element와 rootElement를 파라미터로 받는다.
function render(element, parentElement) {
const { type, props } = element;
const node = type === 'TEXT_NODE'
? document.createTextNode('')
: document.createElement(type);
// Render Children
props.children.forEach((child) => render(child, node));
parentNode.appendChild(node);
}
type이 TEXT_NODE인 경우 `document.createTextNode("")`, 아니면 `document.createElement(type)`으로 노드를 생성한다. 그리고 props 내부에 있는 children을 반복문으로 돌려서 parentNode 안에 넣는다. 원래 `const childNodes = props.children || []` 형태로 할당 후 사용했는데, 생각해보니 어차피 createNode, 또는 createTextNode 함수를 탄다면 children은 무조건 배열이라 불필요한 할당이라 제거했다.
props 내부에 children 말고 올 수 있는 것들은 프로퍼티, 또는 이벤트들이다. 이 부분을 생성해주는 로직은 다음과 같이 구현했다.
function render(element, parentElement) {
const { type, props } = element;
const node = type === 'TEXT_NODE'
? document.createTextNode('')
: document.createElement(type);
// Set EventListener
const isEvent = (key) => key.startsWith('on');
Object.keys(props)
.filter(isEvent)
.forEach((key) => {
const eventType = key.toLowerCase().substring(2);
node.addEventListener(eventType, props[key]);
});
// Set Properties
const isProperty = (key) => key !== 'children' && !isEvent(key);
Object.keys(props)
.filter(isProperty)
.forEach((key) => {
node[key] = props[key];
});
// Render Children
props.children.forEach((child) => render(child, node));
parentNode.appendChild(node);
}
이벤트의 경우 'on'을 prefix로 가지기 때문에 위와 같이 구현했다. property는 children과 on으로 시작하는 이벤트 키워드를 제외한 나머지 모두이다. text node의 경우 `node["nodeValue"] = props["nodeValue"]`가 된다.
Dumb Down React Repository Link
GitHub - oxahex/dumb-down-react: React 구현 방식을 이해하고자 기능을 단순화하였습니다.
React 구현 방식을 이해하고자 기능을 단순화하였습니다. Contribute to oxahex/dumb-down-react development by creating an account on GitHub.
github.com
React의 기능을 단순화해서 하나씩 만들어보고 싶어 시작했는데 꽤 재미있었다.
이렇게 하면 몇 가지 문제가 생기는데, 일단 render 작업이 진행되는 동안에는 render 동작을 정지할 수 없다. 도중에 변경점이 생겼을 때 진행하면 render를 중지하고 즉각적으로 반응하려면 어떻게 해야 할까? 어떻게 저 render를 잠깐 멈췄다가 이어서 작업하도록 할 수 있을까? 이 부분을 조금 더 고민해봐야 겠다.
'Note' 카테고리의 다른 글
| Spring은 DB Transaction을 어떻게 알아서 처리할까? (0) | 2024.05.24 |
|---|---|
| Java Optional (0) | 2024.05.24 |
| React reconciliation 구현해보기 (0) | 2024.05.24 |
| React Concurrent Mode 구현해보기 (0) | 2024.05.24 |
| 테스트 코드 (0) | 2024.05.23 |
- Total
- Today
- Yesterday
- Dreamhack
- CSRF
- PS
- Framework
- sql injection
- webgoat
- Database
- SEO
- math
- sqli
- 회고
- opengraph
- Spring Security
- Transaction
- oauth2
- test
- askers
- linux
- DP
- Misc
- XSS
- JPA
- React
- WEB
- Spring
- Bandit
- WarGame
- java
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |