화면 업데이트의 비밀 - 렌더링과 커밋 과정 파헤치기
리액트 공식문서 기반의 리액트 입문기
들어가며
지난 3-2편에서는 useState 훅을 통해 컴포넌트에 "기억력"을 부여하는 방법을 알아보았습니다. 마치 프레젠테이션을 준비하는 각 담당자가 '개인적인 기록 노트'를 가지게 된 것과 같습니다. 그렇다면 이 노트의 내용이 바뀌었을 때, 혹은 다른 팀원들과의 소통(Props, Context)을 통해 새로운 정보가 들어왔을 때, 눈앞의 화이트보드(UI)는 어떻게 업데이트되는 걸까요? 자바스크립트에서는 우리가 직접 화이트보드를 지우고 다시 그렸던 경험을 떠올려보면, 리액트에서는 어떤 방식으로 이러한 UI 업데이트가 일어나는지 궁금해지는 것이 당연합니다. 이번 편에서는 리액트가 상태 변화에 따라 UI를 어떻게 업데이트하는지, 그 "화면 업데이트의 비밀"인 렌더링과 커밋 과정을 "역동적인 화이트보드 프레젠테이션"이라는 비유를 통해 파헤쳐 보겠습니다.
리액트 렌더링 과정의 세 단계
우리는 3-2편에서 useState를 통해 컴포넌트에 상태를 부여하고, 그 상태가 변경될 때 컴포넌트 함수가 다시 호출된다는 것을 배웠습니다. 하지만 컴포넌트 함수가 다시 호출된다는 것이 곧바로 브라우저 화면에 UI가 업데이트된다는 것을 의미하지는 않습니다. 리액트에는 이러한 상태 변화를 실제 DOM에 반영하기 위한 특별한 과정이 존재합니다. 바로 **렌더링(Render)**과 **커밋(Commit)**이라는 두 단계입니다. 이 과정을 좀 더 자세히 세 단계로 나누어 살펴보겠습니다.
1. 렌더링 트리거 (When to Render?)
리액트가 컴포넌트를 다시 렌더링해야겠다고 판단하는 시점을 렌더링 트리거라고 합니다. 주로 다음과 같은 경우에 렌더링이 트리거됩니다.
- 최초 마운트 시 (Initial Render): 애플리케이션이 처음 로드될 때, 루트 컴포넌트와 그 자식 컴포넌트들이 최초로 렌더링됩니다.
- State 변경: 컴포넌트 내에서
useState의set함수가 호출되어 상태(State)가 변경될 때 렌더링이 트리거됩니다. 이때 중요한 점은set함수 호출이 상태를 즉시 변경하는 것이 아니라, 다음 렌더링을 위해 상태의 새로운 '스냅샷' 생성을 예약한다는 것입니다. 이는 해당 컴포넌트와 그 자식 컴포넌트들의 리렌더링으로 이어집니다. (이 '스냅샷' 개념은 3-4편에서 더 깊이 다룹니다.) - Props 변경: 부모 컴포넌트가 리렌더링되면서 자식 컴포넌트에 전달하는
props가 변경될 때, 자식 컴포넌트의 렌더링이 트리거됩니다. - Context 값 변경:
Context를 사용하는(구독하는) 컴포넌트의 경우,Context의 값이 변경되면 해당 컴포넌트와 그 자식 컴포넌트들의 렌더링이 트리거됩니다.
이러한 트리거들이 발생하면, 리액트는 해당 컴포넌트와 관련 자식 컴포넌트들을 "다시 그려야 한다"는 신호를 받게 됩니다. 이를 "프레젠테이션 업데이트 요청"이라는 비유로 설명해볼 수 있습니다.
useState(개인적인 기록 노트): 각 발표 담당자(컴포넌트)가 가지고 있는 '개인적인 기록 노트'의 내용이 변경되었을 때, 마치 담당자가 자기 노트 내용을 수정하고 "내용이 바뀌었으니 화이트보드를 업데이트해야 해요!"라고 알리는 것과 같습니다. 예를 들어, 카운터 컴포넌트의count값이 변경되는 것이죠.props(구체적인 지시사항이 담긴 메모): 상위 담당자(부모 컴포넌트)가 하위 담당자(자식 컴포넌트)에게 전달하는 '구체적인 지시사항이 담긴 메모'의 내용이 바뀌었을 때 발생합니다. "제목 담당자가 버튼 담당자에게 버튼 색깔을 빨강으로 바꿔달라는 메모를 새로 줬어요!"라고 알리는 것과 같습니다.Context(공통 참조 게시판): 모든 담당자가 공유하는 '공통 참조 게시판'의 내용이 변경되었을 때 발생합니다. "오늘의 행사 테마가 바뀌었으니 모두 화이트보드를 업데이트해야 해요!"라고 알리는 것과 같습니다.
2. 컴포넌트 렌더링 (What to Render?)
렌더링 트리거에 의해 리액트는 업데이트가 필요한 컴포넌트 함수를 다시 호출합니다. 이 과정이 바로 컴포넌트 렌더링 단계입니다. 이때 다음과 같은 일들이 일어납니다.
- 함수 호출 및 JSX 반환: 컴포넌트 함수가 실행되고, 현재
state와props를 기반으로 새로운 JSX가 반환됩니다. - Virtual DOM 생성: 리액트는 반환된 JSX를 실제 DOM 엘리먼트가 아닌 "리액트 엘리먼트"라는 가벼운 자바스크립트 객체 형태로 변환합니다. 이 리액트 엘리먼트들로 구성된 트리를 **Virtual DOM(가상 DOM)**이라고 부릅니다. Virtual DOM은 실제 브라우저의 DOM과 동일한 계층 구조를 가지고 있지만, 메모리에 존재하며 직접적인 조작 비용이 훨씬 저렴하다는 특징이 있습니다.
이 단계에서는 아직 실제 브라우저 화면에는 아무런 변화도 일어나지 않습니다. 리액트는 단순히 "다음 화면은 이렇게 생길 것이다"라는 청사진을 메모리에 그려놓는 것에 불과합니다. 이 Virtual DOM은 이후 실제 DOM과 비교하는 데 사용될 것입니다. 이를 **"담당자가 새로운 그림을 머릿속으로 구상"**하는 것에 비유할 수 있습니다. 발표 업데이트 요청을 받은 각 담당자(컴포넌트)들이 자신의 '개인적인 기록 노트'(useState), '지시사항 메모'(props), '공통 참조 게시판'(Context) 내용을 참고하여, 기존 화이트보드 그림과 무엇이 달라져야 할지 머릿속으로 새로운 그림을 계획하는 것입니다. 이때 실제 화이트보드(실제 DOM)에 그리는 것이 아니라, 머릿속으로만 '새로운 그림의 스케치'(Virtual DOM)를 만드는 단계입니다.
3. DOM에 커밋 (How to Commit?)
렌더링 단계를 통해 새로운 Virtual DOM 트리가 생성되면, 리액트는 이제 실제 브라우저 화면을 업데이트할 차례라고 판단합니다. 이 과정을 커밋(Commit) 단계라고 합니다. 커밋 단계에서는 두 가지 주요 작업이 이루어집니다.
- 재조정 (Reconciliation) - Diffing 알고리즘: 리액트는 새로 생성된 Virtual DOM 트리와 이전에 존재했던 Virtual DOM 트리를 효율적으로 비교합니다. 이 비교 과정을 **재조정(Reconciliation)**이라고 하며, 이때 Diffing 알고리즘이라는 최적화된 방법론을 사용합니다. Diffing 알고리즘은 두 트리 간의 차이점을 최소화하는 방식으로 변경 사항(어떤 엘리먼트가 추가되었는지, 삭제되었는지, 속성이나 텍스트 내용이 변경되었는지 등)을 찾아냅니다.
- 실제 DOM 업데이트: 재조정 과정을 통해 파악된 최소한의 변경 사항만을 실제 브라우저의 DOM에 적용합니다. 예를 들어, 텍스트 내용만 변경되었다면 해당
TextNode만 업데이트하고, 새로운 엘리먼트가 추가되었다면 해당 엘리먼트만 DOM에 삽입하는 식입니다. 이를 통해 불필요한 DOM 조작을 최소화하여 브라우저의 렌더링 성능을 크게 향상시킵니다.
이러한 과정을 통해 리액트는 개발자가 직접 DOM을 조작하는 부담 없이, 선언적으로 상태를 관리하고 효율적으로 UI를 업데이트할 수 있도록 돕습니다. 우리는 단지 "무엇을 보여줄지"만 선언하면, 리액트가 "어떻게" 효율적으로 화면을 업데이트할지 결정하고 실행하는 것입니다.
마지막으로, DOM에 커밋 과정은 **"기존 그림과 스케치를 비교하여 최소한의 수정 사항만 반영"**하는 것에 비유할 수 있습니다. 각 담당자(컴포넌트)들이 머릿속으로 구상한 '새로운 그림 스케치'(Virtual DOM)와 현재 화이트보드(실제 DOM)에 그려진 '기존 그림'을 비교하여, 실제 화이트보드에 어떤 부분을 수정해야 할지(재조정/Diffing 알고리즘) 최소한의 차이점만을 찾아냅니다. 예를 들어, 카운터 숫자가 1만 증가했다면, 화이트보드 전체를 새로 그리는 것이 아니라 숫자 부분만 지우고 새로 쓰는 것과 같습니다. 이렇게 찾아낸 최소한의 변경 사항만 실제 화이트보드(실제 DOM)에 반영하여 최종 그림을 완성하고 프레젠테이션을 이어나가는 것입니다.
리액트 카운터 예제를 통해 렌더링 과정 이해하기
위에서 살펴본 리액트 렌더링의 세 단계를 간단한 카운터 컴포넌트 예제를 통해 다시 한번 확인해 보겠습니다. 이 코드를 살펴보면서 각 단계가 어떻게 작동하는지 집중해주세요.
import { useState } from 'react';
import ReactDOM from 'react-dom/client';
function Counter() {
// 1. 상태 선언: `count`와 `setCount`는 컴포넌트의 상태와 이를 업데이트하는 함수입니다.
const [count, setCount] = useState(0);
// 렌더링 시점 확인을 위한 console.log
console.log('Counter 컴포넌트 렌더링됨. 현재 count:', count);
const handleClick = () => {
// 렌더링 트리거: setCount 호출로 인해 상태가 변경되고, 리액트에게 다시 렌더링할 것을 알립니다.
setCount(count + 1);
console.log('상태 업데이트 요청됨. 다음 렌더링 시 count는', count + 1, '이 됩니다.');
};
return (
<div>
<p>
카운트: <span>{count}</span>
</p>
<button onClick={handleClick}>증가</button>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
// 최초 마운트 시 렌더링 트리거가 발생하고, Counter 컴포넌트가 렌더링되어 DOM에 커밋됩니다.
root.render(<Counter />);위 코드에서 setCount(count + 1)가 호출될 때마다 렌더링이 트리거됩니다. 이는 마치 담당자의 '개인적인 기록 노트'(useState) 내용이 바뀌어 **"프레젠테이션 업데이트 요청"**이 들어온 것과 같습니다. 이 요청에 따라 Counter 컴포넌트 함수가 다시 실행되면서 (console.log("Counter 컴포넌트 렌더링됨...") 출력 확인), 현재 count 값을 반영한 새로운 "그림 스케치"(Virtual DOM)를 머릿속으로 구상합니다. 마지막으로 리액트는 이 새로운 "그림 스케치"와 현재 화이트보드에 그려진 그림을 비교하여, 변경된 <span> 태그의 텍스트 부분만 실제 화이트보드에 효율적으로 반영하는 것을 확인할 수 있습니다.
리액트 렌더링 과정의 특징
리액트의 렌더링 과정은 다음과 같은 주요 특징을 가집니다.
- 1. 선언형(Declarative) UI와 개발자의 역할 변화: 리액트의 핵심은 선언형 UI입니다. 개발자는 특정 상태일 때 UI가 "어떻게 보일지"만 선언합니다. 즉, UI의 최종 모습(
JSX)을 기술하는 데 집중하고, "언제, 어떻게 DOM을 조작할지"에 대한 구체적인 과정은 리액트가 Virtual DOM과 재조정(Reconciliation) 알고리즘을 통해 알아서 처리합니다. 이러한 역할 분담은 개발자가 복잡한 DOM 조작에 대한 부담을 덜고, 애플리케이션의 핵심 비즈니스 로직과 UI의 논리적 구조에 더 집중할 수 있게 하여 개발 생산성과 코드의 가독성을 크게 향상시킵니다. - 2. Virtual DOM과 재조정을 통한 성능 최적화: 리액트가 상태 변화를 감지하면, 새로운 Virtual DOM 트리를 생성하고 이전에 존재했던 Virtual DOM 트리와 효율적으로 비교합니다. 이 비교 과정(
Diffing 알고리즘)을 통해 실제 DOM에서 변경이 필요한 최소한의 부분만을 찾아냅니다. 결과적으로 실제 DOM 조작을 최소화하여 브라우저의 부담을 줄이고 애플리케이션의 렌더링 성능을 향상시키는 핵심적인 메커니즘을 제공합니다. 이는 특히 복잡한 UI나 빈번한 업데이트가 발생하는 상황에서 빛을 발합니다. - 3. 예측 가능한 상태 기반 렌더링: 리액트의 렌더링 과정은 상태(State)와 Props에 전적으로 의존합니다. 즉, 특정 상태와 Props가 주어졌을 때 어떤 UI가 그려질지 항상 예측할 수 있습니다. 이러한 예측 가능성은 애플리케이션의 동작을 쉽게 추론하고, 복잡한 애플리케이션에서도 디버깅을 용이하게 하며 코드의 안정성을 높이는 데 결정적인 역할을 합니다. 개발자는 상태를 변경하는 것만으로 원하는 UI 변화를 얻을 수 있기에, UI 흐름에 대한 제어력을 확보하게 됩니다.
- 4. 효율적인 업데이트 스케줄링:
setState호출 시 모든 컴포넌트가 무조건 리렌더링되는 것이 아니라, 리액트는 내부적으로 스케줄링(Scheduling, 언제 어떤 순서로 UI 업데이트를 처리할지 결정하는 과정) 과정을 통해 변경된 부분만 효율적으로 찾아내어 업데이트합니다. 또한, 여러setState호출을 배치(Batching) 처리하여 한 번의 리렌더링으로 묶어 불필요한 연산을 줄입니다. 이는 마치 여러 개의 작은 그림 수정 요청이 들어와도 한 번에 모아서 화이트보드에 반영함으로써, 발표의 흐름을 끊지 않고 효율적으로 진행하는 것과 유사합니다. 이러한 최적화된 업데이트 전략은 리액트 애플리케이션이 복잡해질수록 더욱 중요한 성능 이점으로 작용합니다. (배치 처리는 3-5편에서 더 자세히 다룹니다.)
자바스크립트 vs 리액트 차이
| 구분 | 자바스크립트 | 리액트 |
|---|---|---|
| 업데이트 트리거 | 개발자가 DOM API 호출로 즉시 변경 | State/Props/Context 변경 시 렌더링 스케줄 |
| 화면 구성 | 실제 DOM을 직접 조작 | JSX → Virtual DOM 생성 후 Diffing으로 최소 변경만 커밋 |
| 성능 전략 | 개발자가 수동 최적화 필요 | 재조정(Reconciliation)과 배치 처리로 불필요한 작업 최소화 |
| 동기화 책임 | 상태와 UI 일치를 개발자가 직접 보장 | 상태 기반 선언형 렌더링으로 리액트가 일관성/동기화 책임 담당 |
요약
이번 3-3편에서는 "역동적인 화이트보드 프레젠테이션"이라는 비유를 통해 리액트의 핵심 동작 방식인 렌더링과 커밋 과정을 탐구했습니다. useState, props, Context와 같은 리액트의 주요 개념들이 어떻게 UI 업데이트를 촉발하고, 리액트가 이를 얼마나 효율적으로 처리하는지 단계별로 살펴보았습니다. 공부한 것을 정리해보면 다음과 같습니다.
- 렌더링 트리거 (프레젠테이션 업데이트 요청): 담당자의 '개인적인 기록 노트'(
State) 변경, 상위 담당자의 '지시사항 메모'(Props) 전달, '공통 참조 게시판'(Context) 내용 변경, 또는 최초 발표 시작 시점에 발생하는 "화이트보드를 업데이트해야 한다"는 요청입니다. - 컴포넌트 렌더링 (새로운 그림 구상): 업데이트 요청을 받은 담당자(컴포넌트)들이 자신의 정보들을 바탕으로 머릿속으로 새로운 그림의 스케치(
Virtual DOM)를 계획하는 단계입니다. 아직 실제 화이트보드에 그리지 않고, 어떤 모습으로 바뀔지 상상하는 것입니다. - DOM에 커밋 (최소한의 수정 반영): 머릿속으로 구상한 새로운 그림 스케치(
Virtual DOM)와 현재 화이트보드의 그림을재조정(Reconciliation)알고리즘과Diffing 알고리즘으로 비교하여, 실제 화이트보드에 필요한 최소한의 변경 사항만(DOM업데이트) 반영하는 최종 단계입니다. - Virtual DOM (그림 스케치): 실제 화이트보드(실제
DOM)의 가벼운 복사본으로, 리액트가 UI 변경 사항을 효율적으로 비교하고 최소한의 수정만으로 화이트보드를 업데이트할 수 있도록 돕는 핵심적인 개념입니다.
다음 편(3-4편)에서는 상태 값이 "스냅샷"처럼 동작한다는 관점에서 왜 setState 직후 콘솔에 이전 값이 보이는지, 그리고 그 의미를 자세히 이어서 설명하겠습니다.