다들 아시다시피 리액트에서 리스트를 구현할 때 우리는 리스트 아이템의 가장 부모되는 jsx 태그에 key 속성을 부여한다. 안그러면 자꾸 에러가 뜬다.
{ list.map((item, i) => (
<ListItem key={i} data={item} />
)}
위 코드에서 어떤 점이 잘못됐는지 바로 알아챘을 것이다. key={i}
부분을 우리는 index
값이 아닌, 각 아이템의 고유한 값으로 줘야 한다는 사실을 잘 알고있다.
왜 그래야만 하는지 이유를 우리는 알고 있지만, 막상 설명하기 힘들때가 있다. 그래서 이 글을 정리해본다.
리액트는 기존의 리스트와 새로운 리스트를 순회하면서 차이점이 있으면 변경을 생성한다. 아래와 같은 리스트가 있다고 가정해보자.
key: 0, children: 영수
key: 1, children: 철이
<ul>
<li>영수</li>
<li>철이</li>
</ul>
위의 리스트에 숙자
를 추가해보자.
key: 0, children: 영수
key: 1, children: 철이
key: 2, children: 숙자
<ul>
<li>영수</li>
<li>철이</li>
<li>숙자</li>
</ul>
리액트는 영수
와 철이
를 기존 리스트와 비교하고, 변화가 없기 때문에 변경을 생성하지 않는다. 그리고 숙자
가 새로 추가 된 것을 감지하고, 리스트 트리의 마지막에 숙자
를 추가한다.
여기는 특별히 문제가 없어 보인다.
위 예시와 같은 코드를 보자.
key: 0, children: 영수
key: 1, children: 철이
<ul>
<li>영수</li>
<li>철이</li>
</ul>
여기서 리스트의 맨 앞에 숙자
를 추가해보자.
key: 0, children: 숙자
key: 1, children: 영수
key: 2, children: 철이
<ul>
<li>숙자</li> // key: 0 입장에서, 영수 -> 숙자로 값이 변경 됨.
<li>영수</li> // key: 1 입장에서, 철이 -> 영수로 값이 변경 됨.
<li>철이</li> // key: 2 입장에서, 철이 값이 새로 추가 됨.
</ul>
뭐가 잘못 됐는지 눈치 챘는가? 0
번이 철수
였는데, 숙자
로 바뀌면서 key가 모두 한칸씩 밀리게 되었다. 우리 입장에서야 한칸씩 밀린 것이지, 리액트 입장에서는 리스트 전체가 변경 됐다고 감지하게 된다. key와 children이 각각 모두 다르기 때문이다. 따라서 숙자
만 렌더하면 될 것을, 리스트 전체를 다시 렌더하게 된다. 이는 성능 저하를 야기하게 된다.
위 상황을 올바른 id
값을 부여했을 경우는 어떻게 될까?
key: '영수', children: 영수
key: '철이', children: 철이
<ul>
<li>영수</li>
<li>철이</li>
</ul>
위 리스트에서 마찬가지로 맨 앞에 숙자
를 추가해보자.
key: '숙자', children: 숙자
key: '영수', children: 영수
key: '철이', children: 철이
<ul>
<li>숙자</li> // key: '숙자' 입장에서, 숙자 값이 새로 추가 됨.
<li>영수</li> // key: '영수' 입장에서, 아무런 변화 없음. (렌더되지 않음)
<li>철이</li> // key: '철이' 입장에서, 아무런 변화 없음. (렌더되지 않음)
</ul>
key를 제대로 준 경우에는 새로 추가 된 숙자
만 다시 렌더되기 때문에 성능이 낭비되는 것을 방지할 수 있다.
리스트에 사용자 입력을 입력하는 상황을 만들어보자. 그리고 Add 버튼을 누를 경우 맨 위에 새로운 input을 추가하는 상황이다. 아래 그림과 같이, 사용자가 ID: 3046
입력칸에 영수라는 이름을 입력 해 놓은 상태이다.
다시 말하지만, <맨 위>에 새로운 input을 추가 하는 기능이다.
- Add 버튼을 누르면 맨 위에 새로운 빈 input을 추가한다.
- ID 3046 칸에 사용자가
영수
라고 입력했다.
여기서, Add
를 누르면 리스트의 맨 위에 빈 Input을 추가하게 된다. Add
를 눌러보자.
뭔가 이상하다. 분명 ID: 3046
의 input에 영수라고 써놨었는데, 새로 추가 된 ID: 6470
에 영수가 씌여있다. 왜 이런걸까? Add를 누르기 전과 후의 렌더링 된 데이터는 아래와 같다.
// Add 누르기 전
key: 0, id: 3046, input: '영수'
key: 1, id: 9369, input: ''
key: 2, id: 5789, input: ''
// Add 누른 후
key: 0, id: 6470, input: '영수'
key: 1, id: 3046, input: ''
key: 2, id: 9369, input: ''
key: 3, id: 5789, input: '' // <-- key 3 새로 추가 됨.
리액트는 기존 리스트와 새 리스트를 비교하여 변경된 부분을 업데이트 한다고 위에서 이야기 했다. 우리는 맨 위에 리스트를 한 것으로 의도 했지만, 리액트는 key
를 비교하기 때문에, 0, 1, 2
는 순서가 변한게 없다고 생각한다. 그리고 뒤에 새로운 key: 3
아이템이 추가 됐다고 생각한다.
즉, 새로운 아이템이 리스트 맨 위가 아닌 맨 아래에 추가 된다.
key: 0
아이템 입장에서는, item.id
만 내용이 3046
에서 6470
으로 변화 됐지, 그 외에는 아무런 변화도 없다.
그래서 0, 1, 2
항목에 대하여 변경된 id
값만 새로 바뀐 값으로 업데이트 하게 되고 실제로 영수
가 써 있는 input은 아무런 변화가 없는 것이다.
요약
Before: key
0, 1, 2
After: key0, 1, 2, 3
...3
만 맨 뒤에 추가 됐다.
key={item.id}
값을 부여했을 경우는 어떻게 될까?// Add 누르기 전
key: 3046, id: 3046, input: '영수'
key: 9369, id: 9369, input: ''
key: 5789, id: 5789, input: ''
// Add 누른 후
key: 6470, id: 6470, input: '' // <-- key 6470 새로 추가 됨.
key: 3046, id: 3046, input: '영수'
key: 9369, id: 9369, input: ''
key: 5789, id: 5789, input: ''
이 경우, 리액트가 기존 리스트와 새 리스트를 비교할 때, key: 6470
이 맨 앞에 추가 됐고, 나머지는 변화가 없다는 것을 key
를 통해 감지하고, 우리의 의도 대로 실제로 리스트의 맨 앞에 <li>
를 추가하여 Rerender 되는 것을 확인할 수 있다.
요약
Before: key
3046, 9369, 5789
After: key6470, 3046, 9369, 5789
... 맨 앞에6470
이 추가 됐다.