리스트와 키
우리는 조건부 렌더링을 통해 특정 조건에 따라 UI를 동적으로 변경하는 방법을 배웠습니다. 이번 장에서는 여러 개의 데이터를 효율적으로 화면에 나열하는 방법인 리스트(List) 렌더링과, 리액트에서 리스트를 렌더링할 때 반드시 필요한 key
(키) 속성에 대해 알아보겠습니다.
게시판의 게시물 목록, 쇼핑몰의 상품 목록, 댓글 목록 등 대부분의 웹 애플리케이션은 여러 개의 유사한 데이터를 목록 형태로 보여주는 경우가 많습니다. 리액트는 이러한 리스트를 효율적으로 렌더링하기 위한 강력한 방법을 제공합니다.
map()
함수를 이용한 리스트 렌더링
리액트에서 리스트를 렌더링하는 가장 일반적이고 권장되는 방법은 자바스크립트 배열의 map()
메서드를 사용하는 것입니다. map()
메서드는 배열의 각 요소에 대해 지정된 함수를 호출하고, 그 함수의 반환값으로 이루어진 새로운 배열을 생성합니다. 리액트에서는 이 새로운 배열에 담긴 JSX 요소들을 화면에 렌더링합니다.
예제: 간단한 숫자 목록 렌더링
-
NumberList.js
컴포넌트 생성src/components
폴더 안에NumberList.js
파일을 생성합니다.src/components/NumberList.js // src/components/NumberList.js import React from 'react'; 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
를 생성합니다.{listItems}
: JSX 내부에서 자바스크립트 표현식({}
)을 사용하여listItems
배열을 그대로 렌더링합니다. 리액트는 배열 안에 있는 JSX 요소들을 자동으로 펼쳐서 나열합니다.
-
App.js
에서NumberList
컴포넌트 사용:src/App.js // src/App.js import React from 'react'; 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까지의 숫자가 목록으로 나타날 것입니다. 하지만 개발자 도구의 콘솔을 확인해 보면 다음과 같은 경고 메시지를 발견할 수 있습니다.
Warning: Each child in a list should have a unique "key" prop.
이것이 바로
key
속성에 대한 경고입니다. 이제 이key
가 왜 필요하고 어떻게 사용하는지 알아보겠습니다.
key
의 필요성
리액트에서 리스트를 렌더링할 때 각 리스트 아이템에 고유한 key
prop을 부여해야 하는 것은 매우 중요합니다. key
는 리액트가 리스트 내의 어떤 아이템이 변경, 추가 또는 삭제되었는지 식별하는 데 사용되는 특별한 문자열 속성입니다.
key
가 필요한 이유 (리액트의 "재조정" 원리):
리액트는 DOM을 효율적으로 업데이트하기 위해 재조정(Reconciliation) 이라는 과정을 거칩니다. 새로운 UI 트리를 이전 UI 트리와 비교하여 어떤 부분이 변경되었는지 파악하고, 최소한의 DOM 조작만으로 화면을 업데이트합니다.
리스트 아이템의 경우, key
가 없다면 리액트는 단순히 순서가 변경되거나 새로운 아이템이 추가될 때, 각 아이템이 재정렬된 것인지 아니면 완전히 새로운 아이템인지 구분하기 어렵습니다. 예를 들어, 리스트 중간에 새로운 아이템이 삽입되거나 기존 아이템이 삭제될 때, key
가 없다면 리액트는 모든 하위 컴포넌트의 상태를 다시 만들고 DOM을 전부 다시 그리려고 시도할 수 있습니다. 이는 성능 저하로 이어집니다.
반면, key
가 있다면 리액트는 각 아이템을 key
로 식별하여, key
가 일치하는 아이템은 그대로 유지하고, key
가 변경되거나 없는 아이템만 효율적으로 업데이트하거나 새로 생성합니다. 즉, key
는 리액트가 재조정 과정을 최적화할 수 있도록 돕는 힌트(hint) 역할을 합니다.
key
가 없을 때 발생할 수 있는 문제점:
- 성능 저하: 불필요한 재렌더링으로 인해 애플리케이션이 느려질 수 있습니다.
- 컴포넌트 내부 상태 문제: 리스트 아이템 컴포넌트가 자체적인
state
를 가지고 있다면,key
가 없을 때 아이템의 순서가 변경되거나 삭제되면state
가 엉뚱한 아이템에 할당되거나 초기화되는 등 예측 불가능한 버그가 발생할 수 있습니다. (예: 체크박스 목록에서 특정 항목만 체크가 해제되어야 하는데 다른 항목이 해제되는 현상)
key
속성 올바르게 사용하기
key
는 리스트의 각 아이템에 고유하게 식별 가능한 값이어야 합니다.
고유한 ID 사용 (가장 권장)
가장 이상적인 key
값은 데이터 자체가 가지고 있는 고유한 ID입니다. 데이터베이스에서 가져온 데이터라면 id
필드를 key
로 사용하는 것이 일반적입니다.
예제: 게시물 목록 렌더링 (고유 ID 사용)
-
PostList.js
컴포넌트 생성:src/components
폴더 안에PostList.js
파일을 생성합니다.src/components/PostList.js // src/components/PostList.js import React from 'react'; 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.js
에서PostList
컴포넌트 사용:src/App.js // src/App.js import React from 'react'; import './App.css'; import PostList from './components/PostList'; // PostList 컴포넌트 불러오기 function App() { return ( <div className="App"> <h1>React 리스트 렌더링 (Key 적용)</h1> <PostList /> </div> ); } export default App;
-
결과 확인: 이제 콘솔에
key
경고 메시지가 나타나지 않을 것입니다.
인덱스를 key
로 사용하기 (주의 필요!)
만약 데이터에 고유한 ID가 없다면, map()
함수의 두 번째 인자인 index
(배열의 인덱스) 를 key
로 사용할 수도 있습니다.
// src/components/NumberList.js (key 추가)
import React from 'react';
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
로 사용하는 경우의 주의사항
인덱스를 key
로 사용하는 것은 다음 조건에 모두 해당될 때만 안전하게 사용할 수 있습니다.
- 리스트 아이템의 순서가 변경되지 않을 때: 리스트가 정렬되거나, 중간에 아이템이 추가/삭제되지 않을 때.
- 리스트가 필터링되지 않을 때: 리스트에서 특정 아이템이 제거되지 않을 때.
- 리스트 아이템이
state
를 가지고 있지 않을 때: 각 리스트 아이템 컴포넌트가 자신만의 내부state
를 가지고 있지 않을 때.
위 조건 중 하나라도 해당되지 않는다면, 인덱스를 key
로 사용하는 것은 예측 불가능한 버그나 성능 문제를 야기할 수 있습니다. 따라서, 가능하면 항상 데이터의 고유 ID를 key
로 사용하는 것을 강력히 권장합니다. 고유 ID가 없다면, 백엔드에서 생성하거나 프론트엔드에서 uuid
와 같은 라이브러리를 사용하여 임시 고유 ID를 생성하는 것을 고려해 볼 수 있습니다.
컴포넌트로 리스트 아이템 분리하기
리스트를 렌더링할 때 각 아이템을 별도의 컴포넌트로 분리하는 것은 좋은 습관입니다. 이는 코드의 가독성, 재사용성, 그리고 유지보수성을 향상시킵니다. 위 PostList.js
예시에서 PostItem
컴포넌트를 분리하여 사용한 것처럼 말입니다.
// src/components/PostList.js (재확인)
import React from 'react';
// 각 게시물 아이템을 위한 별도의 컴포넌트
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
데이터만 가지고 독립적으로 렌더링됩니다. 이는 리액트의 '컴포넌트 지향' 개발 철학에도 부합합니다.
"리스트와 키"는 여기까지입니다. map()
함수를 이용한 리스트 렌더링 방법, key
속성의 중요성과 필요성, 그리고 key
를 올바르게 사용하는 방법을 상세하게 다루었습니다. 특히 인덱스를 key
로 사용할 때의 주의사항을 강조하여 독자들이 잠재적인 문제를 피할 수 있도록 했습니다.