
리액트의 Reconciliation
react
React의 Reconciliation, 그리고 state가 유지되거나 초기화되는 이유
모던 리액트 DeepDive를 읽기 시작했다.
원래는 1장을 가볍게 읽으려고 했는데, 들어가며 부분에서
reconciliation이라는 익숙한 단어를 보고 괜히 반가운 마음에 공식 문서부터 찾아봤다. 결과적으로는 공식 문서 하나 읽고 정리하는 데 한 시간 반이 걸렸다. 성급했다. 다음부터는 함부로 검색하지 말아야겠다. 아직 1장도 안 읽었는데 들어가며에서 시간을 다 써버렸다.그래도 얻은 건 있었다.
평소에도 “리액트는 상태를 잘 관리한다”, “불필요한 렌더링을 줄인다”, “컴포넌트 단위로 UI를 관리한다” 같은 말을 익숙하게 들어왔지만, 정작 그 내부 논리를 머릿속에서 명확하게 설명하긴 어려웠다. 특히 다음과 같은 것들이다.
- 왜 어떤 state는 유지되고, 어떤 state는 초기화될까?
- 같은 컴포넌트인데 왜 어떤 경우에는 이전 상태를 기억하고, 어떤 경우에는 새로 시작할까?
- React는 컴포넌트를 어떤 기준으로 같은 것으로 보고, 다른 것으로 볼까?
이번에 reconciliation 관련 내용을 읽으면서, 이 질문들이 꽤 선명해졌다.
이 글은 React 공식 문서를 바탕으로 정리한 내용에, 내가 이해한 방식과 예시를 덧붙여 정리한 글이다.
왜 reconciliation을 이해해야 할까

React를 처음 배울 때는 대개 “상태가 바뀌면 다시 렌더링된다” 정도로 이해하고 넘어간다. 이 설명 자체는 틀리지 않지만, 실무에서 코드를 작성하다 보면 그것만으로는 부족하다.
예를 들어 이런 상황이 자주 생긴다.
- 토글했더니 input 값이 사라진다.
- 조건부 렌더링을 했는데 이전 state가 유지된다.
- 리스트를 바꿨더니 예상치 못한 컴포넌트 state가 섞인다.
- 분명 다른 props를 넘겼는데 왜 이전 state를 그대로 들고 있을까?
- 반대로 같은 컴포넌트라고 생각했는데 왜 state가 초기화될까?
이런 문제는 결국 React가 이전 트리와 다음 트리를 비교해서 무엇을 유지하고 무엇을 버릴지 판단하는 방식, 즉 reconciliation과 연결된다.
React는 매 렌더링마다 화면 전체를 무작정 새로 만드는 것이 아니라, 이전 UI 트리와 새로운 UI 트리를 비교해서 필요한 부분만 업데이트한다. 이 비교 과정에서 “이 컴포넌트는 이전과 같은 존재인가?”를 판단하고, 그 결과에 따라 state를 유지할지 초기화할지를 결정한다.
즉, reconciliation은 단순히 성능 최적화 개념이 아니라, React에서 state의 생명주기를 이해하는 핵심 개념이라고 보는 편이 더 정확하다.
React는 UI를 트리로 본다
브라우저가 DOM 트리와 CSSOM 트리를 기반으로 화면을 렌더링하듯, React도 UI를 트리 구조로 바라본다. JSX로 작성한 컴포넌트 구조는 결국 하나의 UI 트리로 표현되고, React는 이 트리를 기준으로 어떤 요소가 어디에 있는지 추적한다.
중요한 점은 React가 state를 “컴포넌트 함수 내부 변수”처럼 보관하는 것이 아니라, UI 트리 안에서 특정 위치에 연결된 값처럼 관리한다는 점이다.
즉, state는 단순히
useState를 호출했다고 생기는 것이 아니라, “이 트리의 이 위치에 렌더링된 이 컴포넌트의 상태”로 관리된다.이 관점을 이해하면, React에서 state가 유지되거나 초기화되는 이유가 훨씬 자연스럽게 보인다.
state는 컴포넌트 내부가 아니라, 트리의 위치와 연결된다
다음 코드를 보자.

처음 보면
counter라는 JSX 값을 재사용했기 때문에 두 개가 같은 상태를 공유할 것처럼 느껴질 수도 있다. 하지만 실제로는 그렇지 않다. 화면에는 두 개의 Counter가 각각 다른 위치에 렌더링되고, React는 이 둘을 서로 다른 state를 가진 독립적인 컴포넌트로 본다.즉, 중요한 것은 “같은 JSX를 재사용했는가”가 아니라 “트리에서 어느 위치에 렌더링되었는가”다.
이걸 React 관점에서 다시 말하면, 두
Counter는 서로 다른 주소에 놓여 있으므로 각각 독립적인 state를 가진다.React가 컴포넌트를 제거하면 그 state도 함께 사라진다
이건 생각해보면 자연스럽다. React는 state를 트리의 위치에 연결해 관리하므로, 해당 위치의 컴포넌트가 사라지면 그 state도 더 이상 유지할 이유가 없다.
예를 들어 어떤 조건에 따라 컴포넌트를 렌더링했다가 제거하면, 다시 나타났을 때는 새로운 컴포넌트로 취급된다.
이런 상태에서
showB가 false가 되어 어떤 컴포넌트가 트리에서 제거되면, 그 컴포넌트의 state도 함께 사라진다. 이후 다시 렌더링되면 이전 state를 복구하는 것이 아니라 처음부터 새로 시작한다.이건 실무에서도 자주 만난다. 예를 들어 모달을 닫았다가 다시 열었는데 form 값이 초기화되는 경우가 있다. 모달이 단순히 숨겨진 것이 아니라 아예 트리에서 제거되었다면, 그 안의 state 역시 사라지는 것이 자연스럽다.
같은 위치에 같은 컴포넌트가 있으면 state는 유지된다
React는 매 렌더링마다 “같은 위치에 같은 종류의 컴포넌트가 계속 있는가”를 본다. 그렇다면 props가 바뀌더라도 state는 보존된다.
다음 예제를 보자.
겉으로 보면 조건문이 있으니 다른 컴포넌트처럼 느껴질 수 있다. 하지만 React 입장에서는 둘 다 같은 위치에 렌더링되는
Counter다. 달라진 것은 isFancy라는 props뿐이다.즉, React는 이 경우 “같은 컴포넌트가 계속 그 자리에 있다”고 판단한다. 그래서
score와 hover state는 유지된다.여기서 중요한 포인트는, JSX 코드가 어떻게 생겼느냐보다 최종적으로 만들어진 트리 구조에서 같은 위치에 같은 타입의 컴포넌트가 유지되느냐는 점이다.
같은 위치라도 다른 컴포넌트가 오면 state는 초기화된다
반대로 같은 위치에 다른 타입의 컴포넌트가 렌더링되면 state는 유지되지 않는다.
이 경우 어떤 순간에는 그 위치에
p가 있고, 어떤 순간에는 Counter가 있다. React는 이 둘을 같은 존재로 볼 수 없다. 따라서 Counter가 사라졌다가 다시 나타나면 이전 state는 사라지고 새로 생성된다.이 원리를 이해하면 “조건부 렌더링했더니 값이 왜 날아갔지?” 같은 현상을 설명할 수 있다.
React는 단순히 화면에 같은 자리에 보인다고 해서 같은 컴포넌트라고 판단하지 않는다. 타입과 위치가 함께 맞아야 state가 유지된다.
부모 구조가 달라져도 하위 트리 state가 초기화될 수 있다
조금 더 헷갈리는 예제를 보자.
겉으로 보기에는 둘 다
Counter를 렌더링하고 있으니 state가 유지될 것처럼 느껴질 수 있다. 하지만 실제로는 상위 구조가 달라졌다. 한쪽은 div > Counter, 다른 한쪽은 section > Counter다.React는 트리를 비교할 때 하위만 떼어놓고 보는 것이 아니라, 전체 구조 안에서 판단한다. 상위 노드가 달라지면 그 아래의 하위 트리도 새로 만들어질 수 있다. 결과적으로
Counter의 state도 초기화된다.이 부분은 실무에서 꽤 중요하다. “내부 컴포넌트는 그대로인데 왜 state가 날아가나?” 싶을 때, 의외로 상위 래퍼 엘리먼트가 바뀌고 있는 경우가 있다.

컴포넌트 함수 정의를 중첩하면 안 되는 이유
다음 예제는 React 공식 문서에서도 강조하는 부분이다.
이 코드는 문법적으로는 가능해 보이지만 좋은 패턴이 아니다. 이유는
MyComponent가 다시 렌더링될 때마다 MyTextField 함수도 새로 정의되기 때문이다. React 입장에서는 이전 렌더링의 MyTextField와 현재 렌더링의 MyTextField를 같은 컴포넌트로 안정적으로 취급하기 어렵다.결과적으로 내부 state가 의도치 않게 초기화될 수 있다.
컴포넌트는 항상 최상위 레벨에서 정의하는 것이 안전하다. 이건 단순한 스타일 가이드가 아니라, React가 컴포넌트 identity를 안정적으로 추적할 수 있도록 하기 위한 규칙에 가깝다.
state를 유지하고 싶을 때와 초기화하고 싶을 때
기본적으로 React는 같은 위치에 같은 컴포넌트가 있으면 state를 유지한다. 하지만 어떤 상황에서는 오히려 state를 리셋하고 싶을 때도 있다.
대표적인 예가 플레이어 전환, 채팅 대상 전환, 탭 전환 같은 경우다.
같은 자리에 두면 state는 유지된다
다음 코드를 보자.
이 경우 React는 같은 위치에 같은
Counter 컴포넌트가 있다고 본다. 다만 person props만 바뀐다. 그래서 Taylor에서 Sarah로 바꿔도 score 같은 state는 유지된다.이게 의도한 동작일 수도 있지만, 아닐 수도 있다. 예를 들어 “Taylor의 점수판”과 “Sarah의 점수판”을 완전히 별개의 상태로 다루고 싶다면, 이 방식은 적절하지 않을 수 있다.
다른 위치에 렌더링하면 state를 분리할 수 있다
이 경우 두
Counter는 서로 다른 위치에 존재하게 된다. 하나가 보일 때 다른 하나는 렌더링되지 않지만, React 관점에서는 서로 다른 위치의 컴포넌트다. 따라서 각각 별개의 state를 가진다.다만 이런 구조는 JSX가 길어질 수 있고, 의도가 명확하지 않을 때도 있다.
더 일반적인 해법은 key다
이럴 때 가장 널리 쓰는 방법이
key다.key는 단순히 리스트 렌더링에서만 쓰는 값이 아니다. React에게 “이 둘은 같은 자리에 있더라도 서로 다른 정체성을 가진 컴포넌트다”라고 알려주는 역할을 한다.즉,
person이 바뀌는 같은 Counter가 아니라,- key가
"Taylor"인 Counter
- key가
"Sarah"인 Counter
를 서로 다른 컴포넌트로 보게 만든다.
그 결과 전환될 때마다 state가 초기화된다.
key는 React의 identity를 명시하는 도구다
많은 경우 key를 “리스트 렌더링에서 경고 없애는 용도” 정도로만 생각하기 쉽다. 하지만 실제로는 더 본질적인 의미가 있다. key는 React가 컴포넌트를 구분하는 기준 중 하나다.
특히 reconciliation을 이해한 뒤에는 key를 이렇게 보는 편이 더 자연스럽다.
- key가 같으면: 같은 컴포넌트로 이어질 가능성이 높다
- key가 다르면: 다른 컴포넌트로 간주되어 state가 초기화될 수 있다
그래서 key는 성능 최적화나 경고 제거 도구가 아니라, 컴포넌트의 정체성을 개발자가 명시하는 장치라고 이해하는 편이 좋다.
채팅 앱에서 input state를 초기화하고 싶은 이유
이 개념이 가장 직관적으로 와닿는 예시 중 하나가 채팅 앱이다.
예를 들어 A와 대화 중일 때 입력창에 메시지를 작성하다가, 수신자를 B로 바꿨다고 해보자. 이때 이전에 A에게 쓰던 초안이 그대로 남아 있으면 이상하다. 보통은 수신자가 바뀌면 입력창도 초기화되는 것이 자연스럽다.
이럴 때 key를 활용할 수 있다.
이렇게 하면 수신자가 바뀔 때마다
Chat 컴포넌트는 새로운 컴포넌트로 취급되고, 그 안의 입력 state도 함께 초기화된다.이건 단순한 편의 기능이 아니라, UX 관점에서도 중요하다. React의 reconciliation 규칙을 이해하면 이런 동작을 “어떻게 우연히 맞출까”가 아니라 “왜 이렇게 설계해야 하는가” 수준에서 판단할 수 있다.
그런데 이전 초안을 다시 복구하고 싶다면?
반대로 어떤 경우에는 이전 state를 다시 복구하고 싶을 수도 있다. 예를 들어 채팅 앱에서 A에게 쓰다 만 메시지를 잠시 저장해두고, 다시 A를 선택하면 이전 초안을 보여주고 싶을 수 있다.
이 경우에는 단순히 컴포넌트 내부 state에만 맡기기 어렵다. 수신자가 바뀔 때 컴포넌트가 교체되며 state가 사라질 수 있기 때문이다. 이럴 때는 상태를 더 바깥으로 끌어올려 관리해야 한다.
예를 들면 다음과 같은 방법이 가능하다.
- 상위 컴포넌트에서 수신자별 draft를 관리
- 전역 상태 저장소 사용
localStorage에 임시 저장
즉, “이 state가 컴포넌트 생명주기에 종속되어야 하는가, 아니면 더 오래 유지되어야 하는가”를 기준으로 상태의 위치를 다시 설계해야 한다.
이건 단순히 React 문법의 문제가 아니라 상태 설계의 문제다.
reconciliation을 이해하고 나니 보이는 것들
이 내용을 보고 나니 평소에 그냥 경험적으로 알고 있던 것들이 조금 더 구조적으로 정리됐다.
예를 들면 이런 것들이다.
1. state는 컴포넌트 안에 있는 것 같지만, 실제로는 트리의 위치에 묶여 있다
그래서 같은 함수 컴포넌트를 썼더라도 위치가 다르면 다른 state를 가진다.
2. props가 바뀌는 것과 컴포넌트가 바뀌는 것은 다르다
같은 위치에 같은 타입의 컴포넌트가 있으면 props가 달라져도 state는 유지된다.
3. state 초기화는 “버그”가 아니라 React의 정체성 판단 결과다
위치, 타입, key 중 하나가 달라지면 React는 이전 컴포넌트를 버리고 새로 만든다.
4. key는 리스트 문법이 아니라 state 보존 전략과도 연결된다
어떤 컴포넌트를 같은 것으로 볼지, 다른 것으로 볼지를 개발자가 직접 제어할 수 있다.
마무리
처음에는 reconciliation이라는 단어가 익숙해서 가볍게 보고 넘어갈 수 있을 줄 알았다. 그런데 막상 공식 문서를 읽다 보니 생각보다 훨씬 중요한 개념이었다. 단순히 “React가 알아서 최적화해준다” 수준의 이야기가 아니라, React가 UI를 어떻게 바라보는지, state를 어떻게 보존하고 초기화하는지에 대한 핵심 규칙과 이어져 있었기 때문이다.
특히 인상 깊었던 건 state를 변수처럼 보는 관점에서 벗어나, 트리의 특정 위치에 연결된 값으로 보는 시각이었다. 이 관점을 받아들이고 나니 조건부 렌더링, key, state 초기화, 컴포넌트 identity 같은 것들이 조금 더 자연스럽게 연결되기 시작했다.
앞으로 React 코드를 작성할 때도 단순히 “왜 값이 날아갔지?”라고 보는 대신,
- 지금 이 컴포넌트는 이전과 같은 위치에 있는가
- 같은 타입으로 유지되고 있는가
- key는 안정적인가
- 이 state는 정말 이 컴포넌트 안에 있어야 하는가
같은 질문을 먼저 해보게 될 것 같다.