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의 주요 특징들을 이해하는데 도움이 됩니다.