리액트를 좀 써보다 보면, ‘렌더링이 왜 이렇게 자주 일어나지?’ 혹은 ‘이 함수가 왜 또 호출되지?’ 같은 궁금증이 생깁니다.
이건 단순히 코드의 문제가 아니라 React의 렌더링 메커니즘을 제대로 이해하지 못해서 생기는 자연스러운 현상입니다.
이번 글에서는 React 내부의 렌더링 구조를 실제 동작 흐름 중심으로 풀어보겠습니다.
React의 렌더링 사이클이란
React는 UI를 함수처럼 다룹니다.
“상태(state)가 바뀌면 함수가 다시 실행되고, 그 결과를 UI로 보여주는 구조”입니다.
이 과정을 크게 세 단계로 나누면 다음과 같습니다.
- 렌더(Render)
컴포넌트 함수가 다시 실행되어 React Element 트리를 새로 만듭니다. - 커밋(Commit)
새로 만들어진 React Element를 실제 DOM에 반영합니다. - 업데이트(Update)
상태나 props가 바뀔 때 다시 렌더 → 커밋 과정을 반복합니다.
중요한 점은, “렌더링이 일어났다고 해서 반드시 DOM이 갱신되는 건 아니다”는 겁니다.
React는 내부적으로 Virtual DOM을 비교(diff)해
바뀐 부분만 실제 DOM에 반영합니다.
렌더링이 일어나는 정확한 순간
렌더링은 다음과 같은 상황에서 발생합니다.
- useState로 상태가 바뀌었을 때
- 부모 컴포넌트의 props가 바뀌었을 때
- Context 값이 변경되었을 때
- React Query나 Zustand 같은 외부 상태가 갱신되었을 때
React는 이러한 변화를 감지하면,
해당 컴포넌트 함수 전체를 다시 실행해 새로운 Virtual DOM을 만듭니다.
그 결과가 이전과 다르면, 그때서야 실제 DOM을 바꿉니다.
이 과정을 “Reconciliation(재조정)”이라고 부릅니다.
렌더링이 잦아지는 이유
한동안 저는 렌더링이 “불필요하게 자주” 일어나는 줄 알았습니다.
하지만 알고 보니 대부분은 React의 정상 동작이었죠.
React는 상태가 조금이라도 바뀌면
그 상태를 참조하는 모든 컴포넌트를 다시 실행합니다.
즉, 함수형 컴포넌트는 “렌더링”이 곧 “다시 호출”이라는 뜻입니다.
이건 문제라기보다 React의 기본 철학에 가깝습니다.
컴포넌트가 순수 함수처럼 동작하도록 만들기 위함이죠.
그래서 불필요한 렌더링을 줄이는 방향은,
렌더링 자체를 막는 게 아니라 비용이 큰 부분만 최소화하는 것입니다.
(이전 글에서 다뤘던 React.memo, useMemo, useCallback이 바로 이 역할을 합니다.)
React 18 이후의 Concurrent Rendering
React 18부터는 “Concurrent Rendering”이라는 개념이 들어왔습니다.
이는 렌더링을 중단하거나, 지연하거나, 병렬로 처리할 수 있는 기능입니다.
예전엔 렌더링이 한 번 시작되면 브라우저가 멈추다시피 했습니다.
하지만 Concurrent Mode에서는
React가 백그라운드에서 새로운 렌더를 준비하고,
필요할 때만 교체하는 식으로 동작합니다.
이 구조 덕분에 “대형 리스트”, “검색 자동완성”, “페이지 전환” 같은 상황에서
화면이 버벅거리지 않고 자연스럽게 반응할 수 있습니다.
대표적인 기능으로 **useTransition**과 **useDeferredValue**가 있습니다.
import { useState, useTransition } from 'react';
function Search() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
startTransition(() => setQuery(value));
}
return (
<div>
<input onChange={handleChange} />
{isPending && <p>검색 중...</p>}
<Results query={query} />
</div>
);
}
이 예제에서 startTransition은
상태 업데이트를 “급하지 않은 작업”으로 분류해 브라우저가 여유 있을 때 처리하게 합니다.
즉, React가 렌더링 우선순위를 조정할 수 있게 된 것입니다.
실제로 렌더링 병목이 생겼던 사례
저는 최근 관리자 대시보드 프로젝트에서
한 페이지에 수천 개의 데이터 카드가 동시에 렌더링되는 문제가 있었습니다.
데이터를 새로 받아올 때마다 화면이 멈췄고,
React DevTools로 보니 렌더링 시간이 200ms 이상 걸리더군요.
해결 방법은 단순했습니다.
useDeferredValue로 입력값을 지연시키고,
React.memo로 리스트 아이템을 감싸줬습니다.
그 결과 렌더링 속도가 5배 이상 개선됐고,
사용자는 로딩 중에도 자연스럽게 타이핑할 수 있었습니다.
import { useDeferredValue } from 'react';
function SearchList({ query }) {
const deferredQuery = useDeferredValue(query);
const filtered = useMemo(
() => data.filter((item) => item.includes(deferredQuery)),
[deferredQuery]
);
return filtered.map((v) => <div key={v}>{v}</div>);
}
이처럼 React의 렌더링 구조를 이해하면
문제가 생겼을 때 어떤 도구를 써야 하는지 명확해집니다.
렌더링 구조를 이해하면 얻게 되는 것
React의 렌더링 과정은 단순히 최적화를 위한 개념이 아닙니다.
이걸 이해하면 코드의 동작을 예측할 수 있게 됩니다.
- “이 상태를 변경하면 어떤 컴포넌트가 다시 실행될까?”
- “이 props는 매번 새로운 참조를 만들고 있지 않은가?”
- “렌더링이 커밋되기 전에 어떤 일이 일어나고 있는가?”
이 질문들에 스스로 답할 수 있을 때,
React 애플리케이션을 정말로 ‘통제’할 수 있게 됩니다.
지금까지는 렌더링이 어떻게 동작하는지 내부 흐름을 중심으로 살펴봤습니다.
다음 글에서는 이 렌더링 과정과 맞물려 작동하는 **React 생명주기(Lifecycle)**를
함수형 기준에서 정리해보겠습니다.
useEffect, useLayoutEffect, 그리고 렌더-커밋 간의 순서를 명확히 이해하면
React의 비동기 처리 타이밍을 완전히 잡을 수 있습니다.
'frontend > react' 카테고리의 다른 글
| [React] React.js 실무 강좌 31. React Router v6 완전 가이드 — 페이지 전환의 모든 것 (0) | 2025.11.11 |
|---|---|
| [React] React.js 강좌 30. useEffect 완전 정복 — 렌더링 이후에 일어나는 일들 (0) | 2025.11.11 |
| [React] React.js 강좌 28. 리렌더링 최소화를 위한 React 성능 최적화 (0) | 2025.11.11 |
| [React] React.js 강좌 27. React Query와 Zustand를 함께 사용하는 구조 설계 (0) | 2025.11.11 |
| [React] React.js 강좌 26. Zustand로 가벼운 전역 상태 관리하기 (0) | 2025.11.11 |