리스트와 키
2. 리스트와 키
React에서 여러 개의 데이터를 화면에 표시하는 방법과 각 항목을 고유하게 식별하기 위한 키(Key)의 중요성을 배워보겠습니다.
2.1 기본 리스트 렌더링
배열 데이터 렌더링
function SimpleList() {
const fruits = ['사과', '바나나', '오렌지', '포도', '딸기'];
return (
<div>
<h2>과일 목록</h2>
<ul>
{fruits.map((fruit, index) => (
<li key={index}>{fruit}</li>
))}
</ul>
</div>
);
}
객체 배열 렌더링
import { useState } from 'react';
function UserList() {
const [users] = useState([
{ id: 1, name: '김개발', age: 25, job: '프론트엔드 개발자' },
{ id: 2, name: '이디자인', age: 28, job: 'UI/UX 디자이너' },
{ id: 3, name: '박백엔드', age: 30, job: '백엔드 개발자' },
{ id: 4, name: '최데이터', age: 27, job: '데이터 사이언티스트' }
]);
return (
<div>
<h2>팀원 목록</h2>
<div>
{users.map(user => (
<div
key={user.id}
>
<h3>{user.name}</h3>
<p>나이: {user.age}세</p>
<p>직업: {user.job}</p>
</div>
))}
</div>
</div>
);
}
2.2 키(Key)의 중요성
키가 없을 때의 문제점
import { useState } from 'react';
function ProblematicList() {
const [items, setItems] = useState(['첫 번째', '두 번째', '세 번째']);
const addItem = () => {
const newItem = `${items.length + 1}번째`;
setItems([newItem, ...items]); // 맨 앞에 추가
};
const removeFirstItem = () => {
setItems(prev => prev.slice(1));
};
return (
<div>
<h3>❌ 키가 인덱스인 경우 (문제가 있는 예제)</h3>
<div>
<button onClick={addItem}>맨 앞에 추가</button>
<button onClick={removeFirstItem}>첫 번째 삭제</button>
</div>
<ul>
{items.map((item, index) => (
<li key={index}>
<input type="text" defaultValue={item} />
<span>{item}</span>
</li>
))}
</ul>
<p>
위 입력 필드에 값을 입력한 후 "맨 앞에 추가"를 클릭해보세요.
입력값이 의도와 다르게 이동하는 것을 확인할 수 있습니다.
</p>
</div>
);
}
올바른 키 사용
import { useState } from 'react';
function CorrectList() {
const [items, setItems] = useState([
{ id: 1, text: '첫 번째' },
{ id: 2, text: '두 번째' },
{ id: 3, text: '세 번째' }
]);
const [nextId, setNextId] = useState(4);
const addItem = () => {
const newItem = { id: nextId, text: `${nextId}번째` };
setItems([newItem, ...items]); // 맨 앞에 추가
setNextId(prev => prev + 1);
};
const removeFirstItem = () => {
setItems(prev => prev.slice(1));
};
return (
<div>
<h3>✅ 키가 고유 ID인 경우 (올바른 예제)</h3>
<div>
<button onClick={addItem}>맨 앞에 추가</button>
<button onClick={removeFirstItem}>첫 번째 삭제</button>
</div>
<ul>
{items.map(item => (
<li key={item.id}>
<input type="text" defaultValue={item.text} />
<span>{item.text}</span>
</li>
))}
</ul>
<p>
고유한 ID를 키로 사용하여 입력값이 올바르게 유지됩니다.
</p>
</div>
);
}
2.3 동적 리스트 조작
CRUD 기능이 있는 할 일 목록
import { useState } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([
{ id: 1, text: 'React 공부하기', completed: false },
{ id: 2, text: 'JSX 이해하기', completed: true },
{ id: 3, text: '컴포넌트 만들기', completed: false }
]);
const [inputValue, setInputValue] = useState('');
const [nextId, setNextId] = useState(4);
const [filter, setFilter] = useState('all'); // all, active, completed
const addTodo = () => {
if (inputValue.trim()) {
const newTodo = {
id: nextId,
text: inputValue.trim(),
completed: false
};
setTodos([...todos, newTodo]);
setInputValue('');
setNextId(prev => prev + 1);
}
};
const toggleTodo = (id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
};
const editTodo = (id, newText) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
};
const filteredTodos = todos.filter(todo => {
if (filter === 'active') return !todo.completed;
if (filter === 'completed') return todo.completed;
return true; // all
});
return (
<div>
<h2>할 일 관리</h2>
{/* 할 일 추가 */}
<div>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="새로운 할 일을 입력하세요"
/>
<button
onClick={addTodo}
>
추가
</button>
</div>
{/* 필터 버튼 */}
<div>
{['all', 'active', 'completed'].map(filterType => (
<button
key={filterType}
onClick={() => setFilter(filterType)}
>
{filterType === 'all' ? '전체' :
filterType === 'active' ? '미완료' : '완료'}
</button>
))}
</div>
{/* 할 일 목록 */}
{filteredTodos.length === 0 ? (
<div>
{filter === 'all' ? '할 일이 없습니다.' :
filter === 'active' ? '미완료된 할 일이 없습니다.' : '완료된 할 일이 없습니다.'}
</div>
) : (
<ul>
{filteredTodos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={toggleTodo}
onDelete={deleteTodo}
onEdit={editTodo}
/>
))}
</ul>
)}
{/* 통계 */}
<div>
<p>전체: {todos.length}개 |
완료: {todos.filter(t => t.completed).length}개 |
미완료: {todos.filter(t => !t.completed).length}개</p>
</div>
</div>
);
}
function TodoItem({ todo, onToggle, onDelete, onEdit }) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleEdit = () => {
if (editText.trim() && editText.trim() !== todo.text) {
onEdit(todo.id, editText.trim());
}
setIsEditing(false);
};
const handleCancel = () => {
setEditText(todo.text);
setIsEditing(false);
};
return (
<li>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
{isEditing ? (
<div>
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') handleEdit();
if (e.key === 'Escape') handleCancel();
}}
autoFocus
/>
<button onClick={handleEdit}>저장</button>
<button onClick={handleCancel}>취소</button>
</div>
) : (
<>
<span
onDoubleClick={() => setIsEditing(true)}
>
{todo.text}
</span>
<div>
<button
onClick={() => setIsEditing(true)}
>
수정
</button>
<button
onClick={() => onDelete(todo.id)}
>
삭제
</button>
</div>
</>
)}
</li>
);
}
export { TodoApp, TodoItem };
2.4 중첩된 리스트 렌더링
카테고리별 제품 목록
import { useState } from 'react';
function NestedListExample() {
const [categories] = useState([
{
id: 1,
name: '전자제품',
items: [
{ id: 101, name: '노트북', price: 1500000 },
{ id: 102, name: '스마트폰', price: 800000 },
{ id: 103, name: '태블릿', price: 600000 }
]
},
{
id: 2,
name: '의류',
items: [
{ id: 201, name: '티셔츠', price: 25000 },
{ id: 202, name: '청바지', price: 80000 },
{ id: 203, name: '운동화', price: 120000 }
]
},
{
id: 3,
name: '도서',
items: [
{ id: 301, name: 'React 완벽 가이드', price: 35000 },
{ id: 302, name: 'JavaScript 심화', price: 28000 },
{ id: 303, name: 'CSS 마스터', price: 32000 }
]
}
]);
const [expandedCategories, setExpandedCategories] = useState(new Set([1]));
const toggleCategory = (categoryId) => {
setExpandedCategories(prev => {
const newSet = new Set(prev);
if (newSet.has(categoryId)) {
newSet.delete(categoryId);
} else {
newSet.add(categoryId);
}
return newSet;
});
};
const formatPrice = (price) => price.toLocaleString() + '원';
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h2>상품 카테고리</h2>
{categories.map(category => (
<div
key={category.id}
style={{
marginBottom: '20px',
border: '1px solid #ddd',
borderRadius: '8px',
overflow: 'hidden'
}}
>
{/* 카테고리 헤더 */}
<div
style={{
backgroundColor: '#f8f9fa',
padding: '15px',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
borderBottom: expandedCategories.has(category.id) ? '1px solid #ddd' : 'none'
}}
onClick={() => toggleCategory(category.id)}
>
<h3 style={{ margin: 0 }}>{category.name}</h3>
<span style={{ fontSize: '18px' }}>
{expandedCategories.has(category.id) ? '▼' : '▶'}
</span>
</div>
{/* 카테고리 아이템들 */}
{expandedCategories.has(category.id) && (
<div style={{ padding: '0' }}>
{category.items.map((item, index) => (
<div
key={item.id}
style={{
padding: '15px',
borderBottom: index < category.items.length - 1 ? '1px solid #f0f0f0' : 'none',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'white'
}}
>
<span style={{ fontWeight: 'medium' }}>{item.name}</span>
<span style={{
color: '#e91e63',
fontWeight: 'bold',
fontSize: '16px'
}}>
{formatPrice(item.price)}
</span>
</div>
))}
</div>
)}
</div>
))}
<div style={{
marginTop: '30px',
padding: '15px',
backgroundColor: '#e3f2fd',
borderRadius: '8px'
}}>
<h4>💡 인터랙션 가이드</h4>
<p style={{ margin: '5px 0', fontSize: '14px' }}>
• 카테고리 이름을 클릭하면 상품 목록이 펼쳐지거나 접힙니다
</p>
<p style={{ margin: '5px 0', fontSize: '14px' }}>
• 각 리스트 항목은 고유한 키(key)를 가지고 있습니다
</p>
</div>
</div>
);
}
2.5 리스트 성능 최적화
React.memo를 활용한 최적화
import { useState, useCallback, memo } from 'react';
const ListItem = memo(function ListItem({ item, onUpdate, onDelete }) {
console.log(`ListItem ${item.id} 렌더링됨`); // 렌더링 확인용
return (
<div>
<div>
<h4>{item.name}</h4>
<p>
{item.description}
</p>
</div>
<div>
<button
onClick={() => onUpdate(item.id)}
>
업데이트
</button>
<button
onClick={() => onDelete(item.id)}
>
삭제
</button>
</div>
</div>
);
});
function OptimizedList() {
const [items, setItems] = useState([
{ id: 1, name: '아이템 1', description: '첫 번째 아이템입니다' },
{ id: 2, name: '아이템 2', description: '두 번째 아이템입니다' },
{ id: 3, name: '아이템 3', description: '세 번째 아이템입니다' }
]);
const [counter, setCounter] = useState(0);
// useCallback으로 함수 메모이제이션
const handleUpdate = useCallback((id) => {
setItems(prev => prev.map(item =>
item.id === id
? { ...item, name: `${item.name} (업데이트됨)` }
: item
));
}, []);
const handleDelete = useCallback((id) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
return (
<div>
<h2>최적화된 리스트</h2>
<div>
<p>카운터: {counter}</p>
<button
onClick={() => setCounter(prev => prev + 1)}
>
카운터 증가
</button>
</div>
<p>
개발자 도구 콘솔을 확인해보세요. 카운터를 증가시켜도 리스트 아이템들이
불필요하게 재렌더링되지 않습니다.
</p>
<div>
{items.map(item => (
<ListItem
key={item.id}
item={item}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>
))}
</div>
</div>
);
}
2.6 키 사용 시 주의사항
올바른 키 vs 잘못된 키
import { useState } from 'react';
function KeyExamples() {
const [items, setItems] = useState([
{ id: 'a', name: '아이템 A' },
{ id: 'b', name: '아이템 B' },
{ id: 'c', name: '아이템 C' }
]);
const shuffleItems = () => {
setItems(prev => [...prev].sort(() => Math.random() - 0.5));
};
return (
<div>
<h2>키(Key) 비교 예제</h2>
<button onClick={shuffleItems}>
항목 순서 섞기
</button>
<div>
{/* 잘못된 예: 인덱스를 키로 사용 */}
<div>
<h3>❌ 인덱스를 키로 사용</h3>
<ul>
{items.map((item, index) => (
<li key={index}>
<input type="text" defaultValue={item.name} />
{item.name}
</li>
))}
</ul>
</div>
{/* 올바른 예: 고유 ID를 키로 사용 */}
<div>
<h3>✅ 고유 ID를 키로 사용</h3>
<ul>
{items.map(item => (
<li key={item.id}>
<input type="text" defaultValue={item.name} />
{item.name}
</li>
))}
</ul>
</div>
</div>
<div>
<strong>테스트 방법:</strong>
<ol>
<li>각 입력 필드에 서로 다른 값을 입력하세요</li>
<li>"항목 순서 섞기" 버튼을 클릭하세요</li>
<li>왼쪽(인덱스 키)과 오른쪽(ID 키)의 차이를 확인하세요</li>
</ol>
</div>
</div>
);
}
2.7 Best Practices
1. 안정적이고 고유한 키 사용
// ✅ 좋은 예
const items = [
{ id: 'uuid-1', name: 'Item 1' },
{ id: 'uuid-2', name: 'Item 2' }
];
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
// ❌ 나쁜 예
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item.name}</li>
))}
</ul>
);
2. 키로 Math.random() 사용 금지
// ❌ 절대 하지 말 것
return (
<ul>
{items.map(item => (
<li key={Math.random()}>{item.name}</li>
))}
</ul>
);
3. 리스트 컴포넌트 분리
// ✅ 좋은 예 - 컴포넌트 분리
function UserCard({ user }) {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
}
function UserList({ users }) {
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
2.8 정리
리스트와 키의 핵심 개념을 학습했습니다:
- map() 메서드를 활용한 배열 데이터 렌더링
- 고유하고 안정적인 키의 중요성과 올바른 사용법
- 동적 리스트 조작 (추가, 수정, 삭제)
- 중첩된 리스트 렌더링 방법
- 성능 최적화를 위한 React.memo와 useCallback 활용
- 키 사용 시 주의사항과 Best Practices
다음 장에서는 폼과 제어 컴포넌트에 대해 알아보겠습니다.