본문 바로가기
Develop/React

Render와 Commit

by 지오는 짱짱걸 입니다 2023. 7. 20.
반응형

여러분이 만든 컴포넌트들이 화면에 보이기 전에 컴포넌트들은 React에 의해 렌더링이 되어야 합니다. 이러한 프로세스를 익힌다면, 우리는 코드가 어떻게 실행되는지와 그 이면에 대해 이해할 수 있을 것입니다.

뜬금없지만 지금부터는 컴포넌트(Component)가 부엌에서 여러 재료들을 조립해서 맛있는 요리를 만드는 요리사라고 생각해봅시다. 이 시나리오에서는 React는 고객의 요청을 접수하고 전달해주는 종업원이 됩니다. UI를 요청하고 제공하는 이 프로세스에는 다음과 같은 세 가지 단계가 존재합니다.

  1. Triggering a render(손님의 주문을 부엌으로 전달)
  2. Render the component(부엌에서 주문을 준비)
  3. Committing to the DOM(테이블에 주문한 음식을 놓기)

첫 번째 단계: Trigger a render
컴포넌트가 렌더링 되는 이유 두 가지는 다음과 같습니다.

  1. 컴포넌트의 초기 렌더링(Initial render).
  2. 컴포넌트 혹은 그 컴포넌트의 상위 컴포넌트 중 하나 이상의 state가 변경됨(Re-renders when state updates).

Initial render
우리의 애플리케이션이 시작될 때, initial render를 위한 trigger가 필요합니다. 다시 말하면 createRoot를 target DOM node와 함께 호출한 후 그것의 render 메서드를 우리의 컴포넌트와 함께 호출하여 수행합니다. 좀 어렵게 느껴진다면, 아래의 코드를 보면 아~ 하고 바로 이해가 되실 겁니다.

import Image from './Image.js';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'))
root.render(<Image />);

Re-render when state updates
컴포넌트가 이미 initial render 되었다면, 우리는 state를 업데이트 하기 위한 set 함수를 사용해서 render를 trigger 할 수 있습니다. 우리 컴포넌트의 state를 업데이트하면 렌더링이 자동으로 queue(대기열)에 추가됩니다.

두 번째 단계: React renders your components
렌더링을 trigger 한 후 React는 컴포넌트를 호출하여 화면에 표시할 내용을 파악합니다. 렌더링은 우리의 컴포넌트를 호출하는 React입니다.

  • Initial render 에서는 React가 root 컴포넌트를 호출합니다.
  • 이후의 render 에서는 state를 업데이트하여 렌더링을 trigger한 함수 컴포넌트를 호출합니다.

이 프로세스는 재귀적입니다. 만약 업데이트된 컴포넌트가 다른 컴포넌트를 반환한다면, React는 다음으로 반환된 컴포넌트를 렌더링 합니다. 그리고 만약 그 컴포넌트 또한 다른 컴포넌트를 반환한다면, React는 반환된 컴포넌트를 렌더링 합니다. 이러한 프로세스는 중첩된 컴포넌트가 존재하지 않을 때까지 진행됩니다. 그리고 React는 이 과정속에서 화면에 표시 되어야 할 것들을 정확히 알고 있습니다.

아래 코드에서 React는 Gallery()및 Image()를 여러 번 호출합니다.
Gallery.js

export default function Gallery() {
  return (
    <section>
      <h1>Inspiring Sculptures</h1>
      <Image />
      <Image />
      <Image />
    </section>
  );
}

function Image() {
  return (
    <img
      src="https://i.imgur.com/ZF6s192.jpg"
      alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
    />
  );
}

index.js

import Gallery from './Gallery.js';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'))
root.render(<Gallery />);
  • Initial 렌더링 중 React는 DOM node인 <section>, <h1>, <img> 태그들을 생성합니다.
  • 리렌더링 중 React는 변경된 속성(있는 경우)을 계산합니다. 다음 단계인 commit 단계까지 해당 정보로 아무 작업도 수행하지 않습니다.

Optimizing performance
위에서 알아본 것 처럼 React는 특정 컴포넌트가 업데이트되면 그 컴포넌트와 그 컴포넌트의 자식 컴포넌트들을 모두 다시 렌더링합니다. 이는 기본적인 동작이며, 이렇게 되면 상태가 바뀐 부모 컴포넌트와 그의 자식 컴포넌트들이 모두 최신의 상태를 반영하여 보여줄 수 있게 됩니다.
하지만 이러한 방식은 성능에 문제를 일으킬 수 있습니다. 특히, 업데이트가 이루어진 컴포넌트가 컴포넌트 트리에서 매우 높은 위치에 있고 그 아래 많은 자식 컴포넌트들이 있다면, 이 모든 컴포넌트들이 다시 렌더링되어야 하므로 상당히 많은 자원을 소모하게 됩니다.
이러한 성능 문제를 해결하기 위해서는 React의 성능 최적화 기법을 사용할 수 있습니다. 예를 들어, shouldComponentUpdate, PureComponent, React.memo 등의 방법을 사용하여 불필요한 렌더링을 방지하고 성능을 향상시킬 수 있습니다.
그러나 성능 문제가 실제로 발생했을 때만 최적화를 고려하는 것이 좋습니다. 이는 미리 최적화를 하려다 보면 불필요한 복잡성을 코드에 추가할 수 있기 때문입니다(리팩터링의 저자가 성능을 포기하더라도 읽기 쉬운 코드가 좋은 코드라고 한 것과 비슷한 의미 같아요).

세 번째 단계: React commits changes to the DOM
우리의 컴포넌트가 렌더링(컴포넌트 호출)을 한 이후에 React는 DOM을 수정하려고 합니다.

  • Initial render의 경우 React는 appendChild() DOM API를 사용해서 생성한 모든 DOM node를 화면에 표시합니다.
  • Re-renders의 경우 React는 DOM이 최신 렌더링 출력과 일치하도록 최소한의 필수 작업(렌더링 중에 계산됨)을 적용합니다.

React는 렌더링간에 차이가 있는 경우에만 DOM node를 변경합니다. 아래의 코드를 통해 예를 들어보겠습니다. 매초마다 부모 컴포넌트로부터 전달된 다른 props 로 리렌더링하는 컴포넌트 입니다. <input> 태그에 일부 텍스트를 추가하여 해당 value를 업데이트할 수 있지만 컴포넌트가 다시 렌더링될 때 텍스트가 사라지지 않습니다.

export default function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}


이러한 동작은 마지막 단계에서 React가 <h1> 태그의 내용만 새로운 time 으로 업데이트하기 때문입니다. <input> 태그가 지난번과 같은 위치에서 JSX에 나타나는 것을 확인하므로 React는 <input> 태그 또는 그 value 를 건드리지 않습니다.

참고
React는 효율적인 렌더링을 위해 가상 DOM(Virtual DOM)을 사용하며, 실제 DOM에 반영하는 과정에서 변경된 부분만 업데이트합니다. 이를 DOM의 diffing 또는 reconciliation 과정이라고 합니다.
이에 대해 좀 더 구체적으로 설명하면, 부모 컴포넌트의 상태가 변경되어 리렌더링이 발생하더라도, 자식 컴포넌트의 props나 state가 변경되지 않았다면, 자식 컴포넌트의 렌더 결과는 이전과 동일하게 됩니다. 이 경우, React의 diffing 알고리즘은 자식 컴포넌트의 DOM이 변경되지 않았음을 감지하고, 실제 DOM에는 아무런 변경사항을 반영하지 않습니다. 이 과정을 통해 React는 불필요한 DOM 업데이트를 방지하고 성능을 최적화합니다.
하지만 여기서 주의해야 할 점은, 실제 DOM 변경은 최적화되지만, React 컴포넌트의 렌더링 과정 자체는 여전히 발생합니다. 즉, 부모 컴포넌트가 리렌더링되면 자식 컴포넌트도 렌더링 과정을 거치게 됩니다. 이 과정은 자원을 사용하므로, 컴포넌트의 렌더링이 불필요하게 자주 발생하는 것을 방지하기 위해 shouldComponentUpdate, PureComponent, React.memo와 같은 기법들을 사용할 수 있습니다.

정리

  • React 애플리케이션은 다음의 세 단계화면을 업데이트 합니다.
  1. Trigger
  2. Render
  3. Commit
  • 우리는 Strict Mode를 사용해서 우리의 컴포넌트에 대한 실수를 찾을 수 있습니다.
  • React는 렌더링 결과가 이전과 같으면 DOM 을 업데이트하지 않습니다.

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

반응형