오늘 key 속성에 대한 글을 읽다 공감한 부분이 있었다.
Who among us honestly can say that they use it because of “…some valid reasons”, rather than “because eslint rule complained at me”.
프론트엔드 개발 경험이 있다면 누구나 한 번 쯤은 Each child in a list should have a unique "key" prop 경고 문구를 봤을 것이다.
저 문장을 읽으면서 나 또한 key를 설정하는 것은 React가 각 요소를 알아야 하니까, 그리고 설정 안 해주면 경고 문구를 띄우니까 라고 무의식적으로 생각해오고 있었다는 것을 알았다.
그래서 이참에 미루고 있던 key에 대해 알아보는 시간을 가지려고 한다.
Key에 대해 알기 전에 Key가 왜 필요한지부터 알아보자.
DOM 노드의 자식들을 재귀적으로 처리할 때 React는 기본적으로 변경 전과 후의 리스트를 순회하고 변경할 것이 있다면 변경한다.
이때 리스트를 순회하며 변경할 사항을 찾는다는 것을 좀 더 자세히 말하자면 변경 전과 후가 완벽하게 일치하지 않으면 변경이 생겼다고 간주하는 것이다.
// 변경 전
<ul>
<li>1</li>
<li>2</li>
</ul>
// 변경 후
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
예를 들어, 변경 전과 후가 위와 같다면 React는 변경 전과 후의 리스트를 동시에 순회하고 <li>3</li>
이 추가된 것이 유일한 변경사항임을 감지할 수 있다.
하지만 다음의 경우에는 이야기가 달라진다.
// 변경 전
<ul>
<li>1</li>
<li>2</li>
</ul>
// 변경 후
<ul>
<li>3</li>
<li>1</li>
<li>2</li>
</ul>
우리가 봤을 때는 단순히 리스트의 제일 앞에 3이 추가된 것 뿐이지만 React의 입장에서는
<li>1</li>
와 <li>3</li>
가 같지 않음 => 변경이 일어남<li>2</li>
과 <li>1</li>
이 같지 않음 => 변경이 일어남<li>2</li>
가 추가됨이렇게 모든 자식을 다시 생성하게 된다.
이러한 비효율적인 문제를 해결하기 위해 React는 key 속성을 지원한다.
key는 React가 어떤 항목을 변경, 추가, 삭제할 것인지 식별하는 것을 돕는다.
만약 자식들이 key를 가지고 있다면 React는 key를 통해 변경 전과 후가 일치하는지 확인한다.
그리고 변경 전과 후에 동일한 key값을 가진 요소가 존재한다면 재사용한다.
// 변경 전
<ul>
<li key="1">1</li>
<li key="2">2</li>
</ul>
// 변경 후
<ul>
<li key="3">3</li>
<li key="1">1</li>
<li key="2">2</li>
</ul>
아까 모든 자식을 교체해야 했던 코드가 key를 추가한 것만으로 React의 행동이 달라진다.
<li key="1">1</li>
와 <li key="2">2</li>
는 단순히 위치 이동<li key="3">3</li>
가 추가됨 => 이것만 새로 만들어주면 됨!key는 형제 사이에서 유일한 문자열이면 되기 때문에 이 속성에 값을 부여하기 위해 다양한 시도를 해볼 수 있다.
const Todo = (todos) => (
<ul>
{todos.map((todo) => <li key={???}>{todo.data}</li>)}
</ul>
);
key 값으로 랜덤한 문자열을 사용할 경우 컴포넌트가 리렌더링될 때마다 React는 새로운 key 값을 생성한다.
element에 key 속성이 존재하기 때문에 React는 변경 전과 후의 element를 비교할 때 key 값을 사용하지만 변경 후에는 리렌더링 과정을 거치며 새로운 key 값이 생성되었기 때문에 변경 전과 변경 후의 element는 다른 element로 감지된다.
따라서 랜덤한 문자열을 key 값으로 사용했을 경우 React는 모든 element를 재생성한다.
공식문서에서 권장하는 방법은 아니지만 key에 배열의 인덱스를 사용하는 경우도 흔히 볼 수 있다.
정적인 컨텐츠를 렌더링하기 위해서라면 배열의 인덱스를 key값으로 사용할 수 있지만 동적인 컨텐츠를 렌더링하는 경우, 그러니까 렌더링할 때마다 항목의 순서나 수가 변경될 가능성이 있는 경우에는 우리가 원하는 대로 동작하지 않을 가능성이 크다.
배열의 인덱스를 key 값으로 사용하는 경우 첫 번째에 위치한 element의 key 값은 무조건 0이고 두 번째 위치한 element의 key 값은 1이다.
렌더링 과정에서 element들이 다른 순서로 재배치되어도 첫 번째 element의 key 값은 0이다.
즉, 렌더링 과정에서 데이터의 순서가 바뀌었음에도 React는 key를 통해 변경 여부를 파악하기 때문에 값만 변경된 동일한 element라고 파악하는 것이다.
이 예제를 보면 왜 이러한 경우 key 값으로 배열의 인덱스를 쓰면 안되는지 이해할 수 있다.
해당 데이터를 표현할 수 있는 특별한 값, 예를 들어 id나 데이터에 해시를 적용한 값을 key로 쓰면 key 값과 데이터 간의 1-1 관계가 생긴다.
초기 렌더링에서나 리렌더링 이후 데이터의 순서가 변경되어도 항상 특정 key 값이면 특정한 data임을 보장할 수 있다.
그렇다고 무조건 데이터와 1-1 관계가 생기는 특정한 값을 key로 써야한다는 것은 아니다.
오히려 이러한 원리를 알고 있기 때문에 다른 방식으로 사용할 수 있다.
아까 key 값으로 배열의 인덱스를 사용하면 첫 번째 element의 key 값은 0, 두 번째 element의 key 값은 1,... 이런 식으로 key 값이 고정된다고 했었다.
쇼핑몰과 같이 페이지가 나누어져 있고 한 페이지에 렌더링할 목록의 개수가 정해져있는 경우를 생각해보자.
이때 key 값으로 상품을 표현할 수 있는 특정한 값을 사용하면 페이지가 바뀔 때마다 상품이 바뀌기 때문에 모든 key 값도 바뀌고 따라서 모든 element를 재생성해야 한다.
따라서 이러한 경우 key 값에 배열의 인덱스를 사용하면 생성해둔 element를 재사용하면서 그 안의 상품 데이터만 바꿔줄 수 있다.
React key attribute: best practices for performant lists
React 공식문서 - list and keys
React 공식문서 - recursing on children