React 교재 · 기초 9편

React 리스트 렌더링 — .map() · key

배열 100개를 JSX 100개로 한 줄에. key prop 의 진짜 의미까지.

동일 형식 카드 여러 개가 세로로 쌓이는 일러스트 — 리스트 렌더링 컨셉

실전 React 앱에서 가장 자주 나오는 패턴 — "배열 데이터를 받아 UI 항목 여러 개로 그리기". 사용자 목록·상품 카탈로그·댓글·알림 — 전부 같은 모양. JS 의 .map() 메서드와 JSX 가 만나는 지점.

이번 9편은 .map() 기본 + key prop 의 진짜 역할 (대부분 잘못 설명됨) + index 를 key 로 쓰면 일어나는 사고 + 필터·정렬 패턴까지.

1. .map() — 배열을 JSX 배열로

JSX 중괄호 안에 JSX 요소의 배열 을 넣으면 React 가 순서대로 그린다.

function UserList({ users }) { return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } // users = [{id:1,name:'박준성'},{id:2,name:'홍길동'}] // → <ul><li>박준성</li><li>홍길동</li></ul>

핵심 — .map() 의 콜백이 JSX 를 return. 각 항목마다 컴포넌트 하나가 생성. 화살표 함수의 본문이 한 줄이면 괄호로 감싸 암묵적 return, 여러 줄이면 중괄호 + 명시적 return.

2. key prop — "성능 최적화" 가 아니다

거의 모든 한국 튜토리얼이 "key 는 성능 최적화용" 이라고 설명. 절반만 맞다. 진짜 역할은 React 가 "어느 항목이 누구인지" 추적하기 위함 — 항목이 추가·삭제·재배치될 때 어떤 DOM 노드를 재사용하고 어떤 걸 새로 만들지 결정.

// key 없으면 <ul> {users.map(user => <li>{user.name}</li>)} // ❌ 경고 </ul> // React 콘솔: "Each child in a list should have a unique 'key' prop."

key 가 없으면 React 가 "이 항목이 이전 렌더의 그 항목인지" 알 수 없다. 100개 리스트에서 첫 항목 삭제하면 — React 입장에선 100개를 새로 그리는 것과 같음. key 가 있으면 "첫 항목만 사라지고 나머지 99개는 그대로" 인 걸 안다.

key 의 4가지 규칙 — ① 형제 사이에서만 유일하면 됨 (다른 부모 안에서 중복 OK). ② 안정적이어야 함 (렌더마다 바뀌면 의미 없음). ③ 데이터의 자연 ID (DB id·UUID) 가 최선. ④ 그것도 없으면 항목 내용 자체 (한 번 정해지면 안 바뀌는 값) — 단 중복 없어야.

3. index 를 key 로 쓰면 안 되는 진짜 이유

가장 흔한 패턴 — 데이터에 id 가 없어서 .map((item, i) => ... key={i}) 처럼 index 사용. 정적 리스트면 OK, 항목이 추가·삭제·재정렬되는 순간 버그 폭탄.

// 시나리오: input 이 있는 리스트 function TodoList() { const [todos, setTodos] = useState([ { text: '우유' }, { text: '빵' }, { text: '커피' } ]); return todos.map((todo, i) => ( <li key={i}> <input defaultValue={todo.text} /> </li> )); } // 사용자가 '빵' input 에 '식빵' 추가 입력 // 그 다음 '우유' 삭제 → 리스트는 [빵, 커피] // 하지만 input 에 입력한 '식빵' 글자가 '커피' 자리로 이동! ❌

왜냐하면 React 가 "key=0 이 빵, key=1 이 커피" 로 기억. 삭제 후엔 "key=0 이 빵→ 우유 자리에 빵을 그림" 이 아니라 "key=0 자리에 새 데이터 빵을 끼움" 인데 input 의 internal state (사용자가 입력한 '식빵') 가 첫 자리에 남아있어 어긋남.

해결책 — 안정적 id. 데이터 생성 시 crypto.randomUUID() 또는 Date.now() 같은 고유값 박기:

const [todos, setTodos] = useState([ { id: crypto.randomUUID(), text: '우유' }, // ... ]); return todos.map(todo => ( <li key={todo.id}> <input defaultValue={todo.text} /> </li> ));
예외 — 리스트가 평생 변하지 않고 단순 표시용 (예: 정적 메뉴) 이면 index 사용 OK. 하지만 일단 추가/삭제 기능이 들어가는 순간 id 필수. 처음부터 id 박는 게 안전.

4. 필터·정렬 — .map() 앞에 체이닝

JSX 중괄호는 JS 표현식이라 메서드 체이닝 자유롭게. 필터 후 정렬 후 렌더.

function ProductList({ products, category }) { return products .filter(p => p.category === category) .sort((a, b) => b.price - a.price) .map(p => ( <li key={p.id}>{p.name} — {p.price.toLocaleString()}원</li> )); }

이 한 줄이 SQL 의 SELECT WHERE ORDER BY 와 같은 일을 한다. 다만 — .sort() 는 원본 배열을 변경한다. props 로 받은 배열을 직접 sort 하면 React 의 immutability 원칙 위반. [...products].sort(...) 처럼 spread 로 복사본을 먼저 만들고 정렬해야 안전.

이 패턴이 손에 잡히면 동적 UI 의 99% 가 가능. 10편에서 Form — controlled vs uncontrolled 입력 관리 + 다중 input 처리로 마무리.

다음 글

React 교재 10편 — Form 다루기. controlled vs uncontrolled, 다중 input useState 패턴, 제출 처리.

© 2026 주나이테크(주) @JUNAITECH