[React] Rendering은 어떻게 동작할까? (2/3) - Render 단계
https://codesmoothie.tistory.com/30
[React] Render의 진짜 의미와 동작 원리
https://codesmoothie.tistory.com/29 [React] Component가 Instance, Element로 변화는 과정 Component 리엑트에서 컴포넌트(Component)는 UI 블럭(조각)을 기술(설명)하는 곳이다. 컴포넌트는 정규 자바스크립트 함수로 이
codesmoothie.tistory.com
지난 포스팅 '[React] Render의 진짜 의미와 동작 원리'에서 연결되는 글입니다.
State의 메카니즘(Mechanics)에 대한 잘못된 상식
React에서 랜더링이 어떻게 동작하는지 알기 전에 먼저 State의 메카니즘에 대해서 알아보자. 일반적으로 화면(View)에서 이벤트가 발생하면 State 업데이트가 일어나고 Re-Render가 발생한다고 알고 있다. 하지만, 이는 기술적으로 바라본다면 올바르지 않다. 리엑트에서 랜더링이란 화면(View) 또는 DOM을 업데이트하는 동작이 아니기 때문이다.
또한, 일반적으로 Re-Render가 발생하면 React는 오래된 Component View 버리고(discards) 새로운 컴포넌트로 변경한다고 알고 있다. 이 또한 기술적으로 정확한 것은 아니다. DOM은 전체 컴포넌트 인스턴스를 위해 실질적으로 업데이트 되지 않기 때문이다. 실제로 React Render가 어떻게 일어나는지 기술적으로 알아보자
Render 단계
Render의 시작 단계에서 React는 컴포넌트 트리 전체를 통과해 Re-Render를 발생시키는 모든 Component 인스턴스를 확인하고 실제로 Render를 한다. 즉, 코드에 작성한 곳에 상응하는 컴포넌트의 함수를 호출하는 것이다. 이는 React elements의 업데이트를 생성하고 Virtual DOM을 구성하게 한다. 그렇다면 Virtual DOM이란 무엇일까?
Virtual(가상) DOM
처음 Render가 발생할 때, React는 전체 Component Tree를 받고(Take) 이를 하나의 큰 React Element Tree로 만든다. 이 React Element Tree를 Virtual DOM이라고 부른다. Virtual DOM이란 Component Tree안에 모든 인스턴스로부터 만들어진 React Elements의 Tree이다. Virtual DOM은 자바스크립트 객체로 반복적인 작업이 많더라도 상대적으로 가볍고(Cheap) 빠르게 multiple tree를 구성한다. 많은 사람들이 React를 설명할 때 Virtual DOM을 가장 많은 예시를 든다. Virtual DOM이 단순히 자바스크립트 객체라면 그렇게 어려운 개념이 아니다. React 공식 문서에도 Virtual DOM란 단어는 이제는 사용하지 않는다. 하지만, 많은 사람들이 사용하고 있는 개념이고 React Element Tree라는 단어보다 더 세련되어 보인다. 또한, 어떠한 사람들은 Shadow DOM이라는 단어와 혼동하기도 한다. Shadow DOM은 Virtual DOM과 아무런 관련이 없다. Shadow DOM은 브라우저 기술로 웹 컴포넌트에 사용된다.
Virtual DOM: Tree of all React elements created from all instances in the component tree
컴포넌트 D의 State가 업데이트 되었다고 가정해보자. 그럼 Re-Render가 발생한다. 이는 React가 Component D의 함수를 재호출하고 새로운 React Element를 생성하고, 새로운 React Element Tree로 변경한다는 의미이다. 즉, 새로운 Virtual DOM이 생성되는 것이다.
React는 컴포넌트는 랜더할 때마다 모든 자식 컴포넌트도 함께 랜더를 발생시킨다. 상기에는 E 컴포넌트가 D와 함께 랜더되는 것이다.이는 Props를 하위 컴포넌트로 전달하는 것과는 상관이 없다. 다시 말해, 하나 이상의 컴포넌트가 업데이트 되어 반환되면 Component Tree에 하위로 중첩된 컴포넌트까지 Re-Render되는 것이다. 이는 상기 이미지의 A 컴포넌트가 Render되면 전체 React Element Tree가 Re-render된다는 것이다. React는 하위 컴포넌트에 영향을 미칠 수 있다는 것을 모르기 때문이다. 이때 중요한 사실은 전체 DOM을 업데이트하는 것은 아니라 Virtual DOM이 다시 생성된다는 것이다.
Reconciliation
새로운 Virtual DOM은 State 업데이트 전에 현재의 Fiber Tree와 조화(reconciled)시키도록 한다. Reconciliation은 React의 Fiber라 불리는 Reconciler에 의해 동작하고, Fiber Tree를 생성한다. Reconciliation 프로세스의 결과로 Fiber Tree가 업데이트 된다. 해당 Fiber Tree는 DOM을 작성(Write)하는데 사용된다.
그렇다면 State가 변경될 때마다 왜 전체 DOM을 업데이트하지 않는 것일까? DOM을 작성하는 것은 무겁고 느리기 때문에 Virtual DOM을 매번 전체 DOM으로 작성하는 것은 매우 비효율적이다. 또한, DOM을 업데이트하는 것은 대부분 작은 부분이며, 변경되지 않은 나머지 DOM은 재사용될 수 있다. 예를 들어, 특정 버튼을 눌렀을 때 Modal 창이 나타나는 사이트를 생각해보자. 이 경우 DOM Element에 Modal만 넣어주고, 나머지는 그대로 있으면 된다. Render가 발생할 때마다 React는 존재하는 DOM를 재사용함으로서 가상 빠르게 동작하려는 방법을 찾으려고 한다. 그렇다면 React는 DOM을 어떻게 재사용하는 것일까? React는 Render의 무엇이 변경되는지 어떻게 알 수 있는 것일까? React는 Reconciliation 프로세스를 통해 이를 인지한다.
Reconciliation은 최근 변경된 State를 반영하기 위해 어떤 DOM elements가 삽입, 삭제, 업데이트되는지를 결정하는 것이다. Reconciliation 프로세스의 결과로 현재의 DOM을 새로운 State 상태로 업데이트하는 데 필요한 DOM 작업 목록이 만들어진다. 앞서 설명했듯이 Reconciliation은 Reconciler의 프로세스에 의해 동작된다. 이에 Reconciler를 React의 엔진(Engine)으로 볼 수 있다. Reconciler는 DOM을 직접적으로 조작하지 않게 해주며, React에게 무엇이 State를 기반으로한 UI의 다음 SnapShot이 어떻게 생겼는지 말해준다.
Reconciler: Fiber
응용 프로그램의 초기 렌더링 동안 Fiber는 전체 React Element Tree(Virtual DOM)를 가져온다(take). 이 Virtual DOM을 기반으로 Fiber Tree라는 또 다른 Tree를 만든다. Fiber Tree는 특별한 내부 트리로 어플리케이션에서 fiber가 가지고 있는 각 Component 인스턴스와 DOM Element의 Tree이다. Fiber는 Virtual DOM의 React Element와는 달리 Fiber는 모든 Render에 대해서 re-created를 발생시키지 않는다. Fiber Tree는 파괴되지 않는다(never destoryed). 그 대신 변경 가능한 데이터 구조이며, 일단 초기 렌더링 단계에서 생성되면 미래에 있을 Redonciliation 단계에서 간단하게 지속적으로 변형한다. 이런 특징들이 Fiber를 현재 Component의 State, Props, Side effects, 사용된 hook 등을 지속적으로 추적하기에 완벽한 장소로 만든다. 실제, Screen에서 보는 컴포넌트 인스턴스의 State와 Props는 Fiber Tree에 대응되는 Fiber 내부에 저장된다.
각 Fiber는 State와 Ref 업데이트, 등록된 Side Effect 실행, DOM 업데이트 등을 수행하기 위한 작업 Queue를 가지고 있다. 그래서 Fiber가 하나의 작업의 단위(Unit of work)로도 정의되기도 한다. 상기 이미지의 Firber Tree를 확인해보자. 왼쪽의 React Element Tree와는 다른 형태로 Tree가 구성된 것을 확인할 수 있다. 해당 구조는 링크 리스트라고 불린다. 이는 React 내에서 Fiber의 작업을 효과적으로 처리할 수 있게 만들어준다. 또한, 양쪽 Tree 모두에서 React Elements나 Components뿐만 아니라 h3나 buttom 같은 일반적인 DOM Elements을 확인할 수 있다. 이는 두 개의 Tree 모두 React Components뿐만 아니라 전체 DOM 구조 정확히 나타낸 것이다.
다시, Fiber의 units of work 개념으로 돌아오자. 가장 중요한 것은 Fiber reconciler는 비동기적으로 동작한다는 것이다. 디사 말해, 이는 Reconciler가 수행하는 Rendering 프로세스 작업이 여러 덩어리로 나뉠 수 있다는 것이다. 작업은 우선순위가 정해질 수 있으며 재사용, 버리기, 멈춤 등을 수행할 수 있다는 의미이다. 이 모든 것이 개발자에게는 보이지 않지만 React의 뒤에서 일어난다는 사실을 기억해야한다. 비동기적 동작은 React 18부터 Suspense 또는 Transitions이라는 특징으로 나타나게 된다. 랜더링 과정을 정지했다가 재개할 수있어 랜더링이 길어질 경우 JavaScript 엔진을 막는 일도 생겨나지 않는다. 이는 Render 단계에서 아직 DOM에 가시적인 출력이 발생하지 않기 때문에 가능한 것이다.
Reconciliation in action
Reconcilication을 이해하기 위해 이전에 나왔던 Tree를 비교해보자. 이는 아래의 이미지에 나와있는 코드와 대응되는 것이다. App Component안에서 현재 true라고 설정되어 있는 showModal이란 state를 호출한다고 가정해보자. 이제 showModal의 state가 false로 업데이트 되었다고 해보자. 이는 Re-Render를 발생시키고 새로운 Virtual DOM을 생성시킨다. 새로 생성된 Tree에는 Modal과 모든 Children Element가 없어진다. 왜냐하면 showModal이 false이면 해당 요소들은 더이상 display될 필요가 없기 때문이다. 또한, 남아있는 모든 React Element는 Re-Render를 의미하는 노란색으로 변경된다. 상기에서 언급했듯이 Re-Render되는 Element의 모든 children Element는 Re-Render되기 때문이다.
이제 새로운 Virtual DOM은 Firber Tree와 함께 Reconciled 과정을 거친다. 그럼 아래의 이미지와 같이 업데이트된 Fiber Tree가 생겨난다. 이는 내부적으로는 Work in progress tree라고 불린다. 이제 Reconciliation이 필요하다. Reconciliation이 발생할때마다 Fiber는 전체 Tree를 단계별로 살펴보고(work through) 새로운 Virtual DOM을 기반으로 Current Fiber Tree와 Updated Fiber Tree를 비교하고 무엇이 변경이 필요한지 분석한다. 이렇게 Element를 단계별로 비교하는 프로세스를 Diffing이라고 한다.
DOM mutation과 관련된 과정을 분석해보자. 먼저 Btn Component는 새로운 text를 가지게 된다. Firber에서 완료해야할 작업은 DOM 업데이트이다. 이 경우 text를 hide에서 rate로 바꾸는 것이다. Modal, Overlay, h3, button는 현재의 Fiber Tree에는 존재하지만, Virtual DOM에는 더이상 존재하지 않는다. 그러므로 해당 Element는 DOM Deletions으로 mark된다. Video Component에 대해서는 재미있는 케이스가 존재한다. 해당 Component는 App의 Child 요소이기에 Re-Render되지만 실제로 변경되는 없으므로 Reconciliation의 결과로 DOM Updated도 없다. 이 과정이 끝나면 DOM Mutation 요소들을 별도의 Effect of List로 분류한다. 이후 다음 Commit 단계에서 실질적으로 DOM을 Mutate 시킨다.
지금까지 Render 단계에 대해서 살펴보았다. 하지만, 지금까지 React는 그 어떠한 DOM도 업데이트 시키지 않고, List of Effect라는 결과물을 만들었다. 이는 Render 단계의 마지막이다.