방구석에 놔둔 개발 노트

1년차 웹 / 앱 프론트엔드 엔지니어의 좌충우돌 얼렁뚱땅 앞뒤짱구 생존기

React에서의 렌더링 과정, 그리고 Fiber

📄 머리말

React를 공부해 본 사람들이라면 가상 DOM(Virtual DOM)이라는 이름을 들어본 사람은 적지 않을 것이다. 가상 DOM을 통해 우리가 React에서 수정한 내용들이 담겨지고 이를 실제 DOM에 반영한다는 건 나도 그렇고, 많은 사람들이 아는 이야기인데.. 하지만, 정작 그 과정에 대해 알아보려고 하진 않았던 것 같다.

그래서 이번 글에서는 기존 DOM에서의 렌더링부터, 가상 DOM, 그리고 이 가상 DOM을 이용한 React에서의 렌더링에 대해 공부한 내용을 정리해봤다. 이번에 공부한 내용을 모던 리액트 Deep Dive 책을 참고해서 공부했다.

부족하거나 잘못 정리한 내용이 있다면 언제나 댓글로 감사히 받고 참고하겠다.

📶 브라우저에서의 렌더링 과정에 대해서 잠시 짚고 가보자.

우선 DOM에 대해서 한 번 짚고 가자.

DOM은 Document Object Model의 약자이며, 웹페이지에 대한 인터페이스로 브라우저가 웹페이지의 콘텐츠와 구조를 어떻게 보여줄지에 대한 정보를 담고 있다.


브라우저에서는 이 DOM을 이용해서 화면을 구상하고, 배치하고, 그려낸 뒤 이용자에게 보여준다.

  1. 브라우저가 사이트의 HTML 파일을 다운로드 받는다. 파싱 단계
  2. 브라우저에서는 HTML을 파싱하고, 각 노드로 구성된 트리를 만든다. (DOM) DOM 트리 구축 단계
  3. 파싱 중 CSS 파일을 발견하면 해당 파일을 다운로드하고, 이 역시 CSS 노드로 구성된 트리를 만든다. (CSSOM) CSSOM 트리 구축 단계
  4. 파싱이 완료되면 완성된 DOM 트리를 최상위인 html부터 최하위 노드까지 순회하여 화면에 보여지지 않는 요소들(dispaly:none)은 배제하고 그려질 노드들만 렌더 트리로 구축한다. 렌더 트리 구축 단계
  5. 이후 구축이 완료된 렌더 트리를 기준으로 똑같이 html부터 순차적으로 CSS 스타일 요소를 적용하기 시작한다. 렌더 트리 배치 단계
    1. 과정은 화면에 어디에 배치할지를 계산하는 레이아웃부터 진행된다.
    2. 이후, 레이아웃이 끝나면 색을 입히는 과정인 페인팅이 진행된다.
  6. 이 과정까지 완료 되면 최종 출력물인 웹 페이지가 브라우저에 노출된다. 렌더링 단계

해당 과정을 도식화하면 아래와 같이 보여질 수 있다.

출저: https://hangem-study.readthedocs.io/en/latest/front_interview/browser-rendering/
출저: https://medium.com/@gneutzling/the-rendering-process-of-a-web-page-78e05a6749dc

이후 화면에 그려진 다음 상호작용 과정 등이 발생하면 리플로우나 리페인팅이 발생하게 된다.

  • 리플로우(Reflow)
    • 특정 레이아웃 등의 변화로 인해서 배치가 변경되거나 크기가 수정됐을 경우 레이아웃 계산을 다시 하게 되는데 이 과정을 리플로우라고 가리킨다.
    • 레이아웃이 변화하게 되는 만큼, 리플로우 이후에는 리페인팅도 다시 일어난다.
  • 리페인팅(Repainting)
    • 위와 같이 리플로우가 일어나거나, 또는 특정 상황에 의해서 요소의 색상이 변경되거나 할 경우에 화면에 다시 색을 입히거나 변경해주는 과정을 진행하게 되는데 이를 리페인팅이라고 부른다.

브라우저는 위와 같은 과정을 거쳐서 웹 페이지를 렌더링하게 된다.

이해가 되지 않는다면 요소가 간단히 있는 페이지에서 네트워크를 느리게 하고 다운 받는 파일들을 확인해보면 html을 먼저 받고, css 파일을 그 다음에 받는 것을 확인할 수 있다.

처음에 html 문서를 받고, 뒤이어 css 파일이 받아지는 걸 볼 수 있다.

이렇게 브라우저에서 말하는 렌더링 과정에 대해서 살펴봤다.

그렇다면 React에서 말하는 렌더링은 무엇을 가리키는 걸까?

🎨 React에서 말하는 렌더링이란?

React에서 말하는 렌더링은 쉽게 말하자면 우리가 말했던 가상 DOM(Virtual DOM)을 이용해서 변경된 사항들을 실제 DOM에 반영하는 과정이라고 말할 수 있다.

좀 더 깊게 말하자면, id=”root”를 기준으로 생성되는 React의 어플리케이션 트리 안에 모든 컴포넌트들이 현재 자신들이 가진 props와 state를 기반으로 UI를 구성하고, 이를 계산해 DOM에 반영하는 과정이다.

React에 대해서 가볍게라도 공부해본 사람이라면 React에서의 렌더링은 크게 두 가지를 가리킨다. 바로 초기 렌더링과 리렌더링이다.

  • 초기 렌더링
    • React의 어플리케이션을 처음 진입하면 보여져야 할 내용들을 그려줘야 볼 수 있다. 이 과정을 초기 렌더링 혹은 최초 렌더링이라고 부른다. (이 글에서는 초기 렌더링이라고 부르겠다.)
  • 리렌더링
    • 위의 초기 렌더링을 제외하고 일어나는 React 어플리케이션의 렌더링 방식은 리렌더링이라고 부르고 있으며, 리렌더링이 일어나는 이유는 여러가지가 있다.
    • React에서의 State가 변경되는 경우
      • 이는 useState의 코드 속에 있는 render()라는 함수를 통해서 렌더링을 일으키게 되는데 요 부분에 대해서는 추후 useState의 과정을 살펴보는 글을 쓰게 되면 다뤄보도록 하겠다.
    • key props에 변화가 발생한 경우
      • 여러 수많은 요소들을 배치해야 하는 상황일 때 map() 메소드를 통해서 요소를 배치해 본 경험이 있을 것이다. 이때 이 메소드에 배치되는 요소에 key prop을 적용해야 한다는 경고를 본 적이 있을 텐데, 이런 key props에 변화가 생길 경우에도 리렌더링이 필연적으로 일어나게 된다.
    • 부모 컴포넌트가 렌더링 되거나 props가 변경된 경우
      • 부모 컴포넌트에 렌더링이 발생했다면 부모 컴포넌트가 포함하고 있는 자식 컴포넌트 또한 필연적으로 렌더링이 일어날 수 밖에 없다.
      • 또한 props로 받아오는 값들은 전부 부모 컴포넌트를 통해서 받아오는 값들이다. 해당 값들이 변한다는 건 필연적으로 자식 컴포넌트에도 변화를 줘야하는 상태이므로 리렌더링이 일어나게 된다.

이처럼 React에서 말하는 렌더링이 무엇인지, 그리고 렌더링이 어떻게 구별되는지도 알았다. 그러면 대체 어떤 식으로 렌더링이 일어난다는 걸까?

렌더링을 하는데 가상 DOM을 어떤 식으로 이용하여 실제 DOM에 반영하게 된다는걸까?

📝 가상 DOM이 대체 뭐고, 어떠한 장점이 있는 걸까?

복잡하기 짝이 없는 브라우저에서의 렌더링 과정은 변화가 발생할 때마다 리플로우나 리페인팅이 계속해서 일어날 수 밖에 없는데, 당연히 이런 과정이 일어날 때마다 화면을 다시 배치하거나 그리는 비용은 필연적으로 커질 수 밖에 없다.

심지어 애플리케이션의 상호작용이 늘어난 요즘을 생각하면 그 비용은 이전보다 더 엄청날 것이다.

이런 과정에서 React를 만든 사람들은 이 문제를 해결하기 위해 고민했다.

어떻게 하면 이런 비용을 줄일 수 있을까?’라는 물음을 던진 그들은 곧이어 React에서 활용되는 가상 DOM을 만들어내게 된다.

가상 DOM(Virtual DOM)이라는 건 단어 그대로, 가상의 DOM이라는 의미인데 실제 브라우저의 DOM을 본따서 React에서만 사용되고 관리하도록 만든 DOM의 복사본이다.


React는 실제 DOM 데이터를 메모리에 저장하고, React에서 가상 DOM에서 변경 사항을 모두 반영했을 때 메모리에 저장해둔 DOM 데이터를 불러온 후 변경된 사항을 반영하고 이를 브라우저에 적용되도록 처리한다.

(이 때의 React는 React 패키지를 설치하면 함께 설치되는 react-dom을 가리킨다.)

이 방식을 거치면 리페인팅이나 리플로우가 일어날 때마다 DOM에 계속해서 반영되는 식이 아니라 React의 가상 DOM에서 모든 계산을 한꺼번에 다 처리하고, 그 처리가 완료되면 메모리에 저장된 DOM 데이터를 가져와 변경된 내용을 적용하기 때문에 계산 비용이 줄어들 뿐만 아니라 브라우저의 렌더링 과정을 최소화할 수 있다는 장점이 있다.


🧵 가상 DOM은 어떻게 만들어질까? - React Fiber

가상 DOM이 어떤 건진 위의 내용을 통해 이해가 됐다.

그렇다면 가상 DOM은 어떻게 만들어지고, 어떤 과정을 통해 DOM에 적용하는 비용을 최소화할 수 있는 걸까?

이를 위해서 알아야 할 친구가 하나 있다. React Fiber라는 녀석이다.

React 파이버 아키텍처 분석

React 톺아보기 - 05. Reconciler_5 | Deep Dive Magic Code

React 톺아보기 - 05. Reconciler_2 | Deep Dive Magic Code

(좀 더 자세하게 알고 싶은 사람은 위의 글을 정독해보길 바란다.)


- React Fiber?

React Fiber란 React에서 관리하는 자바스크립트 객체이다.

React에서는 가상 DOM과 실제 DOM을 비교해 변경 동향을 파악하고 둘에 차이가 존재한다면 변경 사항을 관리하는 이 Fiber를 이용해서 화면에 렌더링을 요청하게 된다.

이처럼 렌더링 과정이 실행되면서 각 컴포넌트의 렌더링 결과들을 수집하고, 기존의 가상 DOM과 비교해 실제 DOM에 반영하기 위한 모든 변경 사항을 차례대로 수집하는 과정을 재조정(Reconcilation)이라고 부르며, Fiber는 이런 재조정 과정을 관리해주는 재조정자(Reconciler)라고 부르기도 한다.

React는 이 Fiber를 이용해 여러 인터렉션에서 발생하는 반응 이슈를 해결하고 올바른 결과물을 화면에 렌더링하고자 했다.

그럼 React Fiber는 어떻게 이루어져 있을까?

function FiberNode(
  this: $FlowFixMe,
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;
  
  .
  .
  .
  
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  
  this.alternate = null,
    .
    .
    .

}

위와 같이 생겼는데, 사실 이보다 내용은 더 많지만, 여기서는 일부 내용만 다루겠다.

  • tag
    • Fiber가 어떤 것을 담고 있는지를 분류해주기 위한 말 그대로의 태그다. 여기서 이 태그는 HTML의 DOM 노드일 수도 있고, 다른 것일 수도 있다.
  • key
    • 각 요소별로 가지고 있는 고유한 key를 가리킨다. 아까 리렌더링에서 이야기할 때 언급했던 key가 바로 이 key를 가리킨다.
  • stateNode
    • 해당 Fiber가 참고하게 되는 실제 요소나 컴포넌트를 가리킨다.
    • 예를 들어 <div id=”root” />라는 요소와 관련한 Fiber에서의 stateNode는 div#root를 참고한다.
  • child, sibling, retrun, index
    • child는 해당 Fiber가 가지고 있는 하위 Fiber를 가리킨다.
      • 여기서 한 가지 알고 있어야 할 것은 React의 모든 컴포넌트나 DOM 노드 등은 하나의 Fiber 객체를 부여받으며 이 Fiber는 가장 먼저, 첫 번째 Fiber를 가리킨다.
    • sibling은 자식 Fiber나 같은 depth의 Fiber와 관련하여 다음에 올 Fiber를 가리킨다.
    • return은 이런 자식 Fiber를 담고 있는 부모 fiber를 가리킨다.
    • index 은 같은 depth에 있는 Fiber들 중에서 어떤 순서를 가지고 있는지를 나타낸다.

출저: https://medium.com/@jettycloud/react-fiber-concurrency-part-1-b9d287077d1b

  • pendingProps, memoizedProps
    • 위에서 살펴봤던 props가 변경됐을 때 리렌더링이 일어난다고 했는데, 그 props를 관리하는 내용이 이 두 속성이다.
    • pendingProps는 전달받은 props가 렌더링이 일어나기 전에 배치되는 내용이다.
    • memoizedProps는 렌더링이 완료된 후 pendingProps가 옮겨질 내용이다. 렌더링 이후 pendingProps가 여기로 옮겨진다고 생각하면 된다.
  • updateQueue
    • Fiber와 관련하여 필요한 작업들을 담아두는 Queue이다.
      • 상태를 업데이트 한다던가, 콜백 함수를 실행시킨다던가 등의 작업들이 관리된다.
  • memoizedState
    • 현재의 함수형 컴포넌트와 관련하여 Hook Function들이 여기에 저장된다.
  • alternative
    • 변경 전후의 Fiber를 비교하기 위해 필요한 값이다.

내용 중에 이야기를 했지만 1개의 Fiber에는 1개의 컴포넌트나 DOM 노드 등과 엮어져 있으며, 그리고 이런 Fiber는 state가 변경되는 등의 DOM의 변경이 필요한 상황에서 실행된다. 이런 Fiber는 keystateNode, tag 등을 통해 보이듯이 UI를 단순하거나 복잡한 값들을 이용해서 객체에 관리하고, 작성한 JavaScript의 코드에 맞게 표현되도록 적용되어 있다.

이런 Fiber를 통해 React가 표현하고자 하는 것은 크게 세 가지이다.

  • 각 컴포넌트나 DOM 노드 들을 1개의 Fiber로 단위를 쪼갠 뒤, indexchild, sibling을 통해 보이듯 우선 순위를 매겨서 적용할 작업을 진행한다.
  • 또한 필요에 따라서는 이러한 작업을 일시 중지하거나 나중에 다시 시작할 수 있다. (pendingProps, memoizedProps)
  • 그 밖에도 이전에 했던 작업을 다시 재사용하거나, 필요하지 않은 경우 폐기하는 것도 가능하다. (updateQueue, memoizedState)

그럼 React에서는 이 Fiber를 이용해 가상 DOM에서 어떤 방식으로 처리하고 DOM에 변화된 내용을 적용하는걸까?

🌳 Fiber를 연결하고 연결해 만들어지는 React Fiber Tree

React의 가상 DOM에는 Fiber로 구성된 2개의 Tree가 존재한다.

하나는 현재 유저가 보고 있는, 렌더링이 이미 된 상태의 화면 구성을 나타내는 current Tree이며, 다른 하나는 이후에 인터렉션 등을 통해서 변경될 사항들이 반영되어있는 workInProgress Tree이다.

React는 각 Fiber에서의 작업들이 끝나는대로 workInProgress Tree를 current Tree로 변경한다.

잠시 야구장에 있는 전광판을 생각해보자.

현재 나와있는 투수가 갑자기 교체되어야 할 상황이 발생하면, 전광판 시스템은 다음 투수가 누구인지를 받아내는 대로 그 정보들을 한꺼번에 정리한 다음 완료가 되면 아나운서의 소개 음성과 함께 관중들이 볼 수 있도록 전광판에 띄워준다.

이처럼 보이지 않는 곳에서 다음 그림을 다 그려낸 뒤에, 완성되면 그 그림을 현재 그림에서 바꾸는 방법을 더블 버퍼링이라고 하는데, React의 가상 DOM에서는 이 두 트리를 이용해서 각각 변화된 사항을 처리하고 있다.

출저: https://medium.com/@ejtac/react-fiber-algorithm-28b7a7665081

이 과정을 이용해서 React는 업데이트가 발생하면 workInProgrss Tree에 새로운 데이터를 반영하고 빌드하기 시작한다.

그리고 빌드가 완료되면 다음 렌더링에 이 트리를 사용해 렌더링을 반영하게 되고, 그렇게 되면 workInProgress Tree의 내용이 이제 current Tree로 덮어씌워지게 된다.

좀 더 쉬운 이해를 위해 이미지를 보도록 해보자, 아래는 current Tree이다.

출저: https://dev.to/afairlie/to-understand-react-fiber-you-need-to-know-about-threads-3dof

위 Tree에서 일부 컴포넌트에 변화가 발생했다고 하면, Fiber Tree는 아래의 그림처럼 workInProgress Tree에서 변화된 과정들을 추적하고 반영하는 작업을 하게 된다.

출저: https://dev.to/afairlie/to-understand-react-fiber-you-need-to-know-about-threads-3dof

Fiber의 작업 순서는 크게 파이버 작업을 수행하는 beginWork(), 작업이 완료되면 파이버 작업을 끝내는 completeWork(), 그리고 모든 Fiber의 변경점이 반영됐을 시에 수행되는 commitWork()로 이루어진다.

위의 그림에서는 다음과 같은 과정으로 진행될 것이다.

  1. <a1>에서 beginWork() active
  2. <b1>에서 beginWork() active, 별도의 자식이 없으므로 completeWork()로 종료
  3. <b2>에서 beginWork() active, 자식이 있으므로 <c1>으로 이동
  4. <c1>beginWork() active, 자식이 있으므로 <d1>으로 이동
  5. <d1>beginWork() active, 별도의 자식이 없으므로 completeWork()로 종료
  6. <d2>beginWork() active, 별도의 자식이 없으므로 completeWork()로 종료
  7. <c1>의 자식에서 모든 수행을 완료했으므로 completeWork()로 종료
  8. <b2>의 자식에서 모든 수행을 완료했으므로 completeWork()로 종료
  9. <b3>에서 beginWork() active, 자식이 있으므로 <c2>로 이동
  10. <c2>beginWork() active, 별도의 자식이 없으므로 completeWork()로 종료
  11. <b3>의 자식에서 모든 수행을 완료했으므로 completeWork()로 종료
  12. <a1>의 자식에서 모든 수행을 완료했으므로 completeWork()로 종료
  13. 최상위인 <a1>까지 모든 과정을 마쳤으므로 completeWork() active, 업데이트가 필요한 사항이 DOM에 반영됨

이러한 과정을 거쳐서 가상 DOM에서 변경 사항들을 반영한 뒤, commitWork()까지 마쳐서 변경된 current Tree는 이제 DOM에 반영되도록 처리된다.

여기서 1번부터 12번까지, commitWork()가 작동되기 전 변경 사항들을 파악하는 단계를 Render Phase라고 부르며, 최종적으로 13번에서 변경 사항이 파악된 사항을 가상 DOM에서 실제 DOM으로 적용하는 과정을 Commit Phase라고 부른다.

참고로 Render Phase비동기적으로 진행되는 반면, Commit Phase는 모든 내용을 다 한 번에 반영해야 하므로 동기적으로 진행된다.

그리고, 앞서 말했던 재조정(Reconcilation)하는 과정이 바로 Render Phase에서 일어나게 된다.

마지막으로 현재의 Function Component 환경인 React Hook Cycle에서 Render Phase와 Commit Phase가 어느 부분에서 일어나는지 확인해보자.

출저: https://medium.com/@galmargalit/react-function-components-hooks-lifecycle-diagram-14f76e0a5988

💮 정리

지금까지 언급된 과정들을 요약해서 정리해면 다음과 같다.

  • React에서의 렌더링이란, 실제 DOM을 본 딴 React에서만 사용되는 가상 DOM에서 각 컴포넌트에서 변경될 사항들을 수집하고, 계산하여 렌더링을 요청하는 재조정 과정을 거친 뒤, 이를 실제 DOM에 반영하는 과정을 말한다.
  • React의 렌더링에는 변경 과정을 비동기적으로 체크하고 수집하여 요청하는 Render Phase, 그리고 Render Phase에서 만든 결과물을 실제 DOM에 반영하도록 해주는 Commit Phase로 진행된다.
  • Render Phase에서는 React에서 각 컴포넌트나 DOM 요소를 관리하는 객체인 Fiber들로 모여진 Fiber Tree를 이용해 변경 전인 current Tree를 기반으로 변경된 사항들을 모아놓은 workInProgrss tree를 구축한다.
  • Commit Phase에서는 구축이 완료된 workInProgress Tree를 실제 DOM에 반영하면서, current Tree로 적용하는 과정을 거친다.
  • 이러한 React의 렌더링 방식을 통해 브라우저에서 일어나는 리플로우나 리페인팅에 따른 렌더링 비용을 최소화할 수 있게 된다. (렌더링이 빨라지는 것은 아니자, 주의하자!)

    🔖 참고 자료

wikibook.co.kr

The rendering process of a web page.

React 파이버 아키텍처 분석

React 톺아보기 - 05. Reconciler_5 | Deep Dive Magic Code

React 톺아보기 - 05. Reconciler_2 | Deep Dive Magic Code

To Understand React Fiber, You Need to Know About Threads

React Fiber Algorithm

React Fiber & Concurrency. Part 1

⚛ React Hooks: Lifecycle Diagram

https://hangem-study.readthedocs.io/en/latest/front_interview/browser-rendering/