React의 동작과 Fiber에 대하여 간략하게 알아봅니다.

Explanation

오늘은 React가 어떻게 동작하는지, 그리고 Fiber에 대해서 알아보려 합니다.
(어쩌다보니 최근 포스팅이 React 3종 세트가 되었네요.)

[참고]
https://www.youtube.com/watch?v=7YhdqIR2Yzo
https://www.youtube.com/watch?v=0ympFIwQFJw

위 영상이 너무 잘 설명해주셔서 한번 보시면 좋을 것 같아요. 강추!!

이 글은 위 참고 영상을 기반으로 작성하였고, 위 영상은 3년 영상이라 현 시점과 차이가 있을 수 있습니다.

우선, React의 동작에 대해서 알아볼 건데요. React의 Reconciliation, Virtual DOM, Rendering, Diffing Algorithm을 중심으로 알아봅니다.

1. React Component

글을 시작하기 앞서, React의 컴포넌트는 함수뿐 아니라 클래스로 사용할 수도 있지만, 요즘은 일반적으로 함수형 컴포넌트를 많이 사용하니까 이 글에서는 함수형만 다루며, React는 브라우저 뿐 아니라 React Native에서도 사용할 수 있지만 이 글에서는 웹 브라우저에서의 사용을 기준으로 작성하였습니다.

아래와 같은 컴포넌트가 있을때,

위 컴포넌트는 React에 의해 대략, 아래와 같이 Virtual DOM이 만들어집니다.

조금 더 자세히 이야기하면, 컴포넌트 함수가 응답하는 JSX 값을 React Core의 React.createElement 메서드를 통해서 Virtual DOM으로 변환합니다. 그리고 이 Virtual DOM은 React-DOM을 통해서 실제 브라우저에 사용되는 DOM으로 변환하고 브라우저는 렌더링 엔진을 통해서 해당 DOM을 해석해서 화면에 그리게 됩니다.

여기서, React의 React Core는 React 컴포넌트를 정의하고 컴포넌트가 응답하는 JSX를 Virtual DOM로 변환하는, Virtual DOM 트리의 요소를 만드는 것입니다. 그리고 나머지의 기능들은 React의 다른 요소들이 담당한답니다.

React는 여러 핵심 모듈들로 구성이 되어 있는데, “React Core(코어)”와 “Reconciler(조정자)”, Scheduler(스케줄러), Renderer 등이 있습니다. 앞서 이야기나온 “React-DOM”은 Renderer 모듈에 해당합니다.

2. Reconciliation

처음 만들어진 Virtual DOM 트리를 가지고 DOM을 생성하고 브라우저에 의해 출력이 되는데, 이후 Virtual DOM에 변화를 초래하는 상태 변경이 있다면, React는 기존의 Virtual DOM 트리에서 변경된 값이 적용된 새로운 트리를 생성하고 이 트리를 가지고 실제 DOM과 동기화 합니다.

하지만 실제 애플리케이션에서는 아주 복잡하고 깊은 Virtual DOM 트리가 만들어지게 되는데요. 이때 전체 트리를 다시 렌더링하는 것은 매우 비효율적인 일입니다. 그래서 React 에서는 Diffing 알고리즘이라는 알고리즘을 사용하여 최소한의 작업으로 새 트리를 만듭니다. 그리고 여기에 핵심은 두 트리의 비교입니다.

우선 첫번째 비교는 둘의 타입이 다르다면 새로운 트리를 생성합니다. 두번째는 변경하는 하위 요소 목록이 있을 때, 목록의 요소엔 고유한 key를 제공하고 key가 변경되었을 때 새로운 트리를 생성합니다.

위와 같이 컴포넌트로 변경된다면, 첫번째 가정으로 새로운 트리를 구성합니다.

위와 같이 동일한 유형에 속성만 변경된 경우 React는 단순히 해당 속성만 업데이트 합니다.

그리고 목록의 경우,

아주 단순하게만 봤을 때, 위와 같은 상황에서 items의 마지막 위치에 요소가 추가되었을 때에는 기존의 요소는 동일하기 때문에 재사용하며 마지막에 새로 추가된 요소만 새 항목임을 확인하고 추가되지만, 만약 첫번째 위치에 요소가 추가된다면 기존과 변경된 목록의 위치의 모든 요소가 달라졌기 때문에 모든 요소를 새로운 트리로 생성합니다.

그렇다면 목록에 요소가 굉장히 많다면, 아주 비효율적으로 DOM을 갱신하게 될 것 입니다.

그렇기 때문에 위와 같이 해당 데이터를 나타내는 key 값을 사용하면 React는 어떤 요소가 여전히 사용되고 어떤 요소가 새롭게 생긴 요소인지 구분하여 사용합니다.

3. Rendering

렌더링이라는 단어는 사실 조금 포괄적인 단어라서 다양한 곳에서 많이 사용되는데요, React에서 렌더링은 크게 두가지 단계로 나뉩니다. 첫 번째가 렌더 단계이고 두 번째가 커밋 단계입니다.

우선 첫 번째 단계인 렌더 단계는 React Core가 전체 트리 구조를 만들고 업데이트 사항을 파악하는 단계로, 컴포넌트가 렌더링 로직을 실행하여 새로운 Virtual DOM을 생성하고 이와 함께 Fiber 트리를 구성합니다.

드디어 오늘의 두번째 주인공 Fiber 등장!

각 컴포넌트는 자신만의 Fiber 노드를 가지고 이 노드에는 해당 컴포넌트에 대한 정보와 상태가 저장됩니다. 예를 들어 컴포넌트의 useState, useRef 그리고 기타 훅(useEffect, useReducer, 등)의 값을 추적하고 이 값들을 Fiber 노드에 저장합니다.

Fiber 노드에는 type, stateNode, memoizedState 그리고 child, sibling, return과 같은 주요 정보를 포함하고 있습니다.

– type : 헤당 노드가 어떤 컴포넌트를 나타내는지에 대한 정보
– stateNode : 실제 컴포넌트 인스턴스 또는 DOM 노드와의 연결
– memoizedState : Hooks의 상태(useState), 레퍼런스 값(useRef) 등이 저장되는 부분
– child, sibling, return : 트리 구조를 이루기 위한 포인터들

위와 같은 컴포넌트가 있을 때,

위와 같은 단일 연결 리스트 형태의 트리를 구성합니다.

Fiber에 대해서는 조금 이따 다시 알아볼 것이니, 다음으로,

다음 두 번째 커밋 단계는 React-DOM과 같은 렌더러가 담당하며, 앞선 렌더 단계에서 만들어진 변경된 Fiber 트리를 실제 DOM에 반영하여 화면에 업데이트를 수행하는 단계입니다.

4. Fiber

이제 두번째 주제인 Fiber에 대해서 알아봅니다!

Fiber는 앞서 잠깐 나온 렌더 단계에서의 Virtual DOM을 생성하면서 함께 만드는 Fiber 노드 트리의 그 노드를 이야기하며, 이 Fiber 노드는 몇가지 속성을 가지는 단순한 자바스크립트 객체입니다.

그리고 그 이름을 따서 v16 이후의 React는 Fiber Reconciler를 기본 Reconciler(조정자)로 사용합니다.

Fiber의 특징으로
– 작업을 여러 단위로 나누고 작업의 우선순위를 지정할 수 있습니다.
– 작업을 일시 중지하고 나중에 다시 돌아올 수 있습니다.
– 이전에 완료된 작업을 재사용하거나 필요하지 않은 경우 중단할 수 있습니다.
– 이전의 React 조정자와 달리 “비동기식”입니다.

React는 이전까지는 Stack 조정자를 사용하였으며, Stack은 “동기식”이며 이름처럼 Stack과 유사하게 동작합니다. 항목을 추가하거나 제거할 수 있지만, Stack이 빌 때까지 작동해야 했고 중단할 수 없습니다.

이 부분의 차이를 React v18에 추가된 useTransition의 예시를 보면 알 수 있는데요.

https://react.dev/reference/react/useTransition#examples
위 React 공식 문서 페이지의 예제에서 UI의 동작 차이를 확인할 수 있습니다.

조금 더 Fiber에 대해서 알아보면,
Fiber 노드는 트리를 구성하는 노드이면서 하나의 작업(Work) 단위를 나타냅니다.

React는 작업(Work) 단위를 처리하고 “완료된 작업”이라는 답을 얻으면 이 작업을 커밋하여 DOM에 적용합니다. 이 부분이 앞서 이야기한 렌더 단계와 커밋 단계의 이야기입니다.

Fiber에서 이야기하는 Work는 업데이트의 단위 작업으로 이는 상태(state)가 변경, 라이프 사이클의 함수 호출, DOM 업데이트 등 React 트리에 변경이 발생했을 때 수행해야 하는 작업을 이야기합니다.

여기서 DOM이 업데이트되었을 때는, 커밋 단계에서 DOM에 실제로 변경을 적용하고 이를 화면에 반영하는 작업도 하나의 Work로 관리됩니다.

여기에서 중요한 부분은 React가 Fiber를 처리할 때마다 작업을 직접 처리하거나 미래에 처리하도록 예약할 수 있다는 것입니다.

5. Fiber Tree

Fiber 트리는 렌더 단계에서 현재 화면에 있는, React가 가리키고 있는 current 트리를 기반으로 새로운 work-in-progress 트리를 만들어 변경 사항이 발생한 부분을 업데이트합니다. 그리고 Fiber는 우선순위가 높은 작업을 먼저 처리하고 비동기적으로 작업을 나누어 처리하며 UI가 막히지 않고 업데이트할 수 있도록 합니다.

Fiber 트리의 업데이트는 앞서 이야기했던 Diffing 알고리즘을 사용하여 기존 current 트리를 기반으로 새로운 work-in-progress 트리를 생성하고 변경 사항만 추적하여 효율적으로 업데이트합니다.

그 후 작업이 완료되면 커밋 단계로 넘어가 동기적으로 실제 DOM에 변경을 반영하고, 필요한 라이프사이클 메서드 및 부수효과를 실행합니다. 커밋 단계가 완료되면 현재의 work-in-progress 트리가 새로운 current 트리가 되며, 다음 업데이트 시에는 이 current 트리를 기반으로 새로운 work-in-progress 트리가 생성됩니다.

그리고 이때 current 트리의 Fiber 노드와 대응되는 work-in-progress 트리의 Fiber 노드는 “alternate”로 연결됩니다. 이 alternate는 React가 효율적으로 변경사항을 관리하고 작업을 처리할 수 있도록 도와줍니다.

간단하게 이야기하면, Diffing 알고리즘를 사용해서 current 트리의 값과 work-in-progress 트리의 값을 비교할 때 React는 alternate를 통해 변경 사항만 파악하고 업데이트 할 수 있으며, 한 번 생성된 work-in-progress 트리가 업데이트가 완료되면 current 트리가 되고, 그다음 업데이트 시에는 다시 current 트리가 work-in-progress 트리가 됩니다. 이 과정에서 두 트리는 alternate를 통해 서로 연결되면서 번갈아 사용됩니다.

6. Effect List

렌더링 될 때 React는 Fiber 트리에서 어떤 작업이 필요할 지 분석하는데, 예를 들면 컴포넌트의 라이프사이클 메서드, React Hooks의 부수 효과(useEffet, useLayoutEffet)를 “Effect List”라는 연결리스트 형태로 저장합니다.

간단하게 예를 들면,
컴포넌트 A가 업데이트 되어 A의 내용이 바뀌고 DOM에 새로운 내용이 추가되고 useFffect도 실행되야 한다고 가정하고 컴포넌트 B도 업데이트 되어 DOM 속성이 변경되어야 한다고 가정 했을 때,

React는 이 정보를 Fiber 트리에 기록하고 A와 B의 부수효과를 Effect List에 추가합니다.

렌더 단계가 끝나면 커밋 단계에서 Effect List를 순회하며 부수효과를 실행하는데, 먼저 A의 변경사항을 DOM에 적용하고 useEffect를 생한한 후 그다음 B의 변경사항을 DOM에 적용합니다.

요약하면, Effect List는 부수효과를 하나의 연결 리스트로 모아서 커밋 단계에서 한번에 순차적으로 처리하는 역할을 합니다.

7. 마무리

React Fiber는 v16에 정식 도입되었고 이때 함께 추가된 Error boundaries와 Suspense, 그리고 v18에서 추가된 useTransition, useDeferredValue와 같은 훅은 React Fiber를 활용한 예시입니다!