재조정(Reconciliation): 리액트는 어떻게 UI를 다시 그릴까?

keemsebeen·2025년 5월 1일
post-thumbnail

공식 문서를 읽다보니 다음 문장에 호기심을 느끼게 됐습니다.

컴포넌트에 state를 줄 때 state가 컴포넌트 안에 “살고” 있다고 생각할 수도 있습니다. 하지만 사실 state는 React 안에 있습니다. React는 컴포넌트가 UI 트리에 있는 위치를 이용해 React가 가지고 있는 각 state를 알맞은 컴포넌트와 연결합니다.

같은 컴포넌트, 다른 위치 - 상태는 유지되지 않는다.

두 예시는 모두 동일한 ProfileCard 컴포넌트를 렌더링합니다. 그런데 하나는 다크모드 토글 시 입력값이 사라지고, 다른 하나는 그대로 유지됩니다. 두 영상의 차이점이 무엇일 거라고 생각하시나요? 단순 useState 선언 차이일까요?

아래 코드를 보면 다른 점은 ProfileCard를 감싸고 있는 부모 태그밖에 없습니다.

<div>
	{isDarkMode ? (
	  <aside>
	    <ProfileCard theme="dark" />
	  </aside>
	) : (
	  <section>
	    <ProfileCard theme="light" />
	  </section>
	)}
</div>

하나는 <aside>, 다른 하나는 <section>입니다. 같은 컴포넌트를 렌더링하는데도 불구하고 상태가 유지되지 않는 이유는 바로 이 차이에 있습니다.

리액트는 UI 업데이트 과정에서 단순히 “같은 코드 모양을 가졌으니 재사용해야겠다“라고 판단하지 않습니다. 그 대신 이 컴포넌트가 UI 트리 상에서 같은 위치에 있는가?를 기준으로 컴포넌트의 정체성을 파악합니다. 이 위치 정보는 컴포넌트의 상태를 보존할지 말지를 결정하는 핵심 기준이 됩니다.

결론적으로, 리액트는 컴포넌트의 코드 모양이 아닌 위치와 요소 타입을 기반으로 정체성을 판단합니다. 그리고 바로 이 개념이 재조정(Reconciliation) 과정의 핵심입니다.

재조정(Reconciliation): 리액트는 어떻게 UI를 다시 그릴까?

리액트에서 렌더링은 실제로 매 순간 전체 UI를 다시 렌더링하지 않고, 이전 렌더링 결과와 비교해서 바뀐 부분만 바꿔주는 작업인 재조정(reconciliation)을 수행합니다.

공식 문서에서도 언급되었듯, 리액트는 컴포넌트의 상태를 컴포넌트 자체에 묶어두지 않고, 컴포넌트의 위치를 기준으로 상태를 리액트 내부에 저장합니다. 그 위치는 Virtual DOM 트리에서의 위치이며, Fiber 구조를 통해 관리됩니다.

이때, 재조정의 핵심 질문은 다음과 같습니다.

이전 렌더링 결과와 새로운 렌더링 결과를 비교했을 때, 어떤 컴포넌트를 그대로 재사용하고, 어떤 컴포넌트를 새로 렌더링해야 할까?

React는 이 질문에 대해 세 가지 주요 기준을 사용합니다.

1️⃣ 요소타입이 같은가?

먼저, 이전과 새로운 요소의 타입이 같은지 확인합니다. 타입이 다르면 리액트는 해당 서브트리를 전부 제거하고 새로 생성합니다.

{isDarkMode ? (
  <aside>
    <ProfileCard />
  </aside>
) : (
  <section>
    <ProfileCard />
  </section>
)}

위 예시에서 <aside><section>은 서로 다른 타입의 DOM 요소이기 때문에, 리액트는 그 자식인 ProfileCard다른 트리의 자식으로 간주하고, unmount → mount를 수행합니다. 따라서 내부 state는 날아가게 됩니다.

2️⃣ 요소의 위치가 같은가?

같은 타입이라고 하더라도, 컴포넌트가 UI 트리 내에서 다른 위치에 있으면 동일한 컴포넌트로 간주되지 않습니다. 리액트 트리 구조에서의 순서와 위치를 기준으로 상태를 연결합니다.

아래는 ProfileCard가 같아 보여도, 상태는 유지되지 않습니다.

{isLoggedIn ? <ProfileCard /> : <Login />}
  • 조건부 표현식의 첫 번째 자리에 <ProfileCard />가 위치합니다.
  • 따라서 React는 이 조건부의 첫 번째 자리에 항상 동일한 컴포넌트가 있어야 한다고 기대합니다.
{isLoggedIn ? <Dashboard /> : <ProfileCard />}
  • 여기에선 <ProfileCard /> 가 조건부 표현식의 두 번째 자리에 위치합니다.
  • 리액트는 두번째 자리엔 보통 <Dashboard />가 있다고 기억하고 있습니다.

따라서 ProfileCard가 아무리 같은 코드여도, 다른 위치에서 렌더링되면 전혀 다른 컴포넌트로 간주되어 상태가 유지되지 않는 것입니다.

리액트는 자식들을 배열처럼 취급하며 index로 순서를 비교하기 때문에 렌더 트리와 새로운 렌더트리를 비교하는 과정에서 다르게 취급하게 됩니다.

3️⃣ key가 같은가? (동적 리스트에서의 재조정)

리스트의 경우, 리액트는 요소의 순서(index)를 기준으로 비교합니다. 그러나 리스트는 추가, 삭제가 많기 때문에 순서(index)만으로 비교하는 건 비효율적일 수 있습니다. 이 때문에 key가 필요합니다.

리액트는 key를 통해 리스트 안의 각 항목의 정체성을 추적합니다. key가 바뀌면, 해당 항목은 새로운 항목으로 간주되어 상태가 초기화됩니다.

const todos = [
  { id: 1, text: "코딩하기" },
  { id: 2, text: "밥 먹기" },
];

return (
  <ul>
    {todos.map((todo) => (
      <li key={todo.id}>{todo.text}</li>
    ))}
  </ul>
);

key가 없으면 리액트는 리스트가 변경될 때마다 모든 항목을 다시 렌더링할 수 있습니다. 하지만 고유한 key가 있으면, React는 어떤 항목이 바뀌었고, 어떤 항목은 그대로일지를 더 정확하게 판단할 수 있습니다.

마치며

컴포넌트는 코드가 아니라 위치로 기억됩니다. 리액트가 상태를 어떻게 추적하고 관리하는지 이해하는 것은 단순한 기술 지식을 넘어, UI 설계의 관점을 바꾸는 계기가 될수도 있지 않을까 생각합니다! "어떤 컴포넌트를 쓸까?"보다 "어디에 둘까?" 고민하는 것도 충분히 의미있는 일 같습니다.
이 글을 보시는 분들도 여러분의 컴포넌트가 어디에 “살고” 있는지 한 번쯤 돌아보면 좋겠습니다!

( ++ 추가적으로 작성하고 싶은 내용이 있는데, 학습 후 내용 추가 예정입니다.)


참고
https://substack.com/redirect/f94794d7-c569-438a-a9ed-005e98ba3b1d?j=eyJ1IjoiNWhyN2EzIn0.1aiRHpg4S06oGuvO63aRdWLUFYCF0SNS2ux_nsaaCJ4

https://ko.legacy.reactjs.org/docs/reconciliation.html

profile
프론트엔드 개발자 김세빈입니다. 👩🏻‍💻

5개의 댓글

comment-user-thumbnail
2025년 5월 2일

세라의 컴포넌트는 어디에 살고 있으시죠..?

3개의 답글
comment-user-thumbnail
2025년 5월 3일

"추가적으로 작성하고 싶은 내용이 있는데, 학습 후 내용 추가 예정" 좋은데요..? 👍

답글 달기