최근 교내 같은 단과대 학생들을 대상으로 React로 입문하는 프론트엔드라는 강의형 스터디를 진행하고 있는데, react가 기존 javascript와 html, css를 사용한 방식과 가지는 차이를 설명하기 위해 virtual dom과 reconciliation 을 가볍게 다룬 적이 있다.

강의가 끝나고 녹화된 영상을 인코딩하며 영상 검수를 진행하다가, 문득 이전에 스터디에서 다른 멤버가 했던 말이 떠올랐다.

<aside> 💡 최근에는 react가 reconciliation이 아니라 다른 방식을 사용한다던데?

</aside>

어쩌면 지금 내가 컴포넌트로 치면 클래스형 컴포넌트를 가르치는 것처럼 구 버전의 react를 설명하고 있을 수도 있다는 생각에, 다시 한 번 react의 reconciliation을 살펴보기로 했다.

(구) reconciliation 의 한계

이전에 사용되었던 reconciliation 과정을 살펴보면, native DOM tree를 복사한 virtual DOM tree가 메모리에 저장되고, 변경 사항이 생기면 새로운 virtual DOM tree가 만들어져 앞서 생성한 virtual DOM tree끼리 차이를 비교한다.

이때 virtual DOM tree는 객체이기에, diff 알고리즘을 통해 비교하며, 그 방식은 재귀 알고리즘을 채용했다.

스크린샷 2024-08-14 오후 5.28.49.png

재귀 알고리즘은 함수 호출마다 call stack에 쌓이며, 함수가 반환되면 call stack에서 pop 되는 구조를 가지고 있다.

image.png

이때 비동기 작업의 경우 event loop가 call stack이 비어있는 여부를 확인한 후에야 콜백함수를을 call stack에 올려놓고 실행하기에, call back queue에 대기중인 이벤트(click, animation 등)들은 실행되지 못한 채 대기한다.

따라서 React로 제작한 레포가 방대해질 경우, 재귀 방식으로 tree를 순회하는 시간이 길어져 비교가 끝날 때까지 다음 이벤트를 처리하지 못할 수 있다.

React의 디자인 원칙

현재 구현에서 React는 트리를 재귀적으로 순회하며 단일 틱(tick) 동안 업데이트된 전체 트리의 렌더 함수를 호출합니다. 그러나 미래에는 프레임 드롭을 방지하기 위해 일부 업데이트를 지연시키기 시작할 수 있습니다. 이는 React 설계의 공통적인 주제입니다. 일부 인기 있는 라이브러리들은 새로운 데이터가 사용 가능해지면 계산을 수행하는 "push" 접근 방식을 구현합니다. 그러나 React는 필요할 때까지 계산을 지연시킬 수 있는 "pull" 접근 방식을 고수합니다. React는 범용 데이터 처리 라이브러리가 아닙니다. 사용자 인터페이스를 구축하기 위한 라이브러리입니다. 우리는 React가 앱 내에서 어떤 계산이 지금 관련 있고 어떤 것이 그렇지 않은지 알 수 있는 고유한 위치에 있다고 생각합니다. 만약 어떤 것이 화면 밖에 있다면, 우리는 그것과 관련된 모든 로직을 지연시킬 수 있습니다. 데이터가 프레임 속도보다 빠르게 도착한다면, 우리는 업데이트를 병합하고 일괄 처리할 수 있습니다. 우리는 프레임 드롭을 방지하기 위해 사용자 상호작용에서 오는 작업(예: 버튼 클릭으로 인한 애니메이션)에 우선순위를 둘 수 있으며, 덜 중요한 백그라운드 작업(예: 네트워크에서 방금 로드된 새 콘텐츠 렌더링)보다 우선시할 수 있습니다.

이전에는 react가 스케줄링에 대해 크게 활용하고 있지 않았고, 변경사항을 업데이트할 시 전체 서브트리를 즉시 다시 렌더링했다.(re-render가 trigger 된 경우), 따라서 스케줄링을 활용하기 위해 React의 핵심 알고리즘 중 하나였던 Reconciliation 알고리즘을 Fiber 기반으로 개편하게 되었다.