하나의 컴포넌트를 만든 뒤, 컴포넌트를 여러 번 반복해서 보여주는 목록 화면을 개발한다고 해보자. React에서 권장하는 배열 렌더링 방법은 JavaScript의 map() 함수를 활용하는 것이다. 예를 들면 다음과 같다.
function Expenses() {
return (
<div>
{EXPENSES_MOCK.map((expense) => (
<ExpenseItem
title={expense.title}
amount={expense.amount}
date={expense.date}
/>
))}
</div>
);
}
map() 함수를 통해서 엘리먼트의 모음을 만들고, 이걸 JSX에 포함시키기만 하면 된다. 그런데 이 상태로 실제로 서버를 띄워 실행해보면 다음과 같은 경고를 만날 수 있을 것이다.
직역해보면, 리스트에 존재하는 각각의 자식 컴포넌트들은 고유한 “key” 값을 가지고 있어야 한다는 것이다. 실행은 가능하지만, 냅두기에는 빨간 색 경고라 약간 찜찜하다. 대체 이 “key”란 무엇이며, 왜 있어야 하는 것일까?
Key
리액트 도큐먼트에 의하면, Key는 엘리먼트 리스트를 만들 때 포함해야 하는 특수한 문자열 어트리뷰트다. 이 값은 리액트가 어떤 항목을 변경, 추가 또는 삭제할지 식별하는 것을 돕는다고 한다. 즉 배열이 업데이트되는 과정에서 효율적인 렌더링을 수행하기 위해 Unique한 key를 사용하는 것이다.
다음과 같은 배열이 있다고 해보자.
const array = ['a', 'b', 'c', 'd'];
위 배열을 map() 함수를 사용하여 뷰에 랜더링시킨다면 다음과 같을 것이다.
array.map(item => <div>{item}</div>);
이때 배열의 ‘b’와 ‘c’ 사이에 ‘z’를 삽입한 뒤 리렌더링을 하게 된다면 어떻게 될까? 먼저 b와 c 사이 인덱스를 z가 찾아낼 것이다. 이후 c가 z로 바뀌고, d는 c로 바뀌고, 마지막에 d가 새로 생기게 된다. 즉, 새롭게 추가된 부분 다음의 배열 요소에 O(n)만큼 영향을 주는 것이다.
반대로 a, b, z, c, d에서 가장 앞인 a를 제거하려고 하면, 기존의 a는 b, b → z, z → c, c → d, 그리고 맨 마지막에 있는 d는 제거된다. 리스트 요소의 삭제 또한 삽입과 동일하게 변경된 리스트 요소 이하에 O(n) 만큼의 영향을 끼친다.
그렇다면 key가 존재할 경우 변경에 최적화를 할 수 있을까?
다음과 같은 고유한 id값을 지닌 배열이 있다고 해보자.
const array = [
{
id: 0,
text: 'a',
},
{
id: 1,
text: 'b',
},
{
id: 2,
text: 'c',
},
{
id: 3,
text: 'd',
},
];
위 배열의 id를 key로 사용하여 뷰에 렌더링시켜보면 다음과 같은 형태일 것이다.
array.map(item => <div key={item.id}>{item.text}</div>);
React에서는 key를 통해 기존 트리와 이후 트리의 자식들이 일치하는지 확인하기 때문에, 트리 변환 작업이 효율적으로 수행될 수 있다. 위의 배열 중간에 { id: 5, text: ‘z’ } 인 요소가 추가된다고 하더라도, 변경되지 않은 값들은 그대로 두고 해당 요소를 삽입할 수 있다. (삭제도 마찬가지다!)
React는 효율적인 재조정(Reconciliation) 프로세스를 가지고 있기 때문에, 배열 렌더링 시 되도록이면 key를 고유하게 넣어주는 것이 좋다. 앞서 퍼포먼스 측면의 이점을 소개했지만, 사실 고유한 키를 부여한다는 것은 각 요소의 고유성을 부여한다는 의미이기도 하다. 이 점이 퍼포먼스상 이점을 가져올 수 있는 것이긴 하지만 말이다.
Key에 넣을 게 없는데...어떻게 사용하지?
배열 내에 해당 항목을 고유하게 구별할 수 있는 식별자 (ex. ID)가 있다면 당연히 그걸 사용하는 것이 베스트다. 그러나 렌더링한 항목에 대한 안정적인 ID가 없을 수도 있다. 이때 최후의 수단으로 인덱스를 key로 넣을 수도 있다.
array.map(item, index => <div key={index}>{item}</div>);
그런데 이건 권장되지 않는 방법이다. (무려 lint 에러 감이다!) 엘리먼트를 고유하지 않게 만들 수 있고, 배열 변경(정렬, 요소 추가 및 삭제) 시 각각의 아이템 인덱스 또한 재배치하게 만들기 때문이다. 이는 결국 불필요한 렌더링 연산을 유발하게 된다. key는 리액트로 하여금 DOM elements를 식별하게 만드는 유일한 값이기 때문에, 인덱스를 key로 사용하는 경우는 신중히 고려되어야 한다.
그렇다면 인덱스를 key로 사용하는 것보다 조금 더 나은 방법은 없을까? 사실 리액트에는 key를 고민하기 위해 나온 여러 라이브러리들이 존재한다. 고유한 키값을 생성해주는 제너레이터를 사용하는 것도 고려해볼 수 있겠다. nanoid를 사용한 예를 들면 다음과 같다.
import { nanoid } from 'nanoid';
const createNewTodo = (text) => ({
completed: false,
id: nanoid(),
text
}
라이브러리를 사용하고 싶지 않다면, 다음과 같이 자바스크립트 내장 함수인 Math.random() 을 사용하여 난수 아이디를 생성하는 방법도 있겠다.
const createNewTodo = (text) => ({
completed: false,
id: Math.random().toString(),
text
}
요약
- 배열 랜더링 시 key 반드시 입력하자.
- key값은 고유할 수록 좋다.
Reference
https://ko.reactjs.org/docs/lists-and-keys.html
https://tecoble.techcourse.co.kr/post/2021-04-25-react-key/
https://github.com/facebook/react/issues/1342#issuecomment-39230939
https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-array-index-key.md
'Web > React' 카테고리의 다른 글
리액트로 만들어보는 마우스 드래그 좌우스크롤 컴포넌트 (1) | 2024.01.21 |
---|---|
[React] controlled vs. uncontrolled input (2) | 2023.05.11 |