렌더링이란 현재 컴포넌트의 props와 state의 상태에 따라 UI를 어떻게 보여줄지 컴포넌트에게 요청하는 작업을 의미합니다.
렌더링이 시작되면 React는 컴포넌트 트리의 루트부터 순회하여 업데이트가 필요한 컴포넌트를 탐색합니다. 업데이트가 필요하다고 표시된 각 컴포넌트에 대해 React는 클래스형 컴포넌트일 경우 classComponentInstance.render()
, 함수형 컴포넌트일 경우 FunctionComponent()
를 호출하여 렌더 결과물을 수집합니다. 컴포넌트의 렌더 결과물은 JSX 구문으로 작성되며, 컴파일 시점에 React.createElement()
호출로 변환됩니다. createElement
는 JS 객체 형식의 React 엘리먼트를 반환하며, 이 객체는 생성되는 UI 구조를 설명합니다.
// 아래과 같은 JSX 문법이 컴파일 되면
return <Component text="test" num={10}>Hello World!<Component/>
// React.createElement를 호출로 변환됩니다.
return React.createElement(Component, {text: "test", num: 10}, "Hello World!")
// 위 결과는 이런 엘리먼트 객체를 반환합니다.
{type: Component, props: {text: "test", num:10}, children:["Hello World!"]}
React는 전체 컴포넌트 트리에서 렌더 결과물을 수집한 후 새로운 Virtual DOM 트리와 이전 Virtual DOM 트리를 비교하고, 실제 DOM에 적용해야 할 변경 사항을 계산하는 재조정(Reconciliation) 과정을 수행합니다. React는 이렇게 계산된 모든 변경 사항을 하나의 동기적 시퀀스(Batch Update)로 실제 DOM에 적용합니다.
실제 DOM과 같은 내용을 담은 복사본으로 실제 DOM이 아닌 객체 형태로 메모리상에 저장되어 있습니다.
리액트는 항상 2개의 Virtual DOM를 가지고 있습니다.
1 ) 렌더링 이전 화면 구조를 나타내는 Virtual DOM
2 ) 렌더링 이후에 보이게될 화면 구조를 나타내는 Virtual DOM
리액트는 실제 브라우저 화면에 그려지기 이전 렌더링이 발생될 상황이 되면 새로운 화면에 들어갈 내용이 담긴 가상돔을 생성합니다.
렌더링 이전 화면 구조를 나타내는 Virtual DOM과 업데이트 이후 화면 구조를 나타내는 Virtual DOM를 비교하여 정확히 어떤 부분이 변경되었는지 효율적으로 비교하는 Diffing을 통해 변경된 부분들을 파악하고 Batch Update를 통해 변경된 부분만을 실제 DOM에 한번에 반영합니다.
이런 리액트의 Virtual DOM 조작과정을 Reconciliation(재조정)이라고 합니다.
리액트의 재조정 과정은 BatchUpdate 통해 효율적으로 진행됩니다.
리액트는 Batch Update 통해 변경된 모든 state를 실제 DOM에 한번에 적용시켜주기 때문입니다.
리액트의 Reconciliation(재조정) 과정은 기존 컴포넌트 트리와 변경된 트리를 비교해 최소한의 DOM 업데이트로 렌더링 효율을 극대화하려는 메커니즘입니다. 리액트는 기본적으로 동일한 위치에 같은 컴포넌트 유형이 있을 경우 기존 것을 재사용하려고 합니다. 특히 클래스형 컴포넌트에서는 기존의 컴포넌트 인스턴스를 계속 유지하고, 함수형 컴포넌트는 함수 참조를 통해 동일한 인스턴스를 재사용하는 방식으로 처리됩니다.
리액트는 컴포넌트를 비교할 때 type
필드를 확인해 동일한 컴포넌트인지 판단합니다. 여기서 리액트는 참조 비교(===
)를 사용해 두 요소가 같은지 확인합니다. 예를 들어 <div>
에서 <span>
으로, 또는 <ComponentA>
에서 <ComponentB>
로 변경되면, 리액트는 전체 하위 트리를 새롭게 만들어야 하는 것으로 판단하고 해당 트리를 파괴 후 재생성합니다.
이로 인해 동일한 컴포넌트 참조를 유지하는 것이 중요합니다. 예를 들어, 다음과 같이 ChildComponent
를 ParentComponent
내부에 정의하면 렌더링 시마다 새 참조를 생성하게 되어 리액트가 이를 매번 새로운 컴포넌트로 인식해 트리를 재구성하게 됩니다.
function ParentComponent() {
// 이 코드에서는 매번 새 `ChildComponent`가 생성됩니다.
function ChildComponent() {}
return <ChildComponent />
}
이렇게 컴포넌트 타입을 생성하는 것은 피해야 하며, 대신 컴포넌트를 별도로 정의하여 참조가 고정되도록 해야 합니다.
function ChildComponent() {} // 컴포넌트를 별도로 선언
function ParentComponent() {
return <ChildComponent /> // 동일 참조 유지
}
리액트의 재조정(Reconciliation) 과정에서 key
는 요소를 고유하게 식별하는 데 중요한 역할을 합니다. 재조정은 리액트가 기존 컴포넌트 트리와 새로운 컴포넌트 트리를 비교하고, 가능한 DOM 요소를 재사용하면서 변경 사항만 효율적으로 업데이트하는 과정입니다. 이때 key
는 리액트가 컴포넌트의 고유 인스턴스를 추적하여 어떤 요소가 추가되거나 삭제되었는지, 위치가 변경되었는지를 식별하는 고유 식별자 역할을 합니다.
리액트는 기본적으로 컴포넌트의 type
(유형)과 key
를 사용해 각 컴포넌트를 비교합니다. 따라서 배열 요소에 key
를 제공하면 요소가 이동하거나 삭제될 때 요소 위치를 정확하게 추적할 수 있어 재조정 과정이 최적화됩니다. 하지만 만약 key
없이 배열 인덱스를 사용하는 경우에는 아래와 같은 상황이 발생할 수 있습니다.
// 10개의 <TodoListItem> 컴포넌트를 배열로 렌더링
todos.map((todo, index) => <TodoListItem key={index} {...todo} />);
위의 예에서 key
로 인덱스를 사용할 경우, 배열 요소가 변경되면 리액트는 기존 요소를 업데이트하여 재사용하려 합니다. 예를 들어, 배열 중간에서 두 개의 요소가 삭제되고 세 개의 새 요소가 추가되면, 인덱스 기준의 key
가 기존 요소와 혼동될 수 있습니다. 결과적으로, 예상치 못한 데이터와 컴포넌트 인스턴스가 연결되어 불필요한 DOM 업데이트가 발생할 수 있습니다.
이를 해결하려면 각 요소가 고유한 id
를 갖도록 설정하는 것이 좋습니다.
// 요소의 고유 id를 key로 사용
todos.map(todo => <TodoListItem key={todo.id} {...todo} />);
이 경우 리액트는 요소가 어떤 방식으로 추가되거나 삭제되는지 정확하게 파악할 수 있어 불필요한 재렌더링이 줄어듭니다. 이를 통해 기존 컴포넌트를 유지하면서 DOM 변경 사항만 업데이트하므로 성능이 더욱 최적화됩니다.
key
는 배열뿐 아니라 개별 컴포넌트를 리셋하고 새로 초기화하는 데에도 유용합니다. 예를 들어, 선택된 항목의 세부 정보를 표시하는 DetailForm
컴포넌트가 있을 때, 선택 항목이 바뀌면 새로운 key
가 지정되어 컴포넌트가 다시 초기화됩니다.
// 선택된 항목의 id로 key를 지정하여 컴포넌트를 리셋
<DetailForm key={selectedItem.id} item={selectedItem} />
위와 같이 key가 변경될 때마다 리액트는 해당 컴포넌트를 제거하고 새롭게 렌더링하므로, 오래된 상태가 남아있어 발생할 수 있는 문제를 방지할 수 있습니다.
리액트의 상태 업데이트 메서드인 setState()
는 기본적으로 비동기적으로 처리됩니다. setState()
가 호출되면 리액트는 즉시 상태를 업데이트하는 대신, 여러 상태 업데이트를 모아서 처리하여 렌더링 효율을 높이는 렌더링 배치를 수행합니다. 여러 setState()
호출이 일괄 처리되면서 약간의 지연이 발생할 수 있지만, 이로 인해 렌더링 성능이 크게 최적화됩니다.
리액트는 특히 이벤트 핸들러 내부에서 발생하는 상태 업데이트를 자동으로 일괄 처리합니다. 이벤트 핸들러의 상태 업데이트는 리액트가 unstable_batchedUpdates
라는 내장 함수로 묶어 처리하기 때문에, 단일 이벤트 내에서 여러 개의 상태 업데이트가 한 번의 렌더링 사이클에서 함께 처리됩니다. 이로 인해 이벤트 핸들러 내에서 발생하는 상태 업데이트가 최적화되어 리렌더링이 불필요하게 중복되지 않습니다.
리액트의 상태 업데이트는 비동기 호출과 함께 동작할 때 다소 다른 방식으로 처리됩니다. 예를 들어, 비동기 작업인 await
를 사용하여 이벤트 핸들러 내에서 상태 업데이트가 연속적으로 발생하면, 비동기 호출 이전의 상태 업데이트는 배치 처리되지만, 이후의 상태 업데이트는 개별 렌더링 패스로 처리됩니다. 아래 예시를 통해 이를 더 명확히 이해할 수 있습니다:
const [counter, setCounter] = useState(0);
const onClick = async () => {
setCounter(0); // 첫 번째 상태 업데이트
setCounter(1); // 두 번째 상태 업데이트
const data = await fetchSomeData(); // 비동기 호출
setCounter(2); // 비동기 이후의 상태 업데이트
setCounter(3); // 비동기 이후의 상태 업데이트
};
위 예시에서 setCounter(0)
와 setCounter(1)
은 같은 이벤트 핸들러 스택에 있기 때문에 unstable_batchedUpdates
에 의해 한 번의 렌더링 패스로 처리됩니다. 하지만 await
이후의 setCounter(2)
와 setCounter(3)
은 새로운 비동기 호출 스택에서 실행되므로 각각 별도의 렌더링 패스를 유발하게 됩니다.
리액트는 또한 componentDidMount
, componentDidUpdate
와 같은 커밋 단계의 생명주기 메소드에서 동기적 렌더링을 수행합니다. 이 메소드들은 렌더링 이후 화면에 그리기 전에 DOM 접근이나 추가적인 작업을 수행하는 데 주로 사용됩니다. 커밋 단계에서는 상태 업데이트가 동기적으로 실행되어 불필요한 중간 상태가 화면에 표시되지 않도록 하여 최종 상태만이 사용자의 화면에 표시됩니다. 예를 들어 div.innerHTML
을 여러 번 변경해도 브라우저는 최종 값만 표시하므로 div.innerHTML = "a"; div.innerHTML = "b";
처럼 연달아 실행되더라도 최종 값인 "b"만 화면에 나타납니다.
useEffect
와 useLayoutEffect
훅도 각각의 처리 타이밍에 따라 상태 업데이트를 다르게 처리합니다. useEffect
는 비동기적으로 실행되므로 DOM 업데이트 이후에 상태 업데이트가 처리되지만, useLayoutEffect
는 동기적으로 실행되어 렌더링 직전에 상태가 업데이트됩니다. useEffect
내부의 상태 업데이트는 대기열에 추가되어 렌더링이 끝난 후 별도의 패스로 처리되므로 불필요한 재렌더링이 발생하지 않도록 합니다.
리액트의 unstable_batchedUpdates
는 실험적인 API로, 상태 업데이트의 일괄처리를 수행합니다. 이 API는 react
패키지에는 포함되지 않고 react-dom
과 react-native
에만 포함되어 있으므로 모든 환경에서 사용할 수 있는 것은 아닙니다. 그러나 리액트 팀은 이 API가 안정적이며, React-Redux와 같은 라이브러리에서도 이를 사용해 일괄 처리를 구현하고 있습니다.
리액트의 Concurrent Mode에서는 모든 상태 업데이트가 자동으로 일괄처리됩니다. 이 모드가 활성화되면 리액트는 모든 업데이트를 일괄적으로 관리하여 더욱 부드럽고 최적화된 사용자 경험을 제공합니다.
💡 자동 배치 중단: flushSync
React 18에서는 새로운 ReactDOM.flushSync
API도 도입되었습니다. flushSync
를 사용하면 상태 업데이트를 강제로 배치에서 제외하고 즉시 반영할 수 있습니다. 예를 들어 DOM 조작 후 상태를 즉시 읽어야 할 경우, flushSync
를 통해 상태 업데이트를 즉시 실행하여 예상된 타이밍에 반영할 수 있습니다. 다만, 이 API는 성능에 영향을 줄 수 있어 꼭 필요할 때에만 사용하는 것이 권장됩니다
렌더링의 과정은 개념적으로 크게 2가지 단계로 나뉘게됩니다.
렌더 단계에서는 상태나 props의 변화를 감지하여 가상 DOM 트리에서 변경 사항을 계산하고, 새로운 가상 DOM과 이전 가상 DOM을 비교하여 업데이트가 필요한 부분을 식별합니다. 이 단계는 비동기적으로 진행되어 실제 DOM에 직접 영향을 미치지 않고, 변경 사항을 효율적으로 계산하는 데 집중합니다. 이후 커밋 단계에서는 렌더 단계에서 준비된 변경 사항을 실제 DOM에 적용하고, componentDidMount
및 componentDidUpdate
와 같은 라이프사이클 메서드와 useEffect
와 같은 부수 효과가 실행됩니다. 이 단계는 동기적으로 수행되며, 최종적으로 변경된 내용이 실제 화면에 반영됩니다. 이러한 구조 덕분에 React는 효율적인 UI 업데이트와 최적화된 렌더링을 제공합니다.
React에서는 기본적으로 부모 컴포넌트가 다시 렌더링될 때, 그 안에 포함된 자식 컴포넌트도 재귀적으로 렌더링됩니다. 예를 들어, 컴포넌트 구조가 A > B > C > D 형태로 연결되어 있다고 가정해보겠습니다. 여기서 B 컴포넌트에 있는 버튼을 클릭하면 B 컴포넌트의 상태가 업데이트되면서 React는 A부터 트리의 최상단에서 렌더링을 시작합니다.
React는 트리를 순회하며 A에는 업데이트가 필요하지 않음을 확인하고 그대로 지나칩니다. B는 업데이트가 필요하기 때문에 렌더링이 수행되며, 이후로는 변경이 없어도 B 하위에 포함된 C와 D도 렌더링을 진행합니다. React는 “부모가 렌더링되면 자식도 렌더링” 규칙을 따르며, 이 과정에서 각 컴포넌트가 받는 props가 변하지 않았더라도 렌더링을 수행합니다. 그래서, 만약 최상단의 컴포넌트(App)에서 상태 변경이 발생하면 컴포넌트 트리 전체가 렌더링되는 효과가 나타나게 됩니다.
다만, 모든 컴포넌트가 상태 업데이트 시에 동일한 렌더링 결과를 반환하는 경우가 많기 때문에 실제 DOM 변화가 필요하지 않을 수 있습니다. React는 이를 확인하기 위해 트리 전체를 렌더링하여 그 결과를 비교하는데, 이 작업은 비교적 많은 시간과 자원을 소모합니다.
중요한 점은 렌더링이 항상 나쁜 것은 아니며, React에게는 DOM 변화를 결정하는 필수적인 과정이라는 것입니다. 또한 React에서는 불필요한 렌더링을 줄이기 위해 React.memo
와 shouldComponentUpdate
같은 최적화 기법을 제공하여, 특정 조건에서만 컴포넌트를 재렌더링하도록 설정할 수 있습니다. 이를 통해 큰 컴포넌트 트리에서 성능을 효과적으로 최적화할 수 있습니다.
React의 렌더링에서 가장 중요한 규칙 중 하나는 렌더링 과정은 반드시 "순수"해야 하며, 부작용(side effect)을 유발해서는 안 된다는 점입니다. 하지만 일부 부작용은 화면에 명확한 오류를 만들지 않고 정상 동작할 수도 있습니다. 예를 들어 console.log()
는 기술적으로는 부작용이지만, 화면 결과에는 아무런 영향을 미치지 않습니다. 비슷하게, props를 직접 수정하는 것도 부작용이지만, 실제로 화면이 깨지지 않을 수도 있습니다. 그러나, 렌더링 도중에 AJAX 호출을 실행하는 것은 명백한 부작용으로, 데이터 요청이 앱에 예상치 못한 영향을 줄 가능성이 있습니다.
React에서 "순수한" 렌더링이란, 상태를 변경하거나 예측 불가능한 요소를 추가하지 않고, 부작용을 배제한 채 정해진 결과만을 반환하는 것을 의미합니다.
렌더링 로직에서 피해야 할 작업
Math.random()
나 Date.now()
): 렌더링은 항상 동일한 결과를 반환해야 하므로, 랜덤 값 생성은 부적절합니다.렌더링 중에서 허용될 수 있는 작업
이러한 규칙들은 React의 예측 가능성을 높이고, 성능과 유지보수성을 확보하기 위해 마련되었습니다. 렌더링은 항상 같은 입력이 주어지면 같은 결과를 반환하도록 보장되어야 하며, 부작용 없이 순수하게 유지되어야 합니다. React는 부작용이 필요한 작업을 useEffect
등으로 분리하여 관리할 수 있는 기능을 제공하며, 이를 통해 안전하게 부작용을 다룰 수 있습니다.
React는 애플리케이션의 모든 컴포넌트 인스턴스를 추적하기 위해 Fiber라는 자료 구조를 사용합니다. Fiber는 컴포넌트 메타데이터와 관련된 핵심 정보를 담고 있는 객체로, React가 렌더링과 업데이트 과정을 최적화하는 데 중요한 역할을 합니다.
각 Fiber 객체는 현재 렌더링 중인 컴포넌트와 관련된 여러 정보를 포함하고 있습니다. 주요 메타데이터는 다음과 같습니다:
Fiber는 기본적으로 React의 비동기 렌더링을 지원하며, 렌더링 성능을 개선하기 위해 도입된 자료 구조입니다. 전통적인 React 렌더링에서는 업데이트가 발생할 때 전체 컴포넌트를 한 번에 렌더링했습니다. 그러나 Fiber를 사용하면서부터는 React가 작업을 작은 단위로 나누어, 프레임 단위로 작업을 중단하고 다시 시작할 수 있게 되었습니다. 이를 통해 브라우저의 사용자 인터페이스가 끊김 없이 반응할 수 있게 되며, 작업이 중간에 종료되거나 긴급 업데이트가 발생할 경우 우선순위에 따라 작업을 재조정할 수 있습니다.
React는 부모 컴포넌트가 자식 컴포넌트를 렌더링할 때, 해당 컴포넌트를 추적하기 위한 Fiber 객체를 생성합니다. 클래스형 컴포넌트의 경우, React는 new YourComponentType(props)
를 호출하여 컴포넌트 인스턴스를 생성하고 이를 Fiber에 저장합니다. 여기서 this.props
는 사실 Fiber에 저장된 props의 참조를 복사한 것입니다. 반면, 함수형 컴포넌트와 Hooks의 경우에는 컴포넌트 인스턴스 대신 YourComponentType(props)
를 호출하여 렌더링합니다. 함수형 컴포넌트에서 사용하는 모든 훅은 Fiber 객체의 연결 리스트 형태로 저장됩니다. React가 함수형 컴포넌트를 렌더링할 때마다 해당 Fiber에서 훅 연결 리스트를 참조하며, 호출된 순서에 따라 저장된 상태 값이나 useReducer
의 dispatch 함수를 반환합니다.
위에서 설명한 것 처럼 렌더링은 렌더와 커밋 2가지 단계로 나누어집니다. Fiber를 통해 자세히 설명하자면, 렌더 단계에서는 Fiber를 순회하며 가상 DOM의 변경 사항을 계산합니다. 이 단계에서는 실제 DOM을 변경하지 않으며, 필요에 따라 작업을 중단하거나 이어서 수행할 수 있습니다. 이후 커밋 단계에서는 Fiber에서 준비된 변경 사항을 바탕으로 실제 DOM을 업데이트합니다. 이 단계는 동기적으로 수행되어 최종 변경 사항이 화면에 반영됩니다. Fiber 시스템 덕분에 React는 복잡한 UI를 효율적으로 렌더링하고, 동적이고 상호작용이 많은 애플리케이션에서도 부드러운 사용자 경험을 제공할 수 있습니다.
리액트 내부 코드의 파이버 객체 예시
function FiberNode(tag, pendingProps, key, mode) {
// 기본 속성 초기화
this.tag = tag; // 노드의 유형을 나타내는 태그
this.key = key; // 리액트의 고유 키
this.elementType = null; // JSX 요소 타입 (예: <div>면 "div")
this.type = null; // 컴포넌트 함수 또는 클래스 인스턴스
this.stateNode = null; // DOM 노드 또는 컴포넌트 인스턴스
this.mode = mode; // 리액트 모드 (동기/비동기 여부)
// Props & State
this.pendingProps = pendingProps; // 새로 전달된 props
this.memoizedProps = null; // 마지막 렌더링에 사용된 props
this.memoizedState = null; // 마지막 렌더링에 사용된 상태
this.updateQueue = null; // 상태 업데이트를 관리하는 큐
// 트리 구조
this.return = null; // 부모 파이버 노드
this.child = null; // 첫 번째 자식 파이버 노드
this.sibling = null; // 다음 형제 파이버 노드
this.index = 0; // 자식 노드에서의 인덱스
// 효과 및 우선순위 관리
this.ref = null; // ref 속성 참조
this.effectTag = 0; // 이 노드에서 필요한 작업(플래그)
this.nextEffect = null; // 다음으로 처리할 효과 노드
this.firstEffect = null; // 첫 번째 효과 노드
this.lastEffect = null; // 마지막 효과 노드
// 상위 컨텍스트
this.alternate = null; // 현재 파이버의 대체 파이버(더블 버퍼링)
this.flags = 0; // 여러 상태를 나타내는 플래그
}
파이버는 리액트 요소와 유사하다고 느낄 수 있지만 중요한 차이점은 리액트 요소는 렌더링이 발생할 때마다 새롭게 생성되는 반면 파이버는 가급적 재사용됩니다. 파이버는 컴포넌트가 최초 마운트되는 시점에 생성되어 이후에는 가급적이면 재사용됩니다.
파이버 트리는 React에서 2가지가 존재합니다. 하나는 현재 모습을 담은 파이버 트리, 다른 하나는 작업 중인 상태를 나타내는 workInProgress 트리입니다. 리액트 파이버의 작업이 끝나면 리액트는 단순히 포인터만 변경하여 workInprogress 트리를 현재 트리로 변경합니다. 이러한 기술을 더블 버퍼링이라고 합니다.
리액트에서 더블 버퍼링은 커밋 단계에서 수행되게됩니다. 먼저 현재 UI 렌더링을 위해 존재하는 트리인 current기준으로 모든 작업이 시작됩니다. 여기에서 업데이트가 발생하면 파이버는 리액트에서 새로 받은 데이터로 새로운 workInProgress 트리를 빌드하기 시작합니다. workInprogress 트리를 빌드하는 작업이 끝나면 다음 렌더링에 이 트리를 사용하게됩니다. 이 workInprogress 트리가 UI에 최종적으로 렌더링되어 반영이 완료되면 current가 이 workInprogress로 변경됩니다.
💡 더블 버퍼링
더블 버퍼링은 리액트에서 새롭게 나온 개념이 아니며, 컴퓨터 그래픽 분야에서 사용하는 용어입니다. 그래픽을 통해 화면을 표시되는 것을 그리기 위해서는 내부적으로 처리를 거쳐야 하는데, 이러한 처리를 거치게 되면 사용자에게 미처 다 그리지 못한 모습을 보이는 경우가 발생하게됩니다. 그래서 이러한 상황을 방지하기 위해 보이지 않는 곳에서 그다음으로 그려야 할그림을 미리 그린 다음, 이것이 완성되면 현재 상태를 새로운그림으로 바꾸는 기법을 의미합니다.
// JSX
<A1>
<B1>안녕하세요</B1>
<B2>
<C1>
<D1 />
<D2 />
</C1>
</B2>
<B3 />
</A1>
위 파이버의 작업은 JSX코드에서 아래와 같이 수행됩니다:
1. A1의 beginWork()가 수행됩니다.
2. A1은 자식이 있으므로 B1로 이동해 beginWork()를 수행합니다.
3. B1은 자식이 없으므로 completeWork()가 수행됐다. 자식은 없으므로 형제(sibling)인 B2로 넘어갑니다.
4. B2의 beginWork()가 수행된다. 자식이 있으므로 C1로 이동합니다.
5. C1의 beginWork()가 수행된다. 자식이 있으므로 D1로 이동합니다.
6. D1의 beginWork()가 수행되고, 자식이 없으므로 형제(sibling)인 D2로 넘어갑니다.
7. D2의 beginWork()가 수행되고, 자식이 없으므로 completeWork()가 수행됩니다.
8. D2는 자식도 형제도 없으므로, 위로 이동해 C1, B2 순으로 completeWork()를 호출합니다.
9. B2는 형제(sibling)인 B3으로 이동해 beginWork()를 수행합니다.
10. B3의 completeWork()가 수행되면 반환해 상위로 타고 올라갑니다.
11. A1의 completeWork()가 수행됩니다.
12. 루트 노드가 완성되면 commitWork()가 수행되고 이 중 변경 사항을 비교해 업데이트가 필요한 변경사항이 DOM에 반영됩니다.
위 파이버 트리를 도식화 하면 아래와 같습니다.
여기서 만약 setState로 인한 업데이트가 발생하면 어떻게 될까요? 이미 리액트에는 앞서 만든 current 트리가 존재하고 setState로 인한 업데이트 요청을 받아 workInProgress 트리를 다시 빌드하기 시작합니다 최초 렌더링 시에는 모든 파이버를 새롭게 만들어야 했지만, 이미 파이버가 존재하므로 새로 생성하지 않고 기존의 파이버에서 업데이트된 props를 받아 파이버 내부에서 처리합니다. 새로운 파이버를 만드는 것은 리소스 낭비라고 볼 수 있습니다. 따라서 기존의 파이버 객체를 재활용하여 내부 속성값만 초기화하거나 바꾸는 형태로 트리를 업데이트합니다. 이것이 앞에서 설명한 “가급적이면 새로운 파이버를 생성하지 않는다.”가 바로 이것입니다.
리액트는 이러한 작업을 파이버 단위로 나누어서 수행하며, 애니메이션이나 사용자가 입력하는 작업은 우선순위가 높은 작업으로 분리하거나 목록을 렌더링하는 등의 작업의 우선순위가 낮은 작업으로 분리해 최적의 순위로 작업을 완료하도록합니다.