React의 렌더링 프로세스

limhi·2024년 9월 5일
0
post-thumbnail

리액트의 렌더링에 대해서 전체적인 프로세스를 이해하기 위해 작성하였습니다.

브라우저의 동작 단계

웹 브라우저는 사용자가 요청한 웹 페이지를 화면에 표시하기 위해 여러 단계를 거칩니다.
이 과정을 Critical Rendering Path 라 부르며, 브라우저가 HTML, CSS, JS 등을 처리하여 최종적으로 사용자에게 시각적으로 표시되는 페이지를 생성하는 단계를 의미합니다.

Critical Rendering Path

Critical Rendering Path는 다음과 같은 단계를 거칩니다.

출처 - https://web.dev/articles/critical-rendering-path/render-tree-construction

  1. HTML 파싱: 서버로부터 받아온 HTML 문서로 DOM(Document Object Model) 트리 생성
  2. CSS 파싱: CSS에서 CSSOM 트리 생성
  3. Render Tree 생성: DOM과 CSSOM을 병합하여 Render Tree 생성

출처 - https://web.dev/articles/critical-rendering-path/render-tree-construction

  1. Layout 계산: 각 요소의 정확한 위치와 크기를 정확하게 계산
  2. Painting: Render Tree의 각 노드를 화면의 실제 픽셀로 변환, 요소의 모든 시각적 부분을 그리는 작업을 포함

브라우저의 업데이트

그런데 위 과정이 한 번만 발생하는 것은 아닙니다.

브라우저는 사용자 인터랙션이나 JS 실행에 의해 DOM이 변경될 경우, 변경사항을 렌더링하기 위해 위의 Critical Rendering Path가 다시 순차적으로 진행됩니다.

이 반복적인 과정은 Reflow와 Repaint라는 두 가지 주요작업으로 인해 성능에 상당한 영향을 미칠 수 있습니다.

  • Reflow: DOM의 구조가 변경될 때 발생하며, 레이아웃을 다시 계산해야 합니다.
  • Repaint: 요소의 시각적 속성(색상, 배경 등)이 변경될 때 발생합니다.

업데이트를 최적화하는 방법

<script>
  function handleListUpdate() {
    const $ul = document.getElementById("ul");
    for (let i = 0; i < 3000; i++) {
      $ul.innerHTML += `<li>${i}</li>`;
    }
  }
</script>
<body>
  <button onClick="handleListUpdate()">리스트 추가하기</button>
  <ul id="ul"></ul>
</body>

위 코드의 handleListUpdate() 에서는 반복문 내부에서 innerHTML을 통해 li 태그를 3000번 추가하고 있습니다.
즉, 3000번동안 계속 DOM을 수정하는 것이고, Reflow와 Repaint가 3000번 발생하는 것입니다.

이 코드가 실제로 실행되는 속도를 측정했을 시 4,500ms가 걸립니다.

하지만 이렇게 수정해보는 것은 어떨까요?

<script>
 function handleListUpdate() {
   const $ul = document.getElementById("ul");
   let list = "";
   
   for (let i = 0; i < 3000; i++) {
     list += `<li>${i}</li>`;
   }
   $ul.innerHTML = list;
 }
</script>
<body>
 <button onClick="handleListUpdate()">리스트 추가하기</button>
 <ul id="ul"></ul>
</body>

반복문에서 수정되는 내용을 변수에 담아 DOM을 한 번만 수정하도록 코드를 수정하였습니다.
실제 실행 속도를 측정하였을 때 250ms가 걸리는 것으로 22배 개선됨을 알 수 있습니다.

위와 같이 반복적인 DOM 업데이트는 성능면에서 매우 좋지 않다는 것을 알 수 있습니다.
웹 페이지의 성능을 위해 업데이트를 모아서 DOM을 한 번에 수정하는 방향으로 코드를 작성하는 것, 즉 DOM을 적게 수정하는 것이 중요합니다.

이러한 성능 저하 문제는 대규모의 코드에서 더욱 두드러지며, 이를 최소화하기 위해 다양한 최적화 기법을 적용해야 합니다.

React의 렌더링

리액트는 이 과정을 최적화하기 위해 '업데이트를 모아서 DOM을 적게 수정하는 과정' 을 자동화합니다.

렌더링이란

렌더링이란 현재 props 및 상태를 기반으로 React가 컴포넌트에게 UI 영역이 어떻게 보이길 원하는지 요청하는 프로세스입니다.

렌더링 프로세스

리액트는 컴포넌트 트리의 루트에서 시작해 업데이트가 필요하다고 표시된 모든 컴포넌트를 찾기 위해 아래로 순회합니다. 플래그가 지정된 각 컴포넌트에 대해 리액트는 함수형 컴포넌트의 경우 FunctionComponent() 를,
클래스 컴포넌트의 경우 classComponentInstance.render() 를 호출하고 렌더링의 출력 결과를 저장합니다.

이는 JSX 구문으로 작성되며 자바스크립트가 컴파일되고 배포를 위해 준비될 때 React.createElement() 호출로 변환됩니다. createElelment 는 의도된 UI의 구조를 설명하는 일반 자바스크립트 객체인 React 요소를 반환합니다.

// JSX 구문
return <MyComponent a={42} b="testing">Text here</MyComponent>

// 아래와 같은 호출로 변환
return React.createElement(MyComponent, {a: 42, b: "testing"}, "Text Here")

// React Element
{type: MyComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]}

React Element를 수집한 후 리액트는 새로운 객체 트리인 Virtual DOM을 생성하고 실제 DOM을 원하는 출력과 같이 보이게 하기 위해 비교 및 계산을 합니다. 이 과정을 Reconciliation 이라고 합니다.

그리고 리액트는 계산된 변경사항을 하나의 동기 시퀀스로 DOM에 적용합니다.

렌더 및 커밋 단계

리액트의 공식문서에서는 렌더링 단계를 두 개로 분류하여 설명합니다.

  • Render
    : 컴포넌트를 렌더링하고 변경사항을 계산하는 것까지의 과정
  • Commit
    : 렌더 단계에서 계산된 변경 사항을 DOM에 적용하는 단계

리액트가 커밋 단계에서 DOM을 업데이트한 후, 요청된 DOM 요소 및 컴포넌트 인스턴스를 가리키도록 모든 참조를 적절하게 업데이트 합니다.
그런 다음 componentDidMountcomponentDidUpdate 클래스 라이프 사이클 메소드와 useLayoutEffect 훅을 동기적으로 실행합니다.

그리고 리액트는 짧은 시간제한을 설정한 후, 시간이 만료가 되면 useEffect 훅을 호출합니다.
이러한 단계를 Passive Effects 단계라고 불리웁니다.

React 18 버전이 업데이트 되면서 Concurrent mode (동시성 모드) 을 제공하는 새로운 기능이 추가되었습니다. 이를 통해 브라우저가 이벤트를 처리할 수 있도록 렌더링 단계에서 작업을 일시중지할 수 있습니다. 리액트는 나중에 적절하게 작업을 재개하거나, 폐기하거나 다시 계산합니다. 렌더링이 패스된 이후에도, 리액트는 마찬가지로 커밋 단계를 동기적으로 실행합니다.

렌더링 !== DOM 업데이트

여기서 이해해야 할 핵심은 "렌더링""DOM 업데이트" 와 같지 않으며, 결과적으로 어떠한 가시적인 변경이 없이도 컴포넌트가 렌더링될 수 있다는 것입니다.

즉, 다음과 같습니다.

  • 컴포넌트가 지난번과 동일한 렌더링 출력을 반환해 변경이 필요하지 않을 수 있다.
  • Concurrent mode 에서 리액트는 컴포넌트를 여러번 렌더링할 수 있지만, 다른 업데이트로 인해 현재 수행 중이던 작업이 무효화되는 경우 렌더링 출력을 버린다.

이를 시각화한 리액트 훅 순서도 입니다.
+React hooks Lifecycle
+React Lifecycle methods diagram

Reference

React 공식 문서: Render and Commit
React.js의 렌더링 방식 살펴보기
(번역) 블로그 답변: React 렌더링 동작에 대한 (거의) 완벽한 가이드

profile
null 사랑하지 않아 - 어반자카파

0개의 댓글