React에서 목록을 렌더링하려면 배열에 map()을 호출하고 항목마다 JSX 조각을 반환하세요. 목록이 바뀔 때 React가 항목들을 구별할 수 있도록, 각 항목의 가장 바깥 요소에 그 항목을 식별하는 값(보통 ID)으로 key prop을 줍니다.
배열에 { id: 'dat', name: 'Date', price: 4 }를 추가하면 네 번째 줄이 나타납니다. JSX에는 자체 반복 문법이 없습니다. map이 요소의 배열을 반환하고, React는 배열의 각 요소를 순서대로 렌더링해서 배열을 렌더링합니다.
map으로 배열 렌더링하기
map은 항목마다 함수를 한 번씩 호출하고 결과를 새 배열에 모읍니다. JSX 중괄호 안에서 그 배열은 그냥 값이므로, 위처럼 마크업 안에서 바로 map하거나 배열을 먼저 만들어도 됩니다.
const items = fruits.map((fruit) => <li key={fruit.id}>{fruit.name}</li>);
return <ul>{items}</ul>;
사람들이 자주 걸려 넘어지는 세부 사항이 두 가지 있습니다. 화살표 함수에 중괄호를 쓰면 return이 필요합니다: fruits.map((f) => { return <li key={f.id}>{f.name}</li>; }). 없으면 함수가 undefined를 반환하고 목록이 비게 됩니다. 그리고 key는 map에서 반환하는 요소에 붙여야 하며, 그 안쪽 요소에 붙이면 안 됩니다. 항목이 컴포넌트라면 컴포넌트에 key를 붙이세요: <FruitRow key={fruit.id} fruit={fruit} />.
filter 후 map
일부 항목만 보여 주려면 먼저 배열을 filter하고 그 결과를 map하세요. 각 메서드가 한 가지 일만 하므로, 체인이 "조건에 맞는 과일들을 목록 항목으로"라는 문장처럼 읽힙니다.
검색창에 er을 입력하면 목록이 Eraser, Ruler, Stapler로 좁혀지고, In stock only를 체크하면 Ruler도 빠집니다. 필터링된 목록은 렌더링 중에 두 상태로부터 계산되므로, 동기화를 유지해야 할 두 번째 배열을 상태에 둘 필요가 없습니다. 비교 결과가 진짜 불리언이므로 visible.length === 0 &&는 &&를 안전하게 쓰는 예입니다(맨 0을 쓰면 어떻게 되는지는 조건부 렌더링 페이지에서 보여 줍니다).
key가 하는 일
목록이 다시 렌더링되면 React는 새 요소 배열을 이전 배열과 비교합니다. key는 그 둘을 짝짓는 수단입니다. 지금 key가 3인 요소는 이전에 key가 3이던 요소이며, 위치가 바뀌었더라도 마찬가지입니다. React는 그 항목의 DOM 노드와 컴포넌트 상태를 유지하고, 바뀐 것만 옮기거나 업데이트합니다. 새 key를 가진 항목은 생성되고, 사라진 key는 제거됩니다.
key가 없으면 React가 짝지을 수 있는 기준은 위치뿐입니다. 아무것도 움직이지 않을 때는 괜찮지만, 무언가 움직이는 순간 틀리게 됩니다.
인덱스 key가 깨지는 이유
map은 두 번째 인자로 인덱스를 넘기므로 key={index}를 쓰고 싶어집니다. 하지만 인덱스는 항목이 아니라 위치를 나타냅니다. 맨 위에 항목을 삽입하면 모든 항목의 인덱스가 하나씩 밀리므로, React는 0번 항목이 여전히 0번 항목이라고 믿고 그 자리에 이전 상태를 유지합니다.
아래 데모는 같은 목록을 두 번 렌더링합니다. 한 번은 인덱스를 key로, 한 번은 ID를 key로 씁니다. 각 행에는 자기 텍스트를 유지하는 입력창이 있습니다.
두 목록 모두에서 Ada 옆에 math를 입력한 다음 Add to top을 클릭해 보세요. ID 목록에서는 메모가 Ada에게 남아 있습니다. 인덱스 목록에서는 메모가 새 사람에게로 옮겨 갑니다. 첫 번째 입력창은 여전히 0번 위치의 입력창이고, 0번 위치는 이제 다른 사람이기 때문입니다. 이름은 데이터에서 오므로 두 목록 모두 올바릅니다. 잘못되는 것은 DOM과 컴포넌트 안에 사는 상태뿐입니다. 실제 앱에서는 체크박스, 포커스, 애니메이션, 그리고 useState를 가진 모든 컴포넌트에서 이 문제가 생깁니다.
인덱스 key가 안전한 것은 다음 조건이 모두 성립할 때뿐입니다. 목록이 재정렬되거나 필터링되지 않고, 끝 이외의 위치에서 항목이 삽입되거나 제거되지 않으며, 항목에 자기만의 상태가 없을 때입니다. 푸터 링크의 정적 목록은 해당됩니다. 사용자가 편집할 수 있는 것은 대개 해당되지 않습니다.
key는 어디서 가져올까
가장 좋은 key는 이미 데이터에 있는 ID입니다. 데이터베이스 ID, 상품 SKU, 슬러그, 사용자 이름 같은 것들입니다. 서버에서 온 데이터에는 거의 항상 있습니다.
브라우저에서 만든 항목이라면 항목을 만들 때 ID를 주고 항목과 함께 저장하세요. 위 데모처럼 카운터를 쓰거나 crypto.randomUUID()를 씁니다. key를 렌더링 중에 생성하면 절대 안 됩니다.
// Wrong: a new key on every render, so React remounts every item every time
{todos.map((todo) => <Todo key={Math.random()} todo={todo} />)}
// Right: the ID is created once, in the event that adds the item
function addTodo(text) {
setTodos([...todos, { id: crypto.randomUUID(), text }]);
}
렌더링할 때마다 바뀌는 key는 인덱스보다 나쁩니다. 매번 모든 항목이 파괴되고 다시 만들어지며, 입력하는 동안 입력창이 포커스를 잃고, 어떤 것도 상태를 유지하지 못합니다. 같은 기법을 일부러 쓰면 유용합니다. 컴포넌트의 key를 바꾸면 컴포넌트가 초기화되며, useState 페이지에서 이를 이용해 폼을 비웁니다.
key는 형제 사이에서만 고유하면 됩니다
key는 앱 전체가 아니라 한 목록 안에서 고유해야 합니다. React는 같은 부모의 자식들 사이에서만 key를 비교하므로, 서로 다른 두 목록은 같은 ID를 다시 써도 됩니다.
두 섹션 모두 key가 1과 2인 요리를 가지고 있으며, 이것은 올바릅니다. 각 ul은 별도의 목록입니다. 문제는 한 목록 안의 중복 key입니다. React가 겹치는 항목들을 구별할 수 없어서, 목록이 업데이트될 때 항목을 중복시키거나 빠뜨릴 수 있습니다.
한 항목이 <dt>와 <dd>처럼 여러 형제 요소를 렌더링할 때는 <Fragment key={item.id}>로 감싸세요. 짧은 <> 문법은 key를 받을 수 없습니다.
"unique key prop" 경고
key를 빠뜨리면 React는 개발 중에 브라우저 콘솔에 다음을 출력합니다.
Each child in a list should have a unique "key" prop.
Check the render method of `App`.
두 번째 줄은 목록을 만든 렌더링의 컴포넌트 이름을 알려 주므로 어느 map을 고쳐야 할지 알 수 있습니다. 같은 key를 가진 형제 두 개는 "Encountered two children with the same key"로 시작하는 다른 메시지를 받습니다. 이 페이지의 미리보기는 그 검사를 건너뛰는 프로덕션 빌드처럼 실행되므로, key를 지워도 여기서는 경고가 보이지 않습니다. 그래도 첫 번째 예제에서 하나를 지워 보세요. key가 없는 것은 충돌이 아니라 정확성의 위험이므로 목록은 여전히 렌더링됩니다. 경고를 고치려면 map에서 반환하는 요소에 안정적인 key를 추가하세요. key={index}를 추가하면 경고는 사라지지만 위에서 본 버그가 돌아옵니다.
또한 key는 컴포넌트에 전달되지 않는다는 점에 주의하세요. FruitRow 안에서 props.key는 undefined입니다. 컴포넌트에 ID가 필요하면 일반 prop으로 한 번 더 넘기세요: <FruitRow key={f.id} id={f.id} />.
자주 묻는 질문
React는 왜 목록 항목에 key가 필요한가요?
key는 렌더링 사이에 어느 항목이 어느 항목인지 React에 알려 줍니다. 배열이 바뀌면 React는 key로 이전 항목과 새 항목을 짝지으므로, 위치로 추측하지 않고 각 항목의 상태와 DOM 노드를 올바른 데이터와 함께 유지할 수 있습니다.
배열 인덱스를 key로 써도 되나요?
순서가 절대 바뀌지 않고 중간에 항목이 삽입되거나 제거되지 않는 목록에서만 괜찮습니다. 항목이 움직일 수 있다면 인덱스 key는 상태를 위치에 묶어 버리므로, 입력한 텍스트, 체크박스, 포커스가 엉뚱한 항목에 붙게 됩니다.
key로는 무엇을 써야 하나요?
데이터에 속한 ID입니다. 데이터베이스 ID, 상품 SKU, 사용자 이름 같은 것들입니다. 브라우저에서 만든 항목이라면 항목을 만들 때 ID를 한 번 생성하세요(카운터나 crypto.randomUUID()). 렌더링 중에는 절대 생성하지 마세요.
key는 앱 전체에서 고유해야 하나요?
아니요. key는 같은 목록 안의 형제 사이에서만 고유하면 됩니다. 서로 다른 두 목록은 같은 key를 써도 됩니다.
컴포넌트가 자기 key를 읽을 수 있나요?
아니요. key는 React가 사용하며 컴포넌트에 prop으로 전달되지 않습니다. 컴포넌트에 ID가 필요하면 id={item.id}처럼 다른 이름으로 한 번 더 넘기세요.
"Each child in a list should have a unique key prop" 경고는 어떻게 고치나요?
map에서 반환하는 가장 바깥 요소에 항목의 안정적인 ID로 key를 추가하세요. 그 요소가 프래그먼트라면 <>는 key를 받을 수 없으므로 <Fragment key={id}>를 사용하세요.