React 상태 배열에 push를 했는데 목록이 안 바뀌거나, 어떤 화면에서만 이전 값이 보이면 배열을 제자리에서 수정했을 수 있다. state는 변경 자체보다 새 상태값을 전달하는 방식으로 갱신해야 한다.
push 대신 새 배열을 만든다
도식의 두 경로 중 React가 변경을 쉽게 감지하는 쪽은 새 배열 참조를 만드는 방식이다. 기존 배열에 push하는 코드와 전개 문법을 사용한 코드를 비교해 보자.
import { useState } from "react";
function TodoList() {
const [todos, setTodos] = useState([{ id: 1, title: "읽기", done: false }]);
function addTodo() {
setTodos((current) => [
...current,
{ id: Math.max(0, ...current.map((todo) => todo.id)) + 1,
title: "쓰기", done: false },
]);
}
return <>
<button onClick={addTodo}>추가</button>
<ul>{todos.map((todo) => <li key={todo.id}>{todo.title}</li>)}</ul>
</>;
}위 예제는 누를 때마다 새 항목을 더한다. ID 계산은 숫자만 있는 작은 목록을 위한 예시이며, 서버 데이터라면 서버가 부여한 안정적인 ID를 사용한다. todos.push(newTodo); setTodos(todos)처럼 원본을 수정하면 같은 배열 참조를 다시 전달한다. 반면 spread 문법은 새 배열을 만든다. 함수형 업데이트는 여러 업데이트가 가까운 시점에 예약돼도 최신 상태를 기준으로 계산한다.
수정과 삭제도 map, filter로 표현한다
function markDone(id) {
setTodos((current) => current.map((todo) =>
todo.id === id ? { ...todo, done: true } : todo
));
}
function removeTodo(id) {
setTodos((current) => current.filter((todo) => todo.id !== id));
}두 함수는 위 TodoList 안에 둘 수 있다. markDone(1)은 map으로 새 배열을 만들면서 첫 항목만 새 객체로 바꾸고, removeTodo(1)은 첫 항목을 제외한 새 배열을 만든다. 객체도 복사해야 한다. 배열만 새로 만들고 내부 todo 객체를 직접 수정하면, 그 객체를 공유하는 다른 상태나 메모이제이션이 예상과 다르게 동작할 수 있다.
key는 배열 인덱스보다 항목의 정체성을 표현해야 한다
렌더링에서 key={todo.id}처럼 안정적인 식별자를 사용한다. 삭제·정렬되는 목록에 index를 key로 쓰면 React가 서로 다른 항목을 같은 항목으로 오해해 입력값이나 포커스를 잘못 유지할 수 있다. 서버에서 받은 ID가 없다면 생성 시점에 안정적인 ID를 만든다.
원본을 바꾸지 않는 업데이트를 확인한다
const before = [{ id: 1, done: false }];
const after = before.map((todo) =>
todo.id === 1 ? { ...todo, done: true } : todo
);
console.assert(before[0].done === false);
console.assert(after[0].done === true);
console.assert(before !== after && before[0] !== after[0]);배열과 변경된 객체가 모두 새 참조인지 검사한다. React 컴포넌트에서는 빠른 연속 추가·삭제와 정렬 후에도 올바른 항목이 갱신되는지 테스트한다. key에 배열 인덱스를 사용하면 삭제 뒤 포커스나 입력 상태가 다른 행으로 옮겨갈 수 있다.
참조 검사에 더해 실제 컴포넌트에서 ‘추가’를 빠르게 두 번 누르고, 첫 항목을 삭제한 다음 남은 항목의 제목과 체크 상태가 맞는지도 확인하자. 순수 배열 테스트는 원본 변경을 잡아 주고, 화면 테스트는 key를 잘못 쓴 문제를 잡아 준다.
핵심 요약
React 상태는 직접 수정하지 않고 새 배열·새 객체로 교체한다. 추가는 spread, 변경은 map, 삭제는 filter로 표현하면 코드가 의도를 드러낸다. 목록 렌더링에는 안정적인 key를 쓰고, 동시 업데이트 가능성이 있으면 함수형 setState를 선택하자.

