React 리스트와 Key

React

img

리액트를 개발하면서 아래와 같은 경고 메세지를 정말 많이 본다.

Warning: Each child in an array or iterator should have a unique “key” prop

리액트에서 리스트를 사용할 때 왜 key를 넣어주어야 하는지 대략적으로만 알고 있었는데 좀 더 구체적으로 알기 위해서 도큐를 정리해보았다.

(*정리하자면, 리액트에서 key는 컴포넌트 배열을 렌더링 할 때, 어떤 원소에 변경이 있는지 식별하기 위한 고유한 값이다.)

Key Props 사용 동기

리액트의 render() 함수는 React 엘리먼트 트리를 렌더링 하는데, 해당 트리에서 변경된 사항이 있으면 리액트는 변경된 UI를 갱신한다.

이 때, 하나의 트리를 다른 트리로 변환하기 위해서는 최소 O(n^3)의 복잡도를 가지는데, 이는 1000개의 엘리먼트를 그리기 위해 10억번의 비교 연산을 수행해야 한다는 것으로 매우 비효율적이다.

효율을 높이기 위해, React는 두 가지 가정을 기반하여 **O(n)**의 복잡도를 가진 알고리즘을 사용한다.

  1. 서로 다른 타입의 두 엘리먼트는 서로 다른 트리를 만들어낸다.
  2. 개발자가 key prop 을 통해, 여러 렌더링 사이에서 어떤 자식 엘리먼트가 변경되지 않아야 할지 표시해 줄 수 있다.

재조정 (Reconciliation)

리액트에서 페이지에 변화가 일어나면, 전체 페이지를 수정하는 것이 아니라 **비교 알고리즘 (Diffing Algorithm) ** 을 사용해서 변경된 엘리먼트만 수정을 한다.

1. 엘리먼트 타입이 다른 경우

<div>
  <Counter />
</div>

<span>
  <Counter />
</span>

위 예시처럼 두 루트 엘리먼트 타입이 다르면 (<div><span>) 이전 트리를 버리고 완전히 새로운 트리를 생성한다.

트리를 버릴 때, 이전 DOM 노드들을 모두 파괴하며 기존의 <Counter /> 컴포넌트에서는** componentWillUnmount()** 가 실행된다. 루트 엘리먼트 아래의 모든 컴포넌트도 언마운트되고 그 state도 사라진다.

이후, 새로운 트리가 만들어 질 때 새로운 DOM 노드들이 DOM에 삽입되며 **componentDidMount() **가 이어서 실행된다.

즉, 위 예시에서 이전 <Counter/>는 사라지고, 새로 다시 마운트가 될 것 이다.

2. 엘리먼트 타입이 같은 경우

<div className="before" title="stuff" />

<div className="after" title="stuff" />

같은 타입의 두 React DOM 엘리먼트를 비교할 때, React는 두 엘리먼트의 속성을 확인해 동일한 내역은 유지하고 변경된 속성들만 갱신한다.

위 예시에서는 다른 엘리먼트는 똑같이 유지하면서 className만 수정을 한다.

DOM 노드의 처리가 끝나면, 이어서 해당 노드의 자식들을 재귀적으로 갱신하는데, 인스턴스는 동일하게 유지되어 렌더링 간 state가 유지된다.

React는 새로운 엘리먼트의 내용을 반영하기 위해 현재 컴포넌트 인스턴스의 props를 갱신하고, **componentDidUpdate**를 호출한다.


자식에 대한 재귀적 처리

그렇다면 우리는 왜 key props를 사용해야 하는가?

바로 자식 노드 중에 변경사항이 있는지 없는지 체크하기 위해서 이다. 아래 예시를 통해서 key prop의 필요성을 알 수 있다.

DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 한다.

// 변경 전
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// 변경 후
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

위 예시처럼 자식의 끝에만 새로운 엘리먼트를 추가하면, 마지막 엘리먼트인 <li>third</li>를 트리(리스트)에 추가하는 작업을 할 것이다.

하지만 아래처럼 맨 앞에 엘리먼트를 추가하는 경우에는 어떨까

// 변경 전
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

// 변경 후
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

위 예시의 경우, React는 모든 요소가 바뀌었다고 생각하고 모든 자식을 변경한다.


Key 속성 사용

위 두 번째 예시와 같은 문제를 해결하기 위해, React에서 key 속성을 사용한다. React는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인할 수 있다.

<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

이처럼 key prop을 넘겨주면 React는'2014' key를 가진 엘리먼트가 새로 추가되었고, '2015''2016' key를 가진 엘리먼트는 그저 이동만 하면 되는 것을 알 수 있다.


주의사항

  • key는 일반적으로 데이터의 **식별자 (id)**를 사용하면 된다.

  • 만약 식별자를 사용할 수 없다면, 데이터 구조에 ID 속성을 추가하거나 데이터 일부에 해시를 적용해서 key를 생성할 수 있다.

  • key는 오로지 형제 사이에서만 유일하면 되고, 전역에서 유일할 필요는 없다.

  • (비추천) 배열의 인덱스를 key로 사용할 수 있으나, 항목의 순서가 바뀌었을 때 key 또한 바뀌어 문제가 될 수도 있다. (문제상황 예시: https://codepen.io/pen?&editors=0010)




참고: https://ko.reactjs.org/docs/reconciliation.html#the-diffing-algorithm




Profile picture
@김하연
3년차 프론트엔드 개발자 입니다. 사용자 경험 개선, 코드의 재사용성, 읽기 쉬운 코드에 집중하여 개발합니다.
AboutGithub LinkedinResume
Loading script...