2025년 05월 05일
React는 직접 DOM을 조작함으로 발생했던 성능 이슈를 해결하기 위해 가상 DOM을 도입했습니다. DOM의 변경 사항을 확인하기 위해 모든 부분을 직접 비교하는 것이 아닌, 가상 DOM을 생성하여 변경된 부분만 실제 DOM에 적용합니다.
특히 React는 원하는 UI의 상태를 알려주면 자동으로 DOM은 업데이트가 됩니다. 즉, 선언적 API가 가능하여 사용자는 깊게 고민하지 않고도 원하는 상태의 UI를 확인할 수 있습니다.
그러나 내부적으로는 기존의 가상 DOM과 변경 사항이 생긴 가상 DOM을 비교하여 반영하는 작업이 발생하는데 이러한 과정을 재조정 이라고 합니다.
재조정(Reconciliation)이란 React가 UI를 갱신할 때 가상 DOM에서 변경 사항을 식별하여 실제 DOM에 반영하는 프로세스를 의미합니다.
쉽게 말해, React는 UI를 그릴 때 전체를 새롭게 그리지 않고, 이전 상태와 새로운 상태를 비교하여 달라진 부분만 최소한의 비용으로 업데이트 합니다.
이렇게 하면 성능이 크게 향상됩니다.
재조정의 필요성을 이해하려면 React가 등장하기 이전의 DOM 조작 방식을 떠올려 보면 됩니다. 이전 가상 DOM에 대해 알아봤으므로 이번에는 간단하게만 살펴보겠습니다.
과거에는 자바스크립트로 DOM을 직접 변경했습니다. 이전에도 말했지만 이러한 직접적인 조작은 빈번하고 복잡한 DOM 조작에서 성능 저하를 유발했습니다.
const element = document.getElementById('title')
element.textContent = 'Hello World'
React는 이를 해결하기 위해 가상 DOM을 도입합니다. 따라서 정리하자면 아래와 같습니다.
이렇게 직접적인 DOM 조작을 피함으로 성능 향상을 이루어냈습니다.
React는 컴포넌트를 처음 렌더링할 때 메모리에 가상 DOM 트리를 만듭니다.
// 컴포넌트 최초 렌더링
function App() {
return <h1>Hello World</h1>
}
이 JSX는 다음과 같은 형태로 변환됩니다.
{
type: 'h1',
props: { children: 'Hello World'}
}
상태(state)나 속성(props)이 변경되면 React는 새로운 가상 DOM 트리를 만듭니다.
예를 들어, 상태가 변경되었다고 가정해보겠습니다.
function App({name}) {
return <h1>Hello {name}</h1>
}
// 이전 상태
<App name="World" />
// 새로운 상태
<App name="React" />
위 코드에서 두 경우의 가상 DOM은 다음과 같습니다.
// 이전 상태
{type: 'h1', props: {children: 'Hello World'}}
// 이후
{type: 'h1', props: {children: 'Hello React'}}
React는 이전 가상 DOM과 새로운 가상 DOM의 차이를 식별합니다. 이 때 사용하는 알고리즘을
Diffing 알고리즘 이라고 합니다.
Diffing 알고리즘은 다음 원칙을 따릅니다.
예를 들어 아래와 같은 경우는 <h1>은 변화가 없고, <p> 태그의 자식만 변경되는데, React는 이 점을 인식하여 <p>의 내용만 업데이트합니다.
// 이전 렌더링
<div>
<h1>React</h1>
<p>Hello</p>
</div>
// 이후 렌더링
<div>
<h1>React</h1>
<p>World</p>
</div>
또한 React는 재조정 과정에서 여러 가상 DOM 업데이트를 모아 한 번의 DOM 업데이트로 결합한 후 실제 DOM에 대한 업데이트를 일괄 처리 합니다.
이 과정은 아래 단계에서 자세히 볼 수 있습니다.
Diffing 과정에서 발견한 변경 사항을 실제 DOM에 적용하는 단계를 Commit 단계 라고 합니다.
이 때 React는 최소한의 DOM 연산만 수행합니다.
이렇게 하면 불필요한 DOM 연산을 줄이고 성능을 향상시킬 수 있습니다.
리스트를 다룰 때 React에서 재조정 효율성을 높이려면 키(Key) 사용이 필수입니다.
리스트 아이템이 추가되거나 삭제 될 때 React가 변경된 위치를 찾기 어렵습니다. 그러면 DOM을 불필요하게 많이 업데이트 하는 경우가 생길 수 있습니다.
// Key 없는 경우
{
items.map((item) => <li>{item.text}</li>)
}
불필요한 DOM 업데이트를 최소화합니다.// 올바른 예시 (Key가 있는 경우)
{
items.map((item) => <li key={item.id}>{item.text}</li>)
}
그렇다면 이 재조정 과정에 근간이 되는 파이버 재조정자(Fiber Reconciler)에 대해 알아보겠습니다. 사실 React는 v16 버전부터 파이버 재조정자를 사용했는데 그 이전에는 스택 재조정자를 사용하여 재조정하였습니다.
그래서 이러한 문제를 해결하기 위해 Fiber 아키텍처를 도입하여 이러한 문제를 해결했습니다.
파이버 재조정자는 조정자를 위한 작업 단위를 나타내는 파이버라는 데이터 구조가 사용됩니다.
파이버의 핵심 아이디어는 렌더링 작업을 작은 단위로 쪼갠 후, 우선순위를 정해 비동기적으로 처리하는 것입니다.
이렇게 하면 렌더링 작업을 더 작은 단위로 분할할 수 있고, 우선순위를 정해 중요한 작업을 먼저 처리하여 스택 재조정자의 문제를 해결할 수 있습니다.
파이버에서 모든 UI 구성 요소는 파이버 노드 라는 객체 형태로 나타납니다.
아래와 같은 예시를 보며 이해할 수 있습니다.
<div>
<h1>Hello</h1>
<p>World</p>
</div>
위 코드는 아래와 같은 파이버 노드로 변환됩니다.
또 이렇게 연결된 파이버 노드는 파이버 트리를 구성합니다.
FiberNode {
type: 'div', // 컴포넌트 유형
props: {children: [...]}, // 속성 값
stateNode: HTMLElement, // 실제 DOM 요소에 대한 참조
child: FiberNode (h1), // 자식
sibling: FiberNode (p), // 형제
return: FiberNode (parent), // 부모 노드 연결결
alternate: FiberNode (previous state), // 이전 렌더링 상태를 담고 있는 노드 (비교 위한 용도)
}
정리하자면, 파이버 재조정자(Fiber Reconciler)는 파이버 트리를 이용해 UI를 업데이트 하는 React의 내부 알고리즘입니다.
쉽게 말하면, 파이버 노드 간의 차이를 계산하여, 화면을 최소 비용으로 업데이트 하는 역할을 합니다.
주요 프로세스는 두 단계로 나뉩니다.
작업 단위로 나누어 처리합니다. 이 때 각 작업 단위는 파이버 노드 하나를 처리합니다.
=> 이를 통해 높은 우선순위 작업이 발생하면 낮은 우선순위 작업을 잠시 중단하고, 중요한 작업을 먼저 처리할 수 있습니다.Render 단계에서 찾은 변경 사항을 실제 DOM에 적용합니다. 반드시 동기적으로 처리됩니다.
이 단계에서는 3가지 작업이 진행됩니다.
Before mutation : DOM 변경 전에 실행되는 라이프사이클 메서드 실행Mutation : 실제 DOM 업데이트Layout : DOM 변경 후 라이프사이클 메서드 실행이 Commit 단계에서는 중간에 중단될 수 없으며 반드시 끝까지 완료되어야합니다.
파이버 재조정자를 사용한 재조정은 가상 DOM을 이용해 UI 업데이트를 최적화하고자 하는 React의 핵심 원리입니다. 이러한 재조정 과정을 통해 React는 동적이고 복잡한 UI를 다루는 앱에서도 뛰어난 성능과 사용성을 제공할 수 있게 되었습니다. 아직은 개념적으로만 와닿고 있지만 실제 코드를 보면서 이해해보는 과정도 반드시 거쳐야할 것 같습니다. 나름 서서히 React를 이해하고 있는 것 같아 기분이 좋아 다음 글도 빠른 시일내에 작성해보도록 하겠습니다.