React - Render and Commit

jiny·2023년 4월 3일
34

React

목록 보기
8/9
post-thumbnail

What is "Rendering"?

리액트에서는 렌더링을 다음과 같이 묘사합니다.

React가 컴포넌트에게 props와 state의 조합을 기반으로 UI가 어떻게 생겼으면 좋겠는지 설명하도록 요청하는 프로세스

컴포넌트는 주방에서 재료로 요리를 만드는 요리사로써, 리액트는 고객의 요청을 접수하고 주문을 가져오는 웨이터로써 역할을 수행한다고 리액트 공식문서에서 설명하고 있습니다.

또한, 공식문서에서는 렌더링 과정을 다음과 같이 총 3단계로 설명합니다.

  1. Triggering a render
  2. Rendering the Component
  3. Committing to the DOM

Trigger a render

리액트에서 렌더링이 발생될 수 있는 경우는 총 2가지라고 설명합니다.

  1. initial render (컴포넌트의 초기 렌더링)
  2. Re-renders when state updates (상태 변화로 인한 리렌더링)

Initial render

앱이 시작될 때 초기 렌더링을 트리거하게 되는데, 이는 createRoot 함수와 render 함수를 통해 이뤄지게 됩니다.

// index.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
)

앱이 시작되면 초기 렌더링을 트리거 해야 합니다. 대상 DOM 노드로 createRoot를 호출 후 컴포넌트로 해당 렌더링 메서드를 호출합니다.

createRoot

브라우저 DOM 노드 안에 React 컴포넌트를 표시하는 루트를 생성하는 메서드

ReactDOM.createRoot(document.getElementById('root') as HTMLElement)

createRoot 메서드에는 첫 번째 인자로 domNode, 두 번째 인자로 option를 추가(optional)할 수 있습니다.

이 메서드를 통해 브라우저 DOM 엘리먼트 내 콘텐츠를 표시하기 위한 React Root Node를 생성할 수 있습니다.

<html lang="en">
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

리액트에서 실제로 사용되는 index.html 를 잘 보면 id가 root가 있는 것을 확인할 수 있습니다.

console.log(
  ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
);

아까 index.tsx에 있는 createRoot 메서드를 console로 찍어보았습니다.

ReactDOMRoot 라는 인스턴스가 생성되는데 이는, FiberRootNode를 통해 생성되는 인스턴스 입니다.

내부를 잘 살펴보면 current 프로퍼티에 tag(고유한 어떤 값)라는 속성이 있는걸 확인할 수 있습니다.

// react-dom.development.js
var FunctionComponent = 0;
var ClassComponent = 1;
var IndeterminateComponent = 2; // Before we know whether it is function or class

var HostRoot = 3; // Root of a host tree. Could be nested inside another node.

var HostPortal = 4; // A subtree. Could be an entry point to a different renderer.

node_modules 내 react-dom.development.js을 뜯어보면 3번이 HostRoot라는 것을 확인해볼 수 있습니다.

즉, ReactDOMRoot 인스턴스는 리액트 앱의 가장 최상단 Root 인 것을 확인할 수 있습니다. (Fiber와 Tag는 다음 포스팅에서 더 자세히 설명해보겠습니다.)

render

JSX 들을 브라우저 DOM node로 렌더 하는 메서드

즉, render()를 통해 브라우저 DOM Element 안에 React 컴포넌트를 표시하게 됩니다.

실제로 개발자 도구를 보면 id="root"인 div 태그 안에 h1 태그의 노드가 들어가 있는 것을 확인해볼 수 있습니다.

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <span>my name is jinyoung</span>
  </React.StrictMode>
);

<App/> 컴포넌트 대신 다음과 같이 my name is jinyoung이 있는 컴포넌트를 render 함수에 포함시키게 되면 브라우저 DOM에 해당 컴포넌트를 렌더링하게 됩니다.

Re-renders when state updates

컴포넌트가 처음 렌더링 시 setState를 통해 상태를 업데이트 하여 추가적인 렌더링을 발생 시킬 수 있습니다. 컴포넌트의 상태를 업데이트 시 렌더링이 자동으로 렌더 큐에 추가 됩니다.

리액트 공식 문서에서는 레스토랑에서 손님이 첫 주문 후 갈증이나 배고픔의 상태에 따라 차, 디저트 등 다양한 음식을 주문하는 것을 예로 설명합니다.

function App() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount((prev) => (prev = prev + 1))}>
      {count}
    </button>
  );
}

export default App;

count 앱을 예로 들면, setCount라는 함수는 카운트가 1 증가 하도록 트리거 하여 리 렌더링을 발생시키게 됩니다. 리 렌더링이 발생 시 count의 값은 1 증가한 값으로 브라우저에서 렌더링 될 것입니다.

React renders your components(render phase)

렌더링을 트리거하게 되면 React는 컴포넌트를 호출하여 화면에 표시될 내용을 파악합니다. "렌더링"은 리액트가 컴포넌트를 호출하는 것을 의미합니다.

초기 렌더링에서 React는 Root 컴포넌트 (ex) app.tsx) 를 호출 합니다.
이후 렌더링에서 리액트는 상태 변화가 렌더링을 트리거하는 함수 컴포넌트를 호출 합니다.

업데이트 된 컴포넌트가 다른 컴포넌트를 반환 할 경우 리액트는 그 컴포넌트를 다음에 렌더링 하고, 그 컴포넌트 역시 무언가를 반환 시 그 컴포넌트를 다음에 렌더링하는 식으로 재귀적인 프로세스를 수행합니다.

이 프로세스는 중첩된 컴포넌트가 더 이상 존재하지 않을 때 까지, 그리고 리액트가 화면에 표시해야 할 내용을 정확히 파악할 때 까지 계속됩니다.

createElement

function App() {
  return <h1>hi</h1>
}

export default App;

h1태그로 hi라고 되어있는 App 컴포넌트가 있습니다.

import { createElement } from "react";

function App() {
  return createElement("h1", null, "hi");
}

export default App;

사실 저 h1 태그의 경우 createElement가 반환하는 자바스크립트 객체입니다. 실제로 그대로 출력해도 똑같이 브라우저에서 확인이 가능합니다.

즉, 리액트 함수 컴포넌트들은 자바스크립트 객체로 이루어져 있으며 이 객체는 메모리 상에 저장되게 됩니다.

Virtual DOM

이런 자바스크립트 객체로 이루어진 컴포넌트들은 트리 형태로 이루어지게 되는데, 이런 형태의 데이터를 Virtual DOM(가상 돔)이라고 합니다.

DOM 노드를 생성하거나 기존 DOM 노드에 접근하는 것이 JavaScript 객체로 표현된 트리 노드를 생성하거나 접근하는 거에 비해 느리기 때문에 JavaScript 객체로 표현된 트리에 CRUD 작업을 수행하는 것이 DOM 노드에 CRUD 작업을 수행하는 것보다 일반적으로 더 빠릅니다. (대신 메모리 사용량이 늘어난다는 단점은 있습니다.)

즉, Virtual DOM을 통해서 리액트 내 성능 향상을 기대할 수 있습니다.

Reconciliation

컴포넌트에서 prop이나 state가 변경 되었을 때, 직전 렌더링 된 요소와 새로 반환된 요소를 비교하여 실제 DOM을 업데이트 할지 말지 결정해야 합니다.

이 때, 두 Element가 일치 하지 않으면 새로운 요소로 DOM을 업데이트 하기까지의 프로세스를 의미합니다.

즉, 실제 DOM에 반영하기 전까지 VirtualDOM에서 변경된 요소를 확인 후 그 요소만 실제 DOM에 반영하는 것입니다.

Commit Phase 정리 (3번 컴포넌트가 변경 될 것 이라고 가정)

리액트는 상태 업데이트를 감지하면 렌더링을 큐에 넣은 후 트리의 최상단 부터 렌더 패스를 시작합니다.

1번 컴포넌트의 경우 업데이트가 필요하지 않은 것을 알고 지나칩니다.

2번 컴포넌트의 경우에도 업데이트가 필요하지 않은 것을 알고 지나칩니다.

3번 컴포넌트의 경우 업데이트가 필요하다는 것을 알고 렌더링 하게 됩니다. (이때, 중요한 건 부모 컴포넌트가 렌더링하면 모든 자식 컴포넌트들도 재귀적으로 렌더링 한다는 점 입니다.)

4, 5번 컴포넌트들은 업데이트가 필요하지 않지만, 부모 컴포넌트인 3번 컴포넌트가 렌더링 되었기 때문에, 둘 다 렌더링하게 됩니다.

최종 적으론, 3번을 제외하고 나머지 컴포넌트 들은 같은 결과물을 반환하기 때문에 DOM에 반영할 이유가 없습니다.

이때, 어떻게 필요한 3번 컴포넌트만 DOM에 반영할까요?

이때, Virtual DOM을 사용하여 기존 Virtual DOM과 업데이트 후 Virtual DOM에서 변경된 부분만을 계산(Diffing 알고리즘 활용)합니다.

여기 까지의 과정이 Render Phase(컴포넌트를 렌더링하고 변경 사항을 계산하는 모든 과정이 이루어지는 단계) 입니다.

예시

// Button.tsx
import { useState } from "react";
function OtherButton() {
  return <button>my name is other button!</button>;
}
function Button() {
  const [count, setCount] = useState(0);
  return (
    <section>
      <button onClick={() => setCount((prev) => (prev = prev + 1))}>
        {count}
      </button>
      <OtherButton />
    </section>
  );
}

export default Button;
  1. During the initial render
  • 리액트는 DOM node에 1개의 section 2개의 button 태그를 브라우저에 생성합니다.
  1. During the re-render
  • 리액트는 이전 렌더링 이후 변경된 프로퍼티가 있다면 어떤 것이 있는지 계산 후 다음 단계인 커밋 단계가 될 때까지 해당 정보로 아무 작업도 하지 않습니다.

React commits changes to the DOM

컴포넌트를 렌더링 후 React는 DOM을 수정합니다.

초기 렌더링의 경우 React는 appendChild() DOM API를 통해 생성한 모든 DOM 노드를 화면에 배치 합니다.
리 렌더링이 발생 시 React는 필요한 최소한의 연산을 적용(diffing 알고리즘)하여 DOM이 최신 렌더링 출력과 일치하도록 할 것입니다.

리액트는 렌더링 간 차이가 존재할 때 DOM 노드를 변경합니다.

아까 Render phase에서 3번 컴포넌트가 변경되어야 한다고 React에서 계산이 되면 Render 함수를 통해 실제 DOM으로 반영이 되는데 이를, Commit Phase(변경 사항을 실제 DOM에 적용하는 단계) 라고 합니다.

예시

// Button.tsx
import { useState } from "react";
function OtherButton() {
  return <button>my name is other button!</button>;
}
function Button() {
  const [count, setCount] = useState(0);
  return (
    <section>
      <button onClick={() => setCount((prev) => (prev = prev + 1))}>
        {count}
      </button>
      <OtherButton />
    </section>
  );
}

export default Button;

초기 렌더링 시 버튼에 있는 count 값은 0입니다.

commit phase를 통해 실제 변경사항이 DOM에 반영되어 count가 1 증가 된 값을 브라우저 에서 확인할 수 있다.

번외 - Brower Paint

렌더링이 완료되고 React가 DOM을 업데이트 시 브라우저는 화면을 다시 paint 합니다. 이를 "브라우저 렌더링"이라고 합니다.

레퍼런스

https://velog.io/@hyunjine/Thinking-in-React

https://react.dev/learn/render-and-commit

https://github.com/facebook/react/blob/v16.12.0/packages/react-reconciler/src/ReactFiberRoot.js#L104

https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guide-to-react-rendering-behavior/

원티드 프리온보딩 4월 수업

1개의 댓글

comment-user-thumbnail
2023년 4월 11일

원티드 FE 강의듣다 찾아와봣습니다 너무 깔끔하네요감사합니다.

답글 달기