본문 바로가기

Frontend

key로 컴포넌트 안의 state를 초기화 하기

React는 렌더링 시점마다 Virtual DOM을 비교해서 변경된 부분만 업데이트한다. 이때 React는 이 컴포넌트가 이전에 있던 그 컴포넌트인가를 판단해야 하는데, 그 판단 기준 중 하나가 key다.

 

일반적으로 key는 리스트 렌더링에서 아이템을 고유하게 식별하는 용도로 많이 쓰인다. 하지만 key는 리스트 외에도 컴포넌트의 정체성을 강제로 바꾸는 트리거로 사용할 수 있다. 즉, key가 바뀌면 React는 기존 컴포넌트를 같은 것으로 보지 않고, 완전히 다른 새 컴포넌트로 취급한다.

 

따라서 key가 바뀌는 순간 React는 다음을 수행한다.

  1. 기존 컴포넌트를 unmount한다.
  2. 새로운 컴포넌트를 mount한다.
  3. 그 과정에서 내부 state는 모두 초기화된다.
  4. useEffect 등 라이프사이클 로직도 다시 처음부터 실행된다.

이 성질을 이용하면 특정 조건이 바뀔 때마다 상태를 통째로 리셋하고 싶다는 요구사항을 아주 직관적으로 해결할 수 있다.

 

문제 상황: 클릭한 대상이 바뀌면 검색 필터 state를 초기화하고 싶다.

예를 들어 컴포넌트를 클릭하면 오른쪽 검색 필터 패널이 열리고, 클릭한 컴포넌트를 기준으로 필터를 적용하는 상황을 생각해본다.

이때 사용자가 A 컴포넌트를 클릭해서 필터를 설정하고 있던 중에 B 컴포넌트를 클릭했다고 가정한다. 이때, 클릭한 대상은 A에서 B로 변경되었지만, 필터 컴포넌트는 계속 mount 되어 있는 상황이다.

그러면 필터 패널은 B에 대한 필터 UI로 바뀌어야 하고, A에서 입력하던 state는 남아있으면 안 된다.

하지만 React는 기본적으로 컴포넌트가 계속 살아 있는 한 state를 보존한다.

즉, props만 바뀌었을 뿐 같은 컴포넌트 인스턴스라고 판단하면, 기존 state가 그대로 남는다.

 

해결: key를 부여한다.

아래 코드가 바로 그 해결책이다.

{activeTab === FilterTab.NODE ? (
  <NodeFilterTab key={`node-${highlightedNodeId ?? 'none'}`} />
) : (
  <PathFilterTab key={`path-${highlightedNodeId ?? 'none'}`} />
)}

 

여기서 핵심은 key가 activeTab과 highlightedNodeId에 의해 결정된다는 점이다.

  • 탭이 NODE ↔ PATH로 바뀌면 node-xxx ↔ path-xxx 로 key가 달라진다.
  • 클릭한 노드가 A → B로 바뀌면 node-A → node-B 로 key가 달라진다.

key가 달라지는 순간 React는 다음처럼 동작한다.

  1. 기존 UiCanvasNodeFrontFilter를 unmount 한다.
  2. 새 key를 가진 UiCanvasNodeFrontFilter를 mount 한다.
  3. 내부 state는 초기값으로 다시 시작한다.

즉, 대상이 달라지면 필터 패널을 새로 만든다는 의도를 React에게 명확하게 전달한 것이다.

 

이 패턴이 좋은 이유

이 방식의 장점은 상당히 명확하다. 상태 초기화를 코드로 억지로 하지 않아도 된다. 보통은 아래처럼 useEffect에서 대상 변경을 감지해 수동으로 reset 한다.

useEffect(() => {
  setFilterState(INITIAL_STATE);
}, [highlightedNodeId]);

 

하지만 state가 많아질수록 초기화 로직은 점점 복잡해지고, 빠뜨리는 필드가 생기기 쉽다. key 방식은 그런 수동 초기화를 React 라이프사이클에 자연스럽게 위임하는 패턴이다.