프론트엔드 멋쟁이들은 이런걸 만들어본다던데
프론트엔드를 진심으로 공부하시는 분들이 해보는 필수 코스가 있습니다.
나만의 리액트 만들어보기....!!!!!!
저도 프론트엔드에 진심이고 React를 꽤나 사랑하기 때문에
직접 만들어보며 리액트의 상세 원리를 코드로 이해해보기로 하였습니다.
📌 가상돔
📌 jsx
export function jsx(type, props, ...children) {
return { type, props: props || {}, children };
}
📌 createElemnet
export function createElement(node) {
if (typeof node === "string") {
return document.createTextNode(node);
}
const element = document.createElement(node.type);
Object.entries(node.props).forEach(([name, value]) => {
element.setAttribute(name, value);
});
node.children.forEach((child) => {
element.appendChild(createElement(child));
});
return element;
}
📌 render
export function jsx(type, props, ...children) {
return { type, props: props || {}, children };
}
export function createElement(node) {
if (typeof node === "string") {
return document.createTextNode(node);
}
const element = document.createElement(node.type);
Object.entries(node.props).forEach(([name, value]) => {
element.setAttribute(name, value);
});
node.children.forEach((child) => {
element.appendChild(createElement(child));
});
return element;
}
function updateAttributes(target, newProps, oldProps) {
Object.entries(oldProps || {}).forEach(([key, _]) => {
if (oldProps[key] !== newProps[key]) {
target.setAttribute(key, newProps[key]);
}
if (!(key in newProps)) {
target.removeAttribute(key);
}
});
}
export function render(parent, newNode, oldNode, index = 0) {
if (!newNode && oldNode) {
parent.removeChild(parent.childNodes);
return;
}
if (newNode && !oldNode) {
const newElementNode = createElement(newNode);
parent.appendChild(newElementNode);
return;
}
if (!newNode && !oldNode) {
return;
}
if (typeof newNode === "string" && typeof oldNode === "string") {
if (newNode !== oldNode) {
parent.childNodes.nodeValue = newNode;
}
return;
}
if (newNode.type !== oldNode.type) {
const newElementNode = createElement(newNode);
parent.replaceChild(newElementNode, parent.childNodes);
return;
}
updateAttributes(parent.childNodes[index], newNode.props, oldNode.props);
const newChildren = newNode.children || [];
const oldChildren = oldNode.children || [];
const maxLength = Math.max(newChildren.length, oldChildren.length);
[...new Array(maxLength)].forEach((_, i) => {
render(parent.childNodes[index], newChildren[i], oldChildren[i], i);
});
}
📌 myReact
import { createHooks } from "./hooks";
import { render as updateElement } from "./render";
function MyReact() {
let currentRoot = null;
let rootComponent = null;
const _render = () => {
if (currentRoot && rootComponent) {
resetHookContext();
const vDom = rootComponent();
updateElement(currentRoot, vDom, currentRoot._vDom);
currentRoot._vDom = vDom;
}
};
function render($root, component) {
currentRoot = $root;
rootComponent = component;
_render();
}
const {
useState,
useMemo,
resetContext: resetHookContext,
} = createHooks(_render);
return { render, useState, useMemo };
}
export default MyReact();
📌 batch기능 추가
export function createHooks(callback) {
let states = [];
let currentIndex = 0;
let pendingUpdate = false;
const useState = (initState) => {
const stateIndex = currentIndex;
if (states.length === stateIndex) {
states.push(initState);
}
const state = states[stateIndex];
const setState = (newState) => {
if (states[stateIndex] === newState) {
return;
}
states[stateIndex] = newState;
pendingUpdate = true;
queueMicrotask(() => {
if (pendingUpdate) {
pendingUpdate = false;
callback();
}
});
};
currentIndex++;
return [state, setState];
};
const areArraysEqual = (arr1, arr2) => {
if (!arr1 || !arr2 || arr1.length !== arr2.length) return false;
return arr1.every((value, index) => value === arr2[index]);
};
const useMemo = (fn, deps) => {
const memoIndex = currentIndex++;
const lastMemo = memos[memoIndex];
if (!lastMemo || !deps || !areArraysEqual(lastMemo.deps, deps)) {
const newValue = fn();
memos[memoIndex] = { value: newValue, deps };
return newValue;
}
return lastMemo.value;
};
const resetContext = () => {
currentIndex = 0;
};
return { useState, useMemo, resetContext };
}
📌 트러블 슈팅
pendingUpdate = true;
queueMicrotask(() => {
if (pendingUpdate) {
pendingUpdate = false;
callback();
}
});
let count = 0; const App = () => { const [state, setState] = react.useState(0); const handleClick = () => { setState(state + 1); setState(state + 2); setState(state + 3); }; return jsx('div', null, jsx('button', { onClick: handleClick }, `Clicked ${count++} times`), `State: ${state}` ); }; react.render(document.getElementById('root'), App);
- handleClick 함수 호출
- setState 3번 호출, pendingUpdate 플래그 true
- handleClick 함수 종료, 스택 비움
- 마이크로태스크 큐의 첫 번째 queueMicrotask 콜백 함수 실행
- pendingUpdate 플래그 확인, true이므로 리렌더링 함수 호출
- 리렌더링 함수 실행, 실제 DOM 업데이트
- pendingUpdate 플래그 false
- 마이크로태스크 큐의 두 번째 queueMicrotask 콜백 함수 실행
- pendingUpdate 플래그 확인, false이므로 실행 X
- 마이크로태스크 큐의 세 번째 queueMicrotask 콜백 함수 실행
- pendingUpdate 플래그 확인, false이므로 실행 X
무척 어렵지만, 작은 나만의 리액트를 만들어보다니 무척 즐겁습니다.
리액트를 좀 더 세세하게 배울 수 있었습니다.
코드는 알고 보면 쉽지만, 모를 때는 무서운 존재 같습니다.
뭐든 도전하는게 중요하다는 마음으로 앞으로도 임하려 합니다.
화이팅!!!!
안녕하세요
직접 구현한 작은 리액트 글 너무 유익하고 재밌게 잘 봤습니다
궁금한점이 있어 댓글을 남깁니다
보통 리액트에서 의미하는 batch update는 효율적인 DOM 변경을 위해 다수의 상태(state)가 변경되었을때 UI를 일괄(동시) 업데이트 하는것을 의미합니다
예를들어 한개의 이벤트 핸들러 안에서 두개의 상태 변경이 동기적으로 호출되었을때, 각각 UI 업데이트(re-render re-paint)를 하는것이 아닌, 한 커밋 페이즈에 UI를 모두 업데이트하는것으로 알고있습니다.
구현하신 batch 기능은 batch update보단 debounce에 가깝지 않나 생각이 되는데 어떻게 생각하실까요?!