리액트 Reconciliation 알아보기

Yoomin Kang·2025년 2월 1일
post-thumbnail

이 글은 “전문가를 위한 리액트” 서적을 (많이) 참고하여 작성되었습니다.

React는 모던 프론트엔드 개발에서 빼놓을 수 없는 강력한 라이브러리다.

우리는 대부분 React를 가져다 사용하는 것에 그치고, 내부의 원리에 대해서는 관심을 가지지 않는 경우가 많다.

그래서 준비했다. 리액트에서 Reconciliation은 어떻게 작동하는걸까?

Reconciliation이란?

React가 Virtual DOM을 사용하고 diffing 알고리즘을 통해 달라진 부분만 다시 그린다는 사실은 널리 알려져 있다. state나 props가 바뀌어서 새로운 Virtual DOM이 생성되었을 때 기존 Virtual DOM과의 차이를 비교하여 변경된 부분을 찾고, 이를 바탕으로 UI 업데이트 전략을 결정하는 과정을 Reconciliation(재조정)이라고 한다.

React 16 미만 버전의 한계

React 16 미만 버전에서는 Stack reconciler(스택 재조정자)를 이용했다.

맞다, 모두들 아는 그 LIFO 자료구조 스택이다.

Stack reconciler는 애플리케이션의 복잡성이 증가하며 여러 문제를 보였다.

Stack reconciler는 작업을 일시 중지 또는 연기하지 않고 순서대로 변경된 부분을 렌더링한다. 이는 재귀적으로 동작하기 때문에, 만약 어떤 노드의 업데이트가 필요 없더라도 처리하게 되어 있다.

이 때문에 비싼 계산 비용의 컴포넌트가 렌더링을 막아버리게 되고, 사용자 경험이 크게 저하되는 참사가 발생한다.

다시 말해, Stack reconciler에서는 추가된 순서대로 업데이트가 실행되기 때문에 상대적으로 덜 중요한 업데이트가 더 중요한 업데이트를 차단할 수 있다.

또한, Stack reconciler는 업데이트를 중단 또는 취소할 수 없다는 문제점도 지니고 있다.

이러한 스택 재조정자의 문제로 인해, 새로운 방식의 Reconciliation이 필요해졌고, React 16에서 새로운 방식인 Fiber reconciler(파이버 재조정자)가 등장했다.

React 16 이후의 Reconciliation

React 16 이후부터는 Reconciliation에 Fiber 트리를 사용하기 시작했다.

Fiber reconciler는 업데이트에 우선순위를 정하고, 이에 따라 동시 실행을 가능하게 하여 Stack reconciler의 문제점을 해결했다.

Fiber 트리는 Fiber 노드들로 구성되며, Fiber 노드의 예시는 다음과 같다.

{
	tag: 3,
	type: App,
	key: null,
	ref: null,
	props: {
		name: "Yoomin Kang",
		age: 21
	},
	stateNode: AppInstance,
	return: FiberParent,
	child: FiberChild,
	sibling: FiberSibling,
	index: 0,
	// ...
}

Reconciliation은 크게 두 단계로 나누어진다. Render phase(렌더링 단계)와 Commit phase(커밋 단계)이다. 이렇게 두 단계로 나눠지는 덕분에 렌더링 중단이 가능해졌다.

Render Phase

Render Phase

Render phase는 현재 트리에서 상태 변경이 발생하면 시작되며, beginWork와 completeWork의 두 단계로 나뉜다.

여기서부터 “현재 트리”와 “작업용 트리”라는 말이 등장할 것이다. 둘을 헷갈리지 않도록 유의하자.

beginWork는 작업용 트리에 있는 Fiber node의 업데이트 필요 여부를 나타내는 플래그를 설정하고, 다음 Fiber node로 이동하여 트리의 맨아래에 도달할 때까지 같은 과정을 반복하는(아래 방향으로 순회하는) 단계이다.

beginWork가 완료되면 Fiber node에서 completeWork를 호출하고, 다시 거슬러 올라가며 순회한다.

completeWork는 작업용 Fiber node에 대해 Effect list를 설정해 Commit phase에서 실행될 작업을 준비한다.

여기서 중요한 점은, 이 화면은 중단될 수 있다는 점이다. 우선순위가 더 높은 업데이트가 예약되면 이 UI는 버려질 수 있다. 이것이 Fiber reconciler의 핵심이다.

beginWork와 completeWork의 시그니처는 다음과 같다.

function beginWork(  // 또는 completeWork
	current: Fiber | null,
	workInProgress: Fiber,
	renderLanes: Lanes
): Fiber | null
  • current는 업데이트 중인 작업용 노드에 해당하는 현재 트리의 Fiber node에 대한 참조이다.
  • workInProgress는 작업용 트리에서 업데이트 중인 Fiber node이다.
  • renderLanes는 업데이트가 처리되는 레인을 표현하는 비트마스크다. 변경의 우선순위가 높을수록 더 높은 레인이 할당된다.

Commit Phase

Commit phase는 Render phase에서 Virtual DOM에 적용된 변경사항을 실제 DOM에 반영하는 단계이며, Mutation phase와 Layout phase 두 단계로 나뉜다.

Commit Phase에서는 새 Virtual DOM tree가 호스트 환경에 Commit되고 작업용 트리가 현재 트리로 바뀐다.

Mutation phase에서는 Virtual DOM의 변경 사항을 실제 DOM에 반영한다.

Layout phase에서는 DOM에서 업데이트된 노드의 새 레이아웃을 계산한다.

Commit Phase에서는 Effect(효과)가 실행되는데, Commit Phase에 발생하는 Effect들은 다음과 같다.

  • Placement effect - 새 컴포넌트가 DOM에 추가될 때
  • Update effect - 컴포넌트가 새 state나 props로 업데이트될 때
  • Deletion effect - 컴포넌트가 DOM에서 제거될 때
  • Layout effect - 브라우저의 페인트 가능 시점 전에 발생하며 페이지 레이아웃을 업데이트할 때 사용 (useLayoutEffect로 관리)

Commit Phase에서의 Effect와 달리 Passive effect는 브라우저의 페인트 가능 시점 이후에 발생하며 useEffect로 관리한다. (Passive Phase에서 실행됨)

FiberRootNode

React는 현재 트리나 작업용 트리 중 하나 위에 FiberRootNode를 둔다.

Virtual DOM이 업데이트되면 현재 트리를 변경하지 않은 채 작업용 트리가 업데이트된다.

이렇게 애플리케이션의 현재 상태를 유지하면서 Virtual DOM을 계속 렌더링하고 업데이트할 수 있다.

비유하자면 마치 Double buffering과 같다. (Double buffering은 다음 화면을 화면 밖에서 준비한 후 현재 화면으로 내보내는 게임 개발 기법이다.)

FiberRootNode 관점에서 Reconciliation을 다시 정리해보면, 렌더링 프로세스가 완료되고 commitRoot 함수를 통해 작업용 트리에 적용된 변경 사항이 실제 DOM에 Commit 된다. 이 때, commitRoot는 FiberRootNode의 포인터를 현재 트리에서 작업용 트리로 전환하고 작업용 트리를 새로운 현태 트리로 만드는 것이다.

🎄트리의 흐름 요약

  1. 현재 화면을 나타내는 Fiber tree가 있음 (현재 트리)
  2. 업데이트가 발생하면 새로운 Fiber tree를 만듦 (작업용 트리)
  3. Render Phase에서 작업용 트리를 업데이트하면서 변화가 있는 부분을 찾음
  4. Commit Phase에서 작업용 트리를 현재 트리로 전환하고 실제 DOM을 업데이트
  5. 이전의 현재 트리는 더 이상 사용되지 않음
  6. 작업용 트리가 새로운 현재 트리가 됨
  7. 다음 업데이트가 발생하면 다시 새로운 작업용 트리를 만들고 반복

정리

지금까지 React에서 Reconciliation의 원리에 대해 살펴보았다.

요약하면 다음과 같다.

리액트 16 이후로는 Fiber reconciler를 사용하고, Render phase와 Commit phase로 나뉜다. Render phase에서는 어떤 변경이 필요할지 준비하고, Commit phase에서는 실제 DOM을 변경한다. Stack reconciler가 아닌 Fiber reconciler을 사용하는 이유는 비싼 작업이 실행되는 동안 UX가 저하되는 것을 막을 수 있기 때문이다.

많은 도움 되었길 바란다.

profile
FE Developer @Toss | GSHS 36 | Korea Univ 21

0개의 댓글