2024년 1월 정보들
Table of Contents
1 [250107] build-your-own-react
링크 : https://pomb.us/build-your-own-react/
예전에 보았던 블로그를 다시한번 읽어보게되었다. 리액트를 실무에서 사용한 적 없지만, 아주 좋은 글임을 확실하다. 프론트엔드 시장에서 한 획을 그은 리액트의 코어로직에 대해서 많이 알게 되었다.
1.1 JSX -> createElement
JSX는 Babel 을 이용하여 Javascript로 변환된다. 이것으로 가상DOM 객체를 생성할 수 있다.
// JSX 코드 /** @jsx Didact.createElement */ <div className="container"> <h1>Hello</h1> </div> // Babel이 변환한 JavaScript 코드 Didact.createElement( "div", { className: "container" }, Didact.createElement("h1", null, "Hello") )
createElement
는 이런 형태다
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", props: { nodeValue: text, children: [], }, } }
1.2 fiber 재조정
리액트는 Fiber라는 자료구조를 사용하여 가상DOM을 관리한다.
{ type: string | function, // DOM 엘리먼트 타입 또는 함수형 컴포넌트 dom: DOM Node, // 실제 DOM 노드 props: Object, // 속성들 parent: Fiber, // 부모 Fiber child: Fiber, // 첫번째 자식 Fiber sibling: Fiber, // 형제 Fiber alternate: Fiber, // 이전 트리의 Fiber effectTag: string, // "PLACEMENT", "UPDATE", "DELETION" 중 하나 }
작업단위로 나누어서 처리하며, 이를 통해 브라우저가 렌더링을 중단하지 않고도 작업을 수행할 수 있다.
function workLoop(deadline) { let shouldYield = false while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork) shouldYield = deadline.timeRemaining() < 1 } if (!nextUnitOfWork && wipRoot) { commitRoot() } requestIdleCallback(workLoop) }
작업 처리는
- Render Phase
- Fiber 트리를 생성한다.
- DOM 엘리먼트를 생성하고, 이전 트리와 비교하여 변경사항을 찾는다.
- Commit Phase
- 변경사항을 한번에 반영한다.
Render Phase 는 performUnitOfWork
함수에서 처리된다.
Commit Phase는 performUnitOfWork 에서 변경한 Fiber 트리를 반영한다.
function commitRoot() { deletions.forEach(commitWork) // 삭제된 Fiber를 커밋한다. commitWork(wipRoot.child) // 첫번째 자식 Fiber를 커밋한다. currentRoot = wipRoot wipRoot = null } function commitWork(fiber) { if (!fiber) { return } let domParentFiber = fiber.parent while (!domParentFiber.dom) { domParentFiber = domParentFiber.parent } const domParent = domParentFiber.dom if ( fiber.effectTag === "PLACEMENT" && fiber.dom != null ) { domParent.appendChild(fiber.dom) } else if ( fiber.effectTag === "UPDATE" && fiber.dom != null ) { updateDom( fiber.dom, fiber.alternate.props, fiber.props ) } else if (fiber.effectTag === "DELETION") { commitDeletion(fiber, domParent) } commitWork(fiber.child) commitWork(fiber.sibling) }
1.3 useState Hook
React의 핵심 기능 중 하나인 상태관리를 위한 Hook입니다. useState의 내부동작은 다음과 같다:
1.3.1 초기화와 상태 관리
- 전역 변수로 현재 작업중인 Fiber(wipFiber)와 Hook의 인덱스(hookIndex)를 관리
- Hook은 {state, queue} 형태의 객체로 관리되며 Fiber의 hooks 배열에 순서대로 저장됨
- 컴포넌트가 다시 렌더링될 때 이전 트리(alternate)에서 동일 인덱스의 Hook을 찾아 상태를 복원
1.3.2 상태 업데이트 알고리즘
- setState 호출 시점:
- 업데이트 함수를 Hook의 queue에 추가
- 새로운 Fiber 트리 생성을 트리거
- 다음 작업 단위 설정
- 다음 렌더링 시점:
- 이전 Hook의 queue에 있는 모든 업데이트를 순차적으로 적용
- 새로운 상태 계산 후 queue 초기화
- 새로운 상태와 setState 함수 반환
function useState(initial) { // 이전 Hook의 상태를 가져온다 const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex] // 새로운 Hook을 생성한다 const hook = { state: oldHook ? oldHook.state : initial, // 상태값 queue: [], // 업데이트 큐 } // 3. 이전 큐의 모든 업데이트 적용 const actions = oldHook ? oldHook.queue : [] actions.forEach(action => { // 함수형 업데이트 지원 hook.state = typeof action === 'function' ? action(hook.state) : action }) // setState 함수를 생성한다 const setState = action => { hook.queue.push(action) // 새로운 렌더링을 트리거한다 wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, } // 새로운 wipRoot 를 만들고, nextUnitOfWork에 설정하면 렌더링이 시작된다. // 알겠지만 nextUnitOfWork은 덮어씌워질 수 있음. // 해결방법은 nextUnitOfWorks 로 배열(큐)을 만들어서 순차적으로 처리하면 됨. nextUnitOfWork = wipRoot deletions = [] } // Hook을 저장하고 반환한다 wipFiber.hooks.push(hook) hookIndex++ return [hook.state, setState] }
useState의 주요 특징:
- Hook의 상태는 Fiber 트리에 저장됩니다
- 업데이트는 큐에 저장되어 배치로 처리됩니다
- 상태 변경 시 새로운 렌더링이 트리거됩니다
1.3.3 시간복잡도 분석
- Hook 찾기: O(1) - 인덱스로 직접 접근
- 상태 복원: O(1) - 이전 트리에서 찾아서 복사
- 업데이트 적용: O(n) - 큐에 저장된 모든 업데이트를 순차적으로 적용
1.4 이벤트 처리
React는 이벤트를 Props로 전달받아 처리합니다.
const isEvent = key => key.startsWith("on") const isProperty = key => key !== "children" && !isEvent(key) function updateDom(dom, prevProps, nextProps) { // 이전 이벤트 리스너 제거 Object.keys(prevProps) .filter(isEvent) .filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key) ) .forEach(name => { const eventType = name.toLowerCase().substring(2) dom.removeEventListener(eventType, prevProps[name]) }) // 새로운 이벤트 리스너 추가 Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name.toLowerCase().substring(2) dom.addEventListener(eventType, nextProps[name]) }) }
이벤트 처리의 특징:
- on으로 시작하는 prop을 이벤트로 인식합니다
- 이벤트 리스너를 직접 DOM에 추가/제거합니다
- 이벤트 이름은 소문자로 변환됩니다 (onClick -> click)
이렇게 구현된 미니 React는 실제 React의 핵심 개념들을 잘 보여줍니다. Fiber 아키텍처를 통한 점진적인 렌더링, Hook을 이용한 상태관리, 그리고 선언적인 이벤트 처리 등 React의 주요 특징들을 이해하는데 도움이 됩니다.