리스트와 키
배열을 map으로 컴포넌트 목록으로 변환하고 안정적인 key가 항목의 추가·삭제·재정렬을 식별하는 원리를 이해합니다.
앞 글에서는 조건부 렌더링으로 조건에 따라 UI를 바꾸는 방법을 배웠습니다. 이 글에서는 여러 개의 데이터를 화면에 나열하는 리스트(list) 렌더링과, React에서 리스트를 렌더링할 때 꼭 필요한 key 속성을 알아봅니다. 게시판의 게시물 목록, 쇼핑몰의 상품 목록, 댓글 목록처럼 대부분의 웹 애플리케이션은 비슷한 데이터 여러 개를 목록으로 보여 주며, React는 이런 리스트를 효율적으로 렌더링하는 방법을 제공합니다.
리스트 key와 reconciliation — 상태 추적
재정렬 전후에 key 1/2/3의 위치 변화와 유지되는 컴포넌트 정체성을 구별한다. 같은 key와 타입 유지·index key 반례를 실제 항목 정체성으로 비교할 가치가 있다.
KEYS: ITEM IDENTITY
안정적인 key는 순서가 바뀌어도 같은 항목을 연결합니다
PostList의 post.id를 map이 반환하는 PostItem에 지정합니다. 같은 부모 아래 key와 컴포넌트 타입이 같으면 항목의 state를 이어받으며, props가 바뀌면 UI도 갱신됩니다.
ID로 연결 · key 2가 이동
전: [1, 2, 3]
후: [3, 1, 2]
key 2: 두 번째 → 세 번째교차선은 이전·다음 목록에서 같은 key의 대응입니다. 이동 전후 항목의 state는 같은 게시물에 연결됩니다.
map이 반환하는 바깥 요소
posts.map(post => (
<PostItem key={post.id}
post={post} />
))PostItem 안 li에만 key를 두는 것으로 이 배열의 형제를 식별할 수는 없습니다. key는 컴포넌트의 일반 prop으로 전달되지 않습니다.
인덱스 / 랜덤 key의 차이
인덱스는 위치를 나타내므로 삽입·삭제·정렬 시 state가 다른 데이터에 연결될 수 있습니다. 매 렌더마다 랜덤 key를 만들면 항목을 새로 만들어 state를 잃습니다.
ID의 범위와 수명
같은 부모 아래 형제끼리 고유하면 됩니다. ID가 없다면 데이터 생성 시 한 번 부여해 저장합니다. 정체성 유지가 재렌더나 DOM 갱신을 모두 막는 것은 아닙니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 변경 전 1·2·3과 재정렬 후 3·1·2의 같은 key를 세 개의 대응선으로 연결한다. 같은 부모 아래 컴포넌트 종류와 key가 같으면 정체성을 유지한다. 선의 교차는 병합이나 실행 순서가 아니다. 모바일은 key별 세 개의 독립 대응을 전후 위치 문구와 함께 위아래로 재배치한다. |
| 전체 관계·조건 설명 | 변경 전 1·2·3과 재정렬 후 3·1·2의 같은 key를 세 개의 대응선으로 연결한다. 같은 부모 아래 컴포넌트 종류와 key가 같으면 정체성을 유지한다. 선의 교차는 병합이나 실행 순서가 아니다. 모바일은 key별 세 개의 독립 대응을 전후 위치 문구와 함께 위아래로 재배치한다. 모바일은 같은 노드와 방향을 다른 배치로 보여 줍니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 변경 전 항목 | 순서: 1,2,3 / 정체성: key+같은 종류 | key 1/2/3이 순서 1/2/3에서 각 정체성을 갖는다 | key는 같은 부모의 형제 항목 기준이다 |
| 재정렬 | 순서: 3,1,2 / 항목 state: 안정 key에 연결 | 데이터 순서를 3/1/2로 바꾼다 | 위치 이동과 새 항목 생성은 다르다 |
| 다음 대응 | key3: 앞 위치로 이동 / key1/2: 각 정체성 유지 | 같은 종류/key의 각 항목이 자기 state를 유지한다 | index key 반례는 별도 모델이며 key를 자식 prop으로 읽지 않는다 |
리스트 key와 reconciliation — 상태 추적
재정렬 전후에 key 1/2/3의 위치 변화와 유지되는 컴포넌트 정체성을 구별한다. 같은 key와 타입 유지·index key 반례를 실제 항목 정체성으로 비교할 가치가 있다.
KEYS: ITEM IDENTITY
안정적인 key는 순서가 바뀌어도 같은 항목을 연결합니다
PostList의 post.id를 map이 반환하는 PostItem에 지정합니다. 같은 부모 아래 key와 컴포넌트 타입이 같으면 항목의 state를 이어받으며, props가 바뀌면 UI도 갱신됩니다.
ID로 연결 · key 2가 이동
전: [1, 2, 3]
후: [3, 1, 2]
key 2: 두 번째 → 세 번째교차선은 이전·다음 목록에서 같은 key의 대응입니다. 이동 전후 항목의 state는 같은 게시물에 연결됩니다.
map이 반환하는 바깥 요소
posts.map(post => (
<PostItem key={post.id}
post={post} />
))PostItem 안 li에만 key를 두는 것으로 이 배열의 형제를 식별할 수는 없습니다. key는 컴포넌트의 일반 prop으로 전달되지 않습니다.
인덱스 / 랜덤 key의 차이
인덱스는 위치를 나타내므로 삽입·삭제·정렬 시 state가 다른 데이터에 연결될 수 있습니다. 매 렌더마다 랜덤 key를 만들면 항목을 새로 만들어 state를 잃습니다.
ID의 범위와 수명
같은 부모 아래 형제끼리 고유하면 됩니다. ID가 없다면 데이터 생성 시 한 번 부여해 저장합니다. 정체성 유지가 재렌더나 DOM 갱신을 모두 막는 것은 아닙니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 변경 전 1·2·3과 재정렬 후 3·1·2의 같은 key를 세 개의 대응선으로 연결한다. 같은 부모 아래 컴포넌트 종류와 key가 같으면 정체성을 유지한다. 선의 교차는 병합이나 실행 순서가 아니다. 모바일은 key별 세 개의 독립 대응을 전후 위치 문구와 함께 위아래로 재배치한다. |
| 전체 관계·조건 설명 | 변경 전 1·2·3과 재정렬 후 3·1·2의 같은 key를 세 개의 대응선으로 연결한다. 같은 부모 아래 컴포넌트 종류와 key가 같으면 정체성을 유지한다. 선의 교차는 병합이나 실행 순서가 아니다. 모바일은 key별 세 개의 독립 대응을 전후 위치 문구와 함께 위아래로 재배치한다. 모바일은 같은 노드와 방향을 다른 배치로 보여 줍니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 변경 전 항목 | 순서: 1,2,3 / 정체성: key+같은 종류 | key 1/2/3이 순서 1/2/3에서 각 정체성을 갖는다 | key는 같은 부모의 형제 항목 기준이다 |
| 재정렬 | 순서: 3,1,2 / 항목 state: 안정 key에 연결 | 데이터 순서를 3/1/2로 바꾼다 | 위치 이동과 새 항목 생성은 다르다 |
| 다음 대응 | key3: 앞 위치로 이동 / key1/2: 각 정체성 유지 | 같은 종류/key의 각 항목이 자기 state를 유지한다 | index key 반례는 별도 모델이며 key를 자식 prop으로 읽지 않는다 |
위 그림은 안정적인 key가 목록의 순서가 바뀌어도 같은 항목을 이어 준다는 점을 보여 줍니다. 이 글의 PostList처럼 map이 돌려주는 PostItem에 post.id를 key로 주면, 변경 전 [1, 2, 3]이던 목록이 재정렬 후 [3, 1, 2]가 되어도 React는 같은 key끼리 짝을 지어 각 게시물의 정체성을 유지합니다. 예를 들어 key 2는 두 번째에서 세 번째로 옮겨 가지만 같은 게시물로 이어집니다. 그림의 교차선은 이전 목록과 다음 목록에서 같은 key의 대응을 나타낼 뿐, 항목이 합쳐지거나 실행되는 순서를 뜻하지는 않습니다.
그림의 설명처럼 같은 부모 아래에서 key와 컴포넌트의 종류가 같으면 그 항목의 state를 이어받고, props가 바뀌었다면 UI도 갱신됩니다. 그림 아래의 세 칸은 이 글의 뒤에서 다룰 주의점을 미리 정리합니다. key는 map이 돌려주는 가장 바깥 요소에 두어야 하며, PostItem 안의 li에만 두어서는 이 배열의 형제를 구별할 수 없고, key는 컴포넌트에 일반 prop으로 전달되지 않습니다. 인덱스는 위치를 나타내므로 삽입·삭제·정렬 때 state가 다른 데이터에 붙을 수 있고, 렌더마다 무작위 key를 만들면 항목을 새로 만들어 state를 잃습니다. 또 key는 같은 부모 아래의 형제끼리만 고유하면 되며, 정체성이 유지된다고 다시 렌더링이나 DOM 갱신이 모두 생략되는 것은 아닙니다.
map() 함수를 이용한 리스트 렌더링
React에서 리스트를 렌더링하는 가장 일반적인 방법은 자바스크립트 배열의 map() 메서드를 쓰는 것입니다. map()은 배열의 각 요소에 대해 주어진 함수를 호출하고, 그 반환값으로 이루어진 새 배열을 만듭니다. React는 이 새 배열에 담긴 JSX 요소를 화면에 렌더링합니다. 간단한 숫자 목록부터 만들어 봅니다. src/components 폴더 안에 NumberList.jsx 파일을 만듭니다.
function NumberList(props) {
const numbers = [1, 2, 3, 4, 5]; // 렌더링할 숫자 배열
// (1) map() 함수를 사용하여 각 숫자를 <li> JSX 요소로 변환
const listItems = numbers.map((number) =>
<li>{number}</li> // (2) 경고: 여기에 key 속성이 없습니다! (잠시 후에 추가 예정)
);
return (
<div style={{ border: '1px solid #6c757d', padding: '20px', margin: '20px', borderRadius: '8px' }}>
<h2>숫자 목록</h2>
<ul>{listItems}</ul> {/* (3) JSX 내부에서 배열 렌더링 */}
</div>
);
}
export default NumberList;numbers.map((number) => <li>{number}</li>)는 numbers 배열의 각 number마다 <li>{number}</li>라는 JSX 요소를 돌려줘, 새 배열 listItems를 만듭니다. 그리고 JSX 안에서 {listItems}로 이 배열을 그대로 렌더링하면, React가 배열 안의 JSX 요소를 펼쳐서 나열합니다. 이제 App.jsx에서 NumberList를 씁니다.
import './App.css';
import NumberList from './components/NumberList'; // NumberList 컴포넌트 불러오기
function App() {
return (
<div className="App">
<h1>React 리스트 렌더링</h1>
<NumberList />
</div>
);
}
export default App;브라우저에서는 1부터 5까지의 숫자가 목록으로 나타납니다. 하지만 개발자 도구의 Console을 보면, React 19의 개발 모드에서 다음과 같이 시작하는 오류 메시지를 볼 수 있습니다.
Each child in a list should have a unique "key" prop.이것이 key 속성에 대한 경고입니다. 이제 key가 왜 필요하고 어떻게 쓰는지 알아봅니다.
key의 필요성
React에서 리스트를 렌더링할 때는 각 항목에 고유한 key를 주어야 합니다. key는 React가 리스트 안에서 어떤 항목이 바뀌었는지, 추가되었는지, 삭제되었는지 구별하는 데 쓰는 특별한 속성입니다.
React는 DOM을 효율적으로 갱신하기 위해 재조정(reconciliation)이라는 과정을 거칩니다. 새 UI 트리를 이전 UI 트리와 비교해 바뀐 부분을 찾고, 최소한의 DOM 조작으로 화면을 갱신하는 과정입니다. 리스트에 key가 없으면 React는 순서가 바뀌거나 새 항목이 추가될 때, 그것이 재정렬된 항목인지 완전히 새로운 항목인지 구별하기 어렵습니다. 예를 들어 중간에 항목을 넣거나 빼는데 안정적인 key가 없다면, React가 위치를 기준으로 이전 항목을 이어받아 기존 state가 다른 데이터의 항목에 붙을 수 있습니다.
반면 key가 있다면 React는 각 항목을 key로 구별해, 같은 부모 아래에서 key와 종류가 일치하는 항목의 정체성을 유지하면서 바뀐 props에 따라 UI를 갱신합니다. 즉 key는 React가 재조정을 바르게 하도록 돕는 힌트(hint)입니다.
key가 없으면 두 가지 문제가 생길 수 있습니다. 하나는 성능으로, 불필요하게 다시 만들거나 고치는 DOM 작업 때문에 애플리케이션이 느려질 수 있습니다. 더 큰 문제는 컴포넌트 안의 상태입니다. 리스트 항목 컴포넌트가 자기 state를 가지고 있다면, key가 없을 때 항목의 순서가 바뀌거나 삭제되면 state가 엉뚱한 항목에 붙거나 초기화되는 예측하기 어려운 버그가 생깁니다. 체크박스 목록에서 한 항목의 체크만 풀려야 하는데 다른 항목의 체크가 풀리는 현상이 그 예입니다.
더 깊이: 트리 비교를 O(n)으로 줄이는 두 가지 가정
두 트리의 차이를 최소한의 변경으로 구하는 일반적인 알고리즘은 원소 수 n에 대해 O 표기법으로 나타내면 O(n³) 정도의 시간이 걸립니다. React의 이전 공식 문서는 이 방식이라면 원소 1,000개를 보여 주는 데 10억 번 정도의 비교가 필요하다고 설명하고, React는 그 대신 두 가지 가정에 기댄 O(n) 휴리스틱(heuristic)을 쓴다고 밝힙니다(React의 이전 문서, 재조정). 첫째는 타입이 다른 두 element는 서로 다른 트리를 만든다는 가정이고, 둘째는 렌더가 거듭되어도 유지되는 자식을 개발자가 key로 알려 준다는 가정입니다. 그래서 React는 타입이 바뀐 자리의 하위 트리는 비교하지 않고 새로 만들며, 자식 목록에서는 key로 짝을 찾습니다.
key 없이 목록의 맨 앞에 항목을 넣는 경우가 이 가정이 도움이 되지 않는 대표적인 예입니다. React는 자식을 순서대로 짝지어 비교하므로, 내용이 그대로인 뒤쪽 항목까지 모두 바뀐 것으로 보고 하나씩 고칩니다. 다만 같은 문서가 강조하듯 이 비교 방식은 구현의 세부 사항입니다. 앱이 기대어도 되는 것은 몇 번 비교하는지가 아니라, 같은 자리의 같은 타입과 같은 key가 같은 컴포넌트로 이어진다는 결과입니다.
key 속성 올바르게 사용하기
key는 리스트의 각 항목을 고유하게 구별할 수 있는 값이어야 합니다. 같은 목록의 형제 항목끼리만 겹치지 않으면 되므로, 서로 다른 목록이라면 같은 값을 써도 됩니다. 그리고 같은 항목은 렌더링이 거듭되어도 언제나 같은 key를 받아야, React가 이전 렌더링의 항목과 짝을 맞출 수 있습니다. 아래에서는 데이터의 ID를 쓰는 방법과, ID가 없을 때 인덱스를 쓰는 방법의 조건을 차례로 봅니다.
고유한 ID 사용 (가장 권장)
가장 좋은 key는 데이터 자체가 가진 고유한 ID입니다. 데이터베이스에서 가져온 데이터라면 id 필드를 key로 쓰는 것이 일반적입니다. 게시물 목록을 고유 ID로 렌더링해 봅니다. src/components 폴더 안에 PostList.jsx 파일을 만듭니다.
function PostItem({ post }) { // 각 게시물 데이터를 props로 받음
return (
<li style={{ border: '1px solid #ccc', padding: '10px', margin: '10px 0', borderRadius: '5px', backgroundColor: '#f9f9f9' }}>
<h4>{post.title}</h4>
<p>{post.content}</p>
</li>
);
}
function PostList() {
const posts = [
{ id: 1, title: '리액트 시작하기', content: '리액트의 기본을 배워봅시다.' },
{ id: 2, title: '컴포넌트의 세계', content: '재사용 가능한 UI를 만들어요.' },
{ id: 3, title: '상태 관리의 중요성', content: '데이터가 변경될 때 UI 업데이트!' },
];
return (
<div style={{ border: '1px solid #007bff', padding: '20px', margin: '20px', borderRadius: '8px' }}>
<h2>최신 게시물</h2>
<ul>
{/* map() 메서드를 사용하여 PostItem 컴포넌트 렌더링 */}
{posts.map(post => (
<PostItem key={post.id} post={post} /> // (1) key={post.id}로 고유 ID를 key로 지정
))}
</ul>
</div>
);
}
export default PostList;각 post 객체가 고유한 id를 가지고 있으므로 key={post.id}로 key를 주었습니다. key는 map() 안에서 JSX 요소를 돌려주는 자리에 두어야 하므로, 이 예제에서는 <li> 태그가 아니라 PostItem 컴포넌트에 key를 주었습니다. 이제 App.jsx에서 PostList를 씁니다.
import './App.css';
import PostList from './components/PostList'; // PostList 컴포넌트 불러오기
function App() {
return (
<div className="App">
<h1>React 리스트 렌더링 (Key 적용)</h1>
<PostList />
</div>
);
}
export default App;이제 Console에 key 경고가 나타나지 않고, 세 게시물의 제목과 내용이 목록으로 보입니다.
인덱스를 key로 사용하기 (주의 필요!)
데이터에 고유한 ID가 없다면 map()의 두 번째 인자인 index(배열의 인덱스)를 key로 쓸 수도 있습니다. map()의 콜백은 항목과 인덱스를 차례로 받으므로, 아래 코드처럼 두 번째 매개변수를 하나 더 적으면 0부터 시작하는 위치 번호를 얻습니다. 이 예제의 숫자 배열은 코드에 고정되어 순서가 바뀌지 않으므로, 인덱스 key로도 화면은 올바르게 나옵니다. 이 결과를 보려면 App.jsx에 import NumberList from './components/NumberList';를 다시 더하고 <PostList /> 아래에 <NumberList />를 렌더링합니다.
function NumberList(props) {
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number, index) => // (1) map의 두 번째 인자로 index 받기
<li key={index}>{number}</li> // (2) key={index}로 지정
);
return (
<div style={{ border: '1px solid #6c757d', padding: '20px', margin: '20px', borderRadius: '8px' }}>
<h2>숫자 목록 (Key: Index)</h2>
<ul>{listItems}</ul>
</div>
);
}
export default NumberList;하지만 인덱스를 key로 쓰는 것은 다음 조건을 모두 만족할 때만 안전합니다. 리스트를 정렬하거나 중간에 항목을 넣거나 빼지 않아 항목의 순서가 바뀌지 않아야 하고, 필터링으로 특정 항목이 빠지지 않아야 하며, 각 항목 컴포넌트가 자기만의 state를 가지지 않아야 합니다. 이 가운데 하나라도 맞지 않으면 인덱스 key는 예측하기 어려운 버그나 성능 문제를 일으킬 수 있으므로, 가능하면 언제나 데이터의 고유 ID를 key로 쓰기를 권합니다.
고유 ID가 없다면 데이터를 만들 때 ID를 한 번 부여해 항목과 함께 보관합니다. 렌더할 때마다 새 ID를 만들면 그림에서 본 안정적인 연결이 끊어집니다.
컴포넌트로 리스트 아이템 분리하기
리스트를 렌더링할 때 각 항목을 별도의 컴포넌트로 나누는 것은 좋은 습관입니다. 코드를 읽기 쉽고, 재사용하기 좋고, 고치기도 쉬워집니다. 앞의 PostList.jsx에서 PostItem 컴포넌트를 나눠 쓴 것이 그 예입니다.
// 각 게시물 아이템을 위한 별도의 컴포넌트
function PostItem({ post }) { // PostItem 컴포넌트는 post 객체를 props로 받습니다.
return (
<li style={{ border: '1px solid #ccc', padding: '10px', margin: '10px 0', borderRadius: '5px', backgroundColor: '#f9f9f9' }}>
<h4>{post.title}</h4>
<p>{post.content}</p>
</li>
);
}
function PostList() {
const posts = [
{ id: 1, title: '리액트 시작하기', content: '리액트의 기본을 배워봅시다.' },
{ id: 2, title: '컴포넌트의 세계', content: '재사용 가능한 UI를 만들어요.' },
{ id: 3, title: '상태 관리의 중요성', content: '데이터가 변경될 때 UI 업데이트!' },
];
return (
<div style={{ border: '1px solid #007bff', padding: '20px', margin: '20px', borderRadius: '8px' }}>
<h2>최신 게시물</h2>
<ul>
{posts.map(post => (
// key는 map 함수 안에서 배열의 각 요소를 렌더링하는 가장 바깥쪽 엘리먼트에 부여
<PostItem key={post.id} post={post} />
))}
</ul>
</div>
);
}
export default PostList;이렇게 나누면 PostList는 자기가 선언한 posts 배열을 순회하며 PostItem들을 렌더링하는 일을 맡고, 각 PostItem은 넘겨받은 post 데이터만으로 독립적으로 렌더링됩니다. 이는 React가 지향하는 컴포넌트 중심 개발의 방식과도 잘 맞습니다.
정렬 후에도 입력이 같은 항목에 남는지 확인하기
제목만 보여 주는 원래 게시물 예제에서는 항목에 state가 없어 key의 차이를 눈으로 확인하기 어렵습니다. 다음은 각 행이 작성 중인 메모를 state로 가진 별도의 실습입니다. src/components/KeyPractice.jsx에 저장하고 App.jsx에서 <KeyPractice />를 렌더링합니다.
import { useState } from 'react';
function EditablePost({ post }) {
const [draft, setDraft] = useState('');
return (
<li>
<label>
{post.title}: 메모
<input value={draft} onChange={event => setDraft(event.target.value)} />
</label>
</li>
);
}
export default function KeyPractice() {
const [posts, setPosts] = useState([
{ id: 1, title: '기초' },
{ id: 2, title: '응용' },
]);
return (
<section>
<button onClick={() => setPosts(current => [...current].reverse())}>
순서 뒤집기
</button>
<ul>
{posts.map(post => <EditablePost key={post.id} post={post} />)}
</ul>
</section>
);
}기초 행에 “내 메모”를 입력하고 순서 뒤집기를 누릅니다. 같은 부모 아래에서 같은 컴포넌트 종류와 ID 1의 key를 가진 행의 정체성이 유지되므로, 메모는 순서가 바뀐 뒤에도 기초 행 옆에 남아야 합니다. 다만 key는 정렬 전후의 같은 항목을 이어 주는 기준일 뿐, 그 행의 렌더 함수가 다시 실행되지 않는다는 보장은 아닙니다. 같은 형제 목록 안에서 key가 고유해야 하고 항목이 있는 동안 바뀌지 않아야 한다는 규칙은 React 문서의 key 규칙에 나옵니다.
정리
map()으로 데이터 배열을 JSX 요소의 배열로 바꾸면, React가 그 배열을 펼쳐 목록으로 렌더링합니다.- 리스트의 각 항목에는
key를 주어야 하며,key는map이 돌려주는 가장 바깥 요소에 둡니다. key는 같은 부모 아래 형제끼리 고유하고, 항목이 있는 동안 바뀌지 않는 값이어야 합니다.- 가장 좋은
key는 데이터의 고유 ID이며, ID가 없다면 데이터를 만들 때 한 번 부여합니다. - 인덱스
key는 순서가 바뀌지 않고, 필터링하지 않으며, 항목에 state가 없을 때만 안전합니다. - 렌더할 때마다 무작위
key를 만들면 항목을 새로 만들어 state를 잃습니다.
연습 문제
1. 인덱스 키로 바꾸면
KeyPractice의 key만 posts.map((post, index) => <EditablePost key={index} post={post} />)로 바꾼 뒤, 페이지를 새로고침해 같은 초기 상태에서 시작합니다. 기초 행에 메모를 입력하고 순서를 뒤집으면 메모는 어느 제목 옆에 나타날까요?
해설 보기
메모는 응용 제목 옆에 나타납니다. 첫 번째 위치의 key 0이 그대로 유지되어, 뒤집기 전에 첫 위치에 있던 입력 state를 새로 첫 위치에 온 응용 행이 이어받기 때문입니다. 데이터의 ID와 화면의 위치가 서로 다른 기준이라는 점을 보여 주는 결과입니다.
2. 계획과 복습
KeyPractice의 초기 데이터를 ID 7의 “계획”과 ID 9의 “복습”으로 바꾸고, 복습 행에만 “완료”라는 메모를 입력합니다. 순서를 뒤집은 뒤 메모가 어느 제목 옆에 있어야 하는지, ID key를 쓸 때와 인덱스 key를 쓸 때로 나눠 예측해 보세요. 두 경우 모두 행은 두 개입니다.
해설 보기
ID key에서는 “복습” 옆에 “완료”가 남습니다. ID 9의 행이 두 번째에서 첫 번째로 옮겨 가도 React가 같은 정체성을 찾기 때문입니다.
인덱스 key에서는 두 번째 위치의 key 1이 유지되어 “계획” 옆에 “완료”가 나타납니다. 뒤집기 직전 두 번째 위치에 있던 입력 state를 새로 두 번째에 온 항목이 쓰기 때문입니다.
다음 글 폼 다루기 기초에서는 사용자가 직접 값을 입력하는 폼을 React의 state와 연결하는 방법을 살펴봅니다.