React - 렌더링 그리고 커밋

Sally·2026년 2월 24일

컴포넌트를 화면에 표시하기 이전에 React에서 렌더링하는 과정이 필요하다.
코드가 어떻게 실행되는지, React에서는 렌더링 과정이 어떻게 진행되는지에 대해 정리해보고자 한다.

주방 상황에서 요리사가 컴포넌트를 재료로 요리를 한는 상황이라면

  • React : 고객들의 요청을 받고 주문을 가져오는 웨이터

UI를 요청하고 제공하는 세 가지 단계
1. 렌더링 트리거 (손님의 주문을 주방으로 전달)
2. 컴포넌트 렌더링 (주방에서 주문 준비하기)
3. DOM에 커밋 (테이블에 주문한 요리 내놓기)

여기에서 DOM이란 브라우저가 HTML 코드를 이해하고 화면에 그리기 위해 생성하는 설계도 같은 것이라고 이해하면 된다. 리액트는 실제 DOM 대신 메모리에 가벼운 가상 DOM(Virtual DOM)을 만들어 먼저 시뮬레이션하며, 데이터가 변하면 바뀐 부분만 골라내어 실제 DOM에 반영하기 때문에 웹 속도가 빠르고 효율적인 것이다.

1단계: 렌더링 트리거

컴포넌트는 다음 두 가지 경우에 렌더링된다.
1. 컴포넌트의 초기 렌더링
2. 컴포넌트의 state가 업데이트 된 경우

초기 렌더링과 createRoot의 역할

  1. 초기 렌더링의 트리거: 앱을 처음 시작할 때 화면을 그리는 과정을 시작하려면 '렌더링 트리거'가 필요한데, 리액트에서는 createRoot를 호출하고 render 메서드를 실행하는 것으로 이 작업이 완료된다.
  • 우리가 사용하는 Next.js 같은 프레임워크나 온라인 샌드박스 도구들은 이 createRoot 코드를 내부적으로 숨겨두기도 하지만, 모든 리액트 앱의 밑바닥에는 항상 이 과정이 존재합니다.
  1. 실제 DOM과의 연결고리: createRoot는 브라우저의 실제 DOM 노드(주로 id="root")를 인자로 받아, 리액트 컴포넌트가 뿌리 내릴 수 있는 최상위 진입점을 만든다.

추가로, 이 방식을 통해 리액트 앱이 실행되어야만 Concurrent Mode(동시성 모드)와 같은 최신 성능 최적화 기능을 온전히 사용할 수 있다고 한다.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";

import { AuthProvider } from '@/contexts/AuthContext';

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <AuthProvider> 
      <App />
    </AuthProvider>
  </StrictMode>
);

진행하는 프로젝트의 진입점 main.tsx 코드이다. 여기서 createRoot를 확인할 수 있고, render 메서드를 통해 브라우저의 실제 DOM 요소인 root와 리액트의 가상 DOM 세상을 연결하여 초기 렌더링을 트리거한다.

  • *AuthProvider : <App />을 감싸 앱 전체에 인증 관련 전역 데이터를 공급하는 통로 역할을 수행
  • StrictMode : 컴포넌트에서 실수 찾을 수 있음

📍연결과 렌더링 : createRoot + render (리액트라는 엔진을 브라우저에 장착)

State 업데이트 시 리렌더링

  • 컴포넌트가 처음으로 렌더링 된 후에는 set 함수를 통해 상태를 업데이트하여 추가적인 렌더링을 트리거 할 수 있다.
    • 컴포넌트의 상태를 업데이트하면 자동으로 렌더링 대기열에 추가된다.
  • set 함수 : 리액트에서 상태를 만들 때 사용하는 const [state, setState] = useState(초기값); 문법에서 setState가 set함수

2단계: React 컴포넌트 렌더링

  • 렌더링을 트리거 한 이후, React는 컴포넌트를 호출하여 화면에 표시할 내용을 파악한다.

📍 렌더링 이란 : React에서 컴포넌트를 호출하는 것을 의미

  • 초기 렌더링 : React는 루트 컴포넌트를 호출
  • 이후 렌더링 : React는 state 업데이트가 일어나 렌더링을 트리거한 컴포넌트를 호출
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"
    />
  );
}

리액트의 재귀적 렌더링 단계
1. 시작점(Parent) : 리액트가 Gallery() 컴포넌트를 호출 -> 컴포넌트는 <section>과 그 안에 담긴 <h1> , 그리고 세 개의 <Image/> 컴포넌트를 반환

  • 초기 렌더링 하는 동안 React는 <section>, <h1> 그리고 3개의 <img> 태그에 대한 DOM 노드를 생성함
  1. 재귀적 탐색 (Recursive Step) : 리액트는 여기서 멈추지 않고, 반환된 결과물 속에 있는 자식 컴포넌트인 <image/>를 찾아가 다시 호출
  • 첫 번째 <Image/>를 렌더링하여 <img> 태그를 얻음
  • 두 세번째 <Image/>도 호출하여 더 이상 컴포넌트 형태가 아닌 순수한 HTML 태그 (<img>)가 나올때까지 반복
  1. 최종 단계 : 중첩된 컴포넌트가 더 이상 없고, 리액트가 화면에 표시해야 할 브라우저 태그(h1, section, img 등)를 정확히 알게 될 때까지 재귀 계속

3단계 : React가 DOM에 변경사항을 커밋

  • 컴포넌트를 렌더링(호출)한 후 React는 DOM을 수정한다.
    • 초기 렌더링 : React는 appendChild() DOM API를 사용하여 생성한 모든 DOM 노드를 화면에 표시한다.
      • appendChild() 는 리액트가 내부적으로 몰래 실행하는 동작으로, 개발자가 직접 appendChild()를 호출할 필요 없이, createRoot와 render 과정을 거치면 리액트가 내부적으로 가장 최적화된 시점에 이 API를 사용하여 화면을 업데이트한다.
    • 리렌더링 : React는 필요한 최소한의 작업 (렌더링하는 동안 계산된 것)을 적용하여 DOM이 최신 렌더링 출력과 일치하도록 한다.
  • React는 렌더링 간에 차이가 있는 경우에만 DOM 노드를 변경한다.
export default function Clock({ time }) {
  return (
    <>
      <h1>{time}</h1>
      <input />
    </>
  );
}
  • 위의 Clock 컴포넌트 코드를 보면, time이라는 값이 바뀔 때마다 리액트는 화면을 다시 그리게 되는데, 이때 리액트는 아래와 같이 작동한다.
  1. 차이점 분석 (Diffing): 리액트는 이전의 결과물과 지금 새로 만든 결과물을 비교한다.
  2. 필요한 부분만 수정: * <h1> 태그 안의 {time}은 계속 변하므로, 리액트는 이 부분의 텍스트 내용만 브라우저 DOM에서 업데이트한다.
  • 이 때, <input /> 태그는 이전과 똑같은 위치에 그대로 존재한다.
  1. 상태 보존: 리액트는 <input /> 이나 그 안에 사용자가 입력 중이던 value는 건드리지 않고 그대로 둔다.

덕분에 사용자가 <input>을 치고 있는 와중에 time이 바뀌더라도, 입력창의 포커스나 글자가 사라지지 않고 자연스럽게 유지될 수 있다.

추가 ) 브라우저 페인팅

  • 리액트가 DOM 업데이트를 마치고 나면 브라우저는 화면을 다시 그린다. 이 단계를 브라우저 렌더링이라고 하지만, 나머지 부분과 혼동을 피하고자 리액트 공식문서에서는 이를 브라우저 페인팅 이라고 부른다.

0개의 댓글