리액트에서 배열 요소를 순회하며 컴포넌트를 리턴하는 코드가 자주 사용된다. 아래의 예시 코드를 보자
<>
<IssueFilterTab
issues={issues}
setIssues={setIssues}
allChecked={allChecked}
markMode={markMode}
/>
{issues.map(({ title, checked }, idx) => (
<IssueItem
title={title}
checked={checked}
issues={issues}
setIssues={setIssues}
/>
))}
</>
issues라는 배열을 순회하면서 각 원소의 속성들을 IssueItem 컴포넌트로 전달하여 컴포넌트를 생성하는 코드이다. 위와 같이 코드를 작성하면 아래와 같은 경고를 볼 수 있다.
Each child in an array should have a unique “key” prop
각 요소는 key라는 유일한 prop값을 가져야 한다는 경고이다. 물론 key 값을 전달하지 않아도 화면에 렌더링은 잘 된다. 하지만 key가 없다면 리액트는 자식 요소의 변동을 감지하고 렌더링 할 때 비효율적으로 작동하게 된다. 아래의 공식 문서에 나온 예시를 보자.
1. 맨 뒤에 요소를 추가하는 경우
// 처음 상태
<ul>
<li>first</li>
<li>second</li>
</ul>
// 렌더링 후 상태 (마지막에 third 엘리먼트 추가)
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
[공식 문서에 나온 리액트의 자식 요소의 재귀적 처리 방식]
DOM 노드의 자식들을 재귀적으로 처리할 때, React는 기본적으로 동시에 두 리스트를 순회하고 차이점이 있으면 변경을 생성합니다.
위에 코드를 보면, first, second 까지는 동일하고 third를 추가했을 때의 상황이다. 이전 상태의 first와 second까지 동일함을 확인하고, third를 추가하는 방식으로 동작한다.
하지만 이 리스트의 맨 앞에 요소를 추가한다고 생각하면, 매우 비효율적으로 동작한다. 아래의 코드를 보자.
2. 맨 앞에 요소를 추가하는 경우
// 기존
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
// 변경
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
위의 코드를 보면 자식 요소의 맨 앞에 <li>Connecticut</li>를 추가하고 있다. 이때 리액트는 맨 앞 요소를 비교한 후 <li>Duke</li>와 <li>Connecticut</li>가 다르기 때문에 모든 요소를 새로 만든다. 지극히 비효율적이다.
이러한 상황을 방지하기 위해 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값을 가지고 있다. Duke의 앞에 Connecticut이 새로 추가되었지만, 기존의 key값 비교를 통해 2015와 2016 key를 가진 요소들은 그저 이동만 하게 된다. 그리고 2014 키를 가진 Connecticut이 새로 추가된다.
키 값 요소를 추가하기
처음으로 돌아가서 key에 대한 경고를 없애기 위해 이번에는 key값에 배열 원소의 index를 전달해보자.
<>
<IssueFilterTab
issues={issues}
setIssues={setIssues}
allChecked={allChecked}
markMode={markMode}
/>
{issues.map(({ title, checked }, idx) => (
<IssueItem
key={idx}
title={title}
checked={checked}
issues={issues}
setIssues={setIssues}
/>
))}
</>
이제 아무런 에러 없이 잘 동작하는 것을 볼 수 있다. 하지만 key 값을 index로 설정하는 것은 지양해야 하는 패턴이다. 보통의 상황에서는 잘 동작하지만, 삽입 또는 삭제가 발생하는 상황에서 문제가 생길 수 있다. 아래의 예시를 보자.
key값을 index로 준 경우
// 초기 상태
<ul>
<input name="a" key="0" value=""/>
<input name="b" key="1" value=""/>
</ul>
// 맨 앞에 요소를 추가한 뒤
<ul>
<input name="c" key="0" value=""/>
<input name="a" key="1" value=""/>
<input name="b" key="2" value=""/>
</ul>
문제가 되는 상황은 데이터가 삽입, 삭제되는 경우이다. 위의 상황에서 사용자가 name="a"인 input 태그 안에 "하하하하하"라는 텍스트를 입력했다고 가정하자. 그 상황에서 맨 앞에 name="c" 엘리먼트를 추가했을 때, "하하하하하"라는 텍스트는 input name="a"가 아닌 input name="c"에 렌더링 된다.
우리는 "하하하하하"를 맨 처음에 name="a"인 input에 적었는데, 왜 맨 앞에 요소를 추가하면 name="c"에 "하하하하하"가 렌더링이 되는 것일까?
이유는 다음과 같다. key 값을 index로 주고 있기 때문에, 맨 앞에 요소가 추가된 후(name = c 인 요소 추가) 다시 렌더링 할 때 c가 index 0을 갖게 되고, 나머지 요소들은 기존보다 1씩 큰 값을 갖게 된다.
즉, 맨 처음에는 "하하하하하"가 name="a"에 있었지만, 당시의 key값은 0이었기 때문에 다시 렌더링하고 난 후 에도 key값이 0인 엘리먼트한테 "하하하하하"라는 텍스트를 렌더링 하게 되는 것이다.
이런 상황을 방지하기 위해서 key값에 index값을 주는 것을 지양해야 한다. 대신에 데이터의 유일한 값을 key 값으로 전달해야 한다.
위의 설명이 텍스트로 되어있어 명확하지 않을 수 있으니 아래 블로그의 사진을 보면서 이해하면 도움이 된다.
참고
ko.reactjs.org/docs/reconciliation.html#recursing-on-children
medium.com/@robinpokorny/index-as-a-key-is-an-anti-pattern-e0349aece318
'프론트엔드 > react' 카테고리의 다른 글
[react] redux 없이 전역 상태 관리하기 (Context API, useContext, useReducer) (0) | 2020.10.30 |
---|