리액트는 재조정 과정에서 여러 가상 DOM 업데이트를 모아 한 번의 DOM 업데이트로 결합한 후 실제 DOM에 대한 업데이트를 일괄 처리한다.
이런 방식은 실제 DOM을 업데이트하는 횟수를 줄여 성능이 개선된다.
기존 리액트는 렌더링에 스택 데이터 구조를 사용했다.
이는 애플리케이션의 규모가 커지고 복잡해짐에 따라 문제가 발생하였다.
스택 재조정자는 업데이트의 우선순위를 설정하지 않기 때문에 중요한 업데이트를 방해할 수 있다. 계산 비용이 크다는 등의 이유로 중요 업데이트의 시간이 오래 걸리면 사용자 인터페이스가 느려지거나 끊김 현상이 발생할 수 있다.
또한 스택 조정자는 업데이트를 중단하거나 취소를 할 수 없다. 불필요한 업데이트가 사용자 상호 작용을 방해하게 되면 이로 인해 가상 트리와 DOM에서 불필요한 작업이 수행되어 애플리케이션의 성능에 부정적 영향을 미칠 수 있다.
파이버 재조정자는 조정자를 위한 작업 단위를 나타내는 파어버라는 데이터 구조가 사용된다.
파이버는 리액트 엘리먼트에서 생성된다.
엘리먼트와 파이버의 차이
파이버는 상태를 저장하고 수명이 긴 반면 리액트 엘리먼트는 임시적이고 상태가 없다.
파이버 재조정자는 업데이트의 우선순위를 정하고 이에 따라 동시 실행을 가능케 하여 리액트 애플리케이션의 성능과 응답성을 향상시킨다.
본질적으로 파이버 데이터 구조는 리액트 애플리케이션에서 컴포넌트 인스턴스와 그 상태를 표현한다.
변경 가능한 인스턴스로 설계되었으며 조정 과정에서 필요에 따라 업데이트되고 재배치된다.
파이버 노드의 각 인스턴스에는 다음과 같은 정보들이 포함되어있다.
파이버 재조정에는 현재 파이버 트리와 다음 파이버 트리를 비교해 어느 노드를 업데이트, 추가, 제거할지 파악하는 작업이 포함된다.
조정 과정 중에 파이버 재조정자는 가상 DOM의 각 리액트 엘리먼트에 대해 파이버 노드를 생성한다. (createFiberFromTypeAndProps
함수가 수행)
이 함수는 엘리먼트에서 파생된 파이버를 반환한다. 파이버 노드가 생성되면 파이버 재조정자는 작업 루프를 사용해 사용자 인터페이스를 업데이트한다.
작업 루프는 루트 파이버 노드에서 시작해 컴포넌트 트리를 따라 내려가면서(beginWork)
업데이트가 필요한 경우 각 파이버 노드를 '더티'로 표시한다. 끝에 도달하면 다시 반대로 순회하면서(completeWork)
브라우저의 DOM 트리와는 분리된 새 DOM 트리를 메모리에 생성한다.
파이버 아키텍처는 다음 화면을 밖에서 준비한 다음 현재 화면으로 내보내는 더블 버퍼링 개념을 착안한 것이다.
- 컴퓨터 그래픽 및 비디오 처리에서 깜빡임을 줄이고 체감 성능을 개선하는 기술
- 이미지나 프레임을 저장하기 위한 두 개의 버퍼(또는 메모리 공간)를 생성하고 일정한 간격으로 두 버퍼를 전환해 최종 이미지나 동영상을 표시
파이버 재조정은 더블 버퍼링과 유사하게 업데이트가 발생하면 현재 파이버 트리가 포크되어 주어진 사용자 인터페이스의 새로운 상태를 반영하도록 업데이트된다.
그 후 현재 트리를 대체할 트리를 준비하고 사용자가 기대하는 상태를 반영하면, 현재 파이버 트리와 교체한다.
이를 재조정의 커밋 (커밋 단계) 라고 한다.
작업용 트리를 사용할 때 다음과 같은 장점이 있다.
렌더링 단계
현재 트리에서 상태 변경 이벤트가 발생하면 시작된다.
리액트는 각 파이버를 재귀적, 단계적으로 순회하고 업데이트가 보류 중이라는 신호 플래그를 설정해 대체 트리에 오프스크린 변경 작업을 수행한다. 이는 beginWork
함수에서 발생한다.
beginWork
이 함수는 작업용 트리에 있는 파이버 노드의 업데이트 필요 여부를 나타내는 플래그를 설정한다.
계속 다음 노드로 이동하며 맨 아래 노드에 도달할 때까지 동일 작업을 수행하고 완료되면 completeWork
를 호출하고 다시 올라가며 순회한다.
completeWork
이 함수는 작업용 파이버 노드에 업데이트를 적용하고 애플리케이션의 업데이트된 상태를 나타내는 실제 DOM 트리를 새롭게 생성한다.
이 작업을 통해 DOM에서 분리된 트리를 브라우저가 시각적으로 표현하는 영역 바깥에 구성한다.
앨리먼트를 생성하였더라도 화면에는 그려진 상태가 아니기 때문에 더 높은 업데이트가 에약되면 만들어진 앨리먼트는 버려질 수 있다
completeWork가 트리 맨 위에 도달하여 새 DOM 트리를 구성하면 '렌더링 단계가 완료되었다'고 볼 수 있다.
커밋 단계
커밋 단계에서는 렌더링 단계에서 가상 DOM에 적용된 변경 사항을 실제 DOM에 반영한다.
이 단계에서 새 가상 DOM 트리가 호스트 환경에 커밋되고 작업용 트리가 현재 트리로 바뀌게 된다.
변형 단계
커밋 단계의 첫 부분으로, 가상 DOM에 적용된 변경 사항을 실제 DOM에 반영한다.
commitMutationEffects
함수로 업데이트를 진행하고, commitUnmount
및 commitDeletion
함수로 제거하기도 한다.
레이아웃 단계
변형 단계 이후 DOM에서 업데이트된 노드의 새 레이아웃을 계산한다.
이 작업은 commitLayoutEffects
함수가 호출된다.
효과
커밋 단계에서는 여러 부작용이 특정 순서로 실행되며, 그 순서는 효과 종류에 따라 달라진다.
// 이 코드를 만드는 과정
<div id="container">
<h1>안녕하세요!</h1>
<p>이것은 리액트 파이버 재조정 예시입니다.</p>
</div>
// 1. 파이버 객체의 기본 구조
class Fiber {
constructor(type, props) {
this.type = type; // 컴포넌트 타입 (예: 'div', 'span', 또는 함수 컴포넌트)
this.props = props; // 프로퍼티
this.dom = null; // 실제 DOM 노드
// 파이버 트리 구조
this.parent = null; // 부모 파이버
this.child = null; // 첫 번째 자식 파이버
this.sibling = null; // 다음 형제 파이버
// 작업 관련 정보
this.alternate = null; // 이전 파이버 트리의 해당 파이버 (현재와 작업 중인 파이버 트리를 전환하기 위함)
this.effectTag = null; // 변경사항 표시 (예: 'PLACEMENT', 'UPDATE', 'DELETION')
// 작업 우선순위
this.expirationTime = 0;
}
}
// 2. 현재 파이버 트리와 작업 중인 파이버 트리
let currentRoot = null; // 현재 화면에 렌더링된 파이버 트리
let workInProgressRoot = null; // 작업 중인 파이버 트리
let nextUnitOfWork = null; // 다음 작업 단위
// 3. 재조정 시작 - render 함수
function render(element, container) {
// 작업 중인 파이버 트리의 루트 생성
workInProgressRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
// 다음 작업 단위 설정
nextUnitOfWork = workInProgressRoot;
// 작업 스케줄링
requestIdleCallback(workLoop);
}
// 4. 작업 루프 - 브라우저의 유휴 시간에 실행
function workLoop(deadline) {
let shouldYield = false;
// 다음 작업 단위가 있고, 브라우저가 여전히 유휴 상태인 동안 작업 수행
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
// 모든 작업이 완료되면 변경사항을 실제 DOM에 반영
if (!nextUnitOfWork && workInProgressRoot) {
commitRoot();
}
// 작업이 남아있으면 다시 스케줄링
requestIdleCallback(workLoop);
}
// 5. 작업 단위 수행 - 파이버 작업의 핵심
function performUnitOfWork(fiber) {
// 1단계: DOM 노드 생성 또는 업데이트 (Render 단계 - beginWork)
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// 2단계: 자식 파이버 생성 (Render 단계 - reconcileChildren)
const elements = fiber.props.children || [];
reconcileChildren(fiber, elements);
// 3단계: 다음 작업 단위 찾기 (Render 단계 - completeWork)
// 깊이 우선 탐색: 자식 → 형제 → 부모의 형제 순서로 탐색
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
return null;
}
// 6. DOM 노드 생성
function createDom(fiber) {
const dom =
fiber.type === 'TEXT_ELEMENT'
? document.createTextNode("")
: document.createElement(fiber.type);
// 속성 설정
updateDom(dom, {}, fiber.props);
return dom;
}
// 7. 자식 요소 재조정 (비교 및 업데이트)
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
// 비교: 같은 타입인지 확인
const sameType = oldFiber && element && element.type === oldFiber.type;
// 업데이트: 같은 타입이면 기존 노드 업데이트
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
};
}
// 생성: 새로운 요소가 있지만 이전 파이버가 없으면 새로 생성
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
// 삭제: 이전 파이버는 있지만 해당 요소가 없으면 삭제
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
// 삭제할 파이버들을 모아두기
deletions.push(oldFiber);
}
// 이전 파이버의 형제로 이동
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
// 파이버 트리 구조 연결
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
// 8. DOM 업데이트
function updateDom(dom, prevProps, nextProps) {
// 이전 속성 제거
Object.keys(prevProps)
.filter(key => key !== 'children' && !key.startsWith('on'))
.filter(key => !(key in nextProps))
.forEach(key => {
dom[key] = '';
});
// 새 속성 추가/업데이트
Object.keys(nextProps)
.filter(key => key !== 'children' && !key.startsWith('on'))
.filter(key => prevProps[key] !== nextProps[key])
.forEach(key => {
dom[key] = nextProps[key];
});
// 이벤트 리스너 처리
// 이전 이벤트 리스너 제거
Object.keys(prevProps)
.filter(key => key.startsWith('on'))
.forEach(key => {
const eventType = key.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[key]);
});
// 새 이벤트 리스너 추가
Object.keys(nextProps)
.filter(key => key.startsWith('on'))
.forEach(key => {
const eventType = key.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[key]);
});
}
// 9. 삭제할 파이버 목록
let deletions = [];
// 10. 변경사항 커밋 (Commit 단계)
function commitRoot() {
// 삭제 작업 수행
deletions.forEach(commitWork);
// 변경사항 커밋
commitWork(workInProgressRoot.child);
// 현재 트리를 작업 중이던 트리로 교체
currentRoot = workInProgressRoot;
workInProgressRoot = null;
deletions = [];
}
// 11. 각 파이버의 변경사항 커밋
function commitWork(fiber) {
if (!fiber) return;
const parentDom = fiber.parent.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
// 새 노드 추가
parentDom.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
// 노드 업데이트
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
// 노드 삭제
parentDom.removeChild(fiber.dom);
}
// 재귀적으로 자식과 형제 파이버 커밋
commitWork(fiber.child);
commitWork(fiber.sibling);
}
// 12. 사용 예시
const element = {
type: "div",
props: {
id: "container",
children: [
{ type: "h1", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "안녕하세요!" } }] } },
{ type: "p", props: { children: [{ type: "TEXT_ELEMENT", props: { nodeValue: "이것은 리액트 파이버 재조정 예시입니다." } }] } }
],
},
};
// 13. 렌더링 시작
const container = document.getElementById("root");
render(element, container);
파이버 재조정의 핵심 개념