컴포넌트가 여러분의 화면에 표시되기 전에, React는 반드시 컴포넌트를 렌더링(Render) 해야 합니다. 이 과정이 어떤 단계로 이루어지는지 이해하게 되면, 여러분이 작성한 코드가 어떻게 실행되는지 사고하는 흐름을 잡을 수 있고, 예상치 못한 동작이 발생했을 때 그 원인을 쉽게 파악할 수 있게 됩니다.
여러분이 만든 컴포넌트들을 주방에서 재료를 모아 맛있는 요리를 조리하는 요리사라고 상상해 보세요. 이 시나리오에서 React는 고객의 주문을 받고 요리를 서빙하는 웨이터 역할을 합니다. UI를 요청하고 제공하는 이 과정은 다음 세 가지 단계로 이루어집니다.
👨🏫 강사의 보충 설명:
React가 왜 이런 방식을 취할까요? 바로 '효율성' 때문입니다. 매번 화면을 직접 지우고 새로 그리는 대신, 가상의 공간(Virtual DOM)에서 먼저 변경 사항을 요리(계산)해 본 다음, 정말 필요한 부분만 실제 화면(DOM)에 반영(서빙)하기 위해서죠!

트리거 (Trigger)

렌더링 (Render)

커밋 (Commit)
컴포넌트가 렌더링을 시작하는 데에는 두 가지 이유가 있습니다.
여러분의 앱이 처음 시작될 때, 초기 렌더링을 트리거(발생)해야 합니다. 프레임워크(Next.js 등)나 샌드박스 환경에서는 종종 이 코드가 숨겨져 있기도 하지만, 기본적으로 대상이 되는 DOM 노드를 가지고 createRoot를 호출한 다음, 여러분의 컴포넌트와 함께 render 메서드를 호출하는 방식으로 이루어집니다.
// src/index.js
import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Image />);
// src/Image.js
export default function Image() {
return (
<img
src="[https://i.imgur.com/ZF6s192.jpg](https://i.imgur.com/ZF6s192.jpg)"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
한번 여러분의 코드에서 root.render() 호출 부분을 주석 처리해 보세요. 화면에서 컴포넌트가 싹 사라지는 것을 볼 수 있을 거예요! 즉, 이 코드가 앱의 시작점 역할을 합니다.
컴포넌트가 초기에 렌더링되고 나면, set 함수를 통해 상태(state)를 업데이트하여 추가적인 렌더링(리렌더링)을 트리거할 수 있습니다. 컴포넌트의 상태를 업데이트하면 렌더링이 자동으로 대기열(queue)에 추가됩니다.
(식당 손님이 처음 주문을 한 이후에, 목이 마르거나 배가 고파지는 상태 변화에 따라 차, 디저트, 기타 여러 가지를 추가로 주문하는 것과 같다고 상상해 보세요!)

상태 업데이트...

...트리거...

...렌더링!
여러분이 렌더링을 트리거하고 나면, React는 화면에 무엇을 표시해야 할지 알아내기 위해 여러분의 컴포넌트들을 호출합니다. 즉, "렌더링"이란 React가 여러분의 컴포넌트(함수)를 호출하는 것을 의미합니다.
이 과정은 재귀적(recursive)으로 일어납니다. 업데이트된 컴포넌트가 내부에서 다른 컴포넌트를 반환하면, React는 다음으로 그 컴포넌트를 렌더링하고, 그 컴포넌트가 또 다른 것을 반환하면 그다음 컴포넌트를 렌더링하는 식입니다. 더 이상 중첩된 컴포넌트가 없고, 화면에 표시되어야 할 내용을 React가 정확히 알게 될 때까지 이 과정은 계속됩니다.
👨🏫 강사의 보충 설명:
이 단계에서 React는 실제 브라우저 화면(DOM)을 건드리지 않아요! 오직 메모리 상에서 자신이 만든 가상의 구조(Virtual DOM)만을 그려보며 어떤 변화가 필요한지 '계획'하는 단계랍니다.
다음 예시에서 React는 Gallery() 함수와 Image() 함수를 여러 번 호출하게 됩니다.
// src/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](https://i.imgur.com/ZF6s192.jpg)"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
// src/index.js
import Gallery from './Gallery.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Gallery />);
img { margin: 0 10px 10px 0; }
<section>, <h1>, 그리고 3개의 <img> 태그를 위한 DOM 노드들을 생성합니다. ⚠️ 함정 주의 (Pitfall)
렌더링은 반드시 순수한 계산(pure calculation)이어야만 합니다.
- 동일한 입력, 동일한 출력: 같은 입력(props, state 등)이 주어지면, 컴포넌트는 항상 같은 JSX를 반환해야 합니다. (토마토 샐러드를 주문한 손님에게 양파 샐러드를 서빙하면 안 되겠죠!)
- 자기 일에만 집중하기: 렌더링 전에 존재하던 객체나 변수를 변경해서는 안 됩니다. 즉, 부작용(Side Effect)이 없어야 합니다. (한 사람의 주문이 다른 사람의 주문을 변경해서는 안 됩니다.)
그렇지 않으면 코드베이스가 복잡해지면서 매우 혼란스러운 버그나 예측할 수 없는 동작에 직면할 수 있습니다. "Strict Mode(엄격 모드)"로 개발할 때, React는 각 컴포넌트 함수를 두 번씩 호출하는데, 이는 순수하지 않은 함수로 인해 발생한 실수를 표면으로 드러내는 데 큰 도움을 줍니다.
🧐 깊게 파보기 (Deep Dive): 성능 최적화
업데이트된 컴포넌트 안에 중첩된 모든 컴포넌트를 렌더링하는 기본 동작 방식은, 업데이트된 컴포넌트가 트리 구조상 너무 높은 곳에 위치할 경우 성능 측면에서 최적이 아닐 수 있습니다. 만약 성능 문제에 부딪히게 된다면, Performance 섹션에 설명된 여러 가지 선택적 최적화 방법들을 사용할 수 있습니다. 하지만 미리 설레발치며 너무 이르게 최적화(premature optimization)를 하지는 마세요! 대부분의 경우 기본 동작으로도 충분히 빠릅니다.
컴포넌트를 렌더링(호출)하고 나면, 비로소 React는 실제 DOM을 수정합니다.
appendChild() DOM API를 사용하여 자신이 생성한 모든 DOM 노드들을 화면에 배치합니다.React는 렌더링 결과 간에 차이가 있을 때만 DOM 노드를 변경합니다. 예를 들어, 1초마다 부모로부터 다른 props를 전달받아 리렌더링되는 컴포넌트가 있다고 해봅시다. 이 컴포넌트의 <input> 에 텍스트를 입력해서 value를 업데이트해 보세요. 컴포넌트가 계속 리렌더링됨에도 불구하고 입력한 텍스트가 사라지지 않는 것을 볼 수 있습니다.
// src/Clock.js
export default function Clock({ time }) {
return (
<>
<h1>{time}</h1>
<input />
</>
);
}
// src/App.js
import { useState, useEffect } from 'react';
import Clock from './Clock.js';
function useTime() {
const [time, setTime] = useState(() => new Date());
useEffect(() => {
const id = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(id);
}, []);
return time;
}
export default function App() {
const time = useTime();
return (
<Clock time={time.toLocaleTimeString()} />
);
}
👨🏫 강사의 보충 설명:
이 과정이 바로 React의 마법 중 하나인 '재조정(Reconciliation)'입니다. React는 이전 렌더링 결과와 현재 렌더링 결과를 비교해서 '달라진 부분'만 콕 집어서 실제 DOM에 업데이트해요.
이 예제가 정상적으로 동작하는 이유는 마지막 커밋 단계에서 React가 새로운 time 값으로 <h1>의 내용만 업데이트하기 때문입니다. React는 <input>이 지난번 렌더링과 동일한 위치에 그대로 있다는 것을 확인하고, <input> 요소나 그 안의 value 값을 건드리지 않고 그대로 내버려 둡니다.
렌더링이 완료되고 React가 DOM을 업데이트하고 나면, 브라우저는 화면을 다시 그립니다(repaint). 이 과정은 웹 개발 세계에서 보통 "브라우저 렌더링(browser rendering)"이라고 알려져 있지만, React의 "렌더링"과 혼동하는 것을 막기 위해 공식 문서 전반에 걸쳐 이 과정을 "페인팅(painting)" 이라고 부를 것입니다.
👨🏫 강사의 보충 설명:
브라우저가 화면을 그릴 때는 스타일 계산, 레이아웃(위치와 크기 잡기), 페인트(실제 픽셀 채우기), 합성(레이어 합치기) 등의 과정을 거쳐요. React의 커밋 단계가 끝나면, 브라우저가 이 작업들을 넘겨받아 우리가 보는 최종 화면을 모니터에 뿌려주는 것이죠!
