글을 작성하기에 앞서 React공식 문서와 구글링을 통한 자료수집으로 공부를 해봤다. 공부를 하면서 느낀 점은 정말 대단한 사람들이 대단한 작품을 만들었다는 생각이 들었다.
기초 개념이 공감갔고 왜 이렇게 구현했을까에 대한 호기심도 어느 정도 해결할 수 있었지만, 이것들을 깨닫는다고 해서 제로에서부터 구현하는 것은 어림도 없다는 것도 깨달을 수 있었다.
그래서 코드 구현 자체는 Reference와 별다를 것이 없을 것이다.
다만 내가 이것을 파고들어 공부하면서 깨닫는 것들을 같이 기록하고 어떤 생각으로 개발자들이 만들었을지를 느낄 수 있다는 것만으로도 큰 공부가 될것이라고 생각한다.
그리고 작성하는 코드들을 최대한 이해하려하고 주석이나 아래 comment의 형태로 남겨보려고 한다.
먼저 리액트는 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를 사용할 수 있으므로 구현해야 한다는 것이다.
// 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!')
);
위에서 생성한 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);
}
이렇게 구현을 하고나니 templete string을 사용하지 않고 자식을 추가하는 방식으로 마지막에 호출하는 부분에서만 DOM에 접근할 수 있게 되었다!
const container = document.getElementById('root');
Bingact.render(element, container);
단순하다. #root인 element를 가져오고 위에서 JSX문법 또는 Bingact.createElement()
로 생성한 element를 추가하는 코드이다.
다만 여기서 JSX문법을 사용하려면 몇가지 설정해야하는 것이 있다.
// 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를 사용하게 된다.
지금은 라이브러리를 구현하는 데 신경쓰느라 경로도 복잡하지 않지만 만약 이것을 이용해 실제로 개발한다고하면 어떻게 될까?라는 생각이 들어 절대 경로도 설정해주었다.
// babel.config.json
{
...
"plugins": [
[
"module-resolver",
{
"alias": {
"Bingact": "./src/Bingact"
}
}
]
]
}
방법은 간단하다. 위에서 jsx코드의 트랜스파일링을 실행하기위해 작성한 babel config파일에서 alias를 작성하는 데 왼쪽은 절대경로로 사용할 이름, 오른쪽이 생략하기 위한 경로라고 생각하고 설정해주면 된다. 만약 왼쪽을 "@@@@####"으로 바꾼다면 우리는
import Bingact from '@@@@####';
의 형태로 사용할 수 있는 것이다.
이후 npm i -D babel-plugin-module-resolver
를 실행하게 된다면 우리가 만든 라이브러리를 절대경로로 사용할 수 있게 되는 것이다!
함수형 컴포넌트를 만들자는 목표를 세우고 createElement와 그것을 render하는 함수까지 구현해보았다. 쉽지 않은 내용이지만 막상 JS의 메소드들과 언어적 특성을 사용해 구현하는 것을 알 수 있었던 것 같다. 공부할 수 있던 것을 간략하게 정리해 보자면
setAttribute()
메소드를 이용하는 것이 아닌 Object의 성질을 이용해서 key-value로 접근할 수 있다는 것을 깨달았다.해결해야할 부분이 남아있다.
<div>
<Element />
{Element()}
</div>
위의 Element 코드가 우리가 흔히 사용하는 방법이지만 아직 동작하지 않았다. 현재는 아래와 같은 방식으로 JSX코드를 반환하는 함수를 직접 실행해야만 적용이 되고 있었다.
Reference
나만의 리액트 라이브러리 만들기
build your own react in 90 lines of javascript