본문으로 바로가기
TaeyoungKim.dev

React 배열 상태를 직접 수정하면 왜 화면이 갱신되지 않을까?

웹작성 약 3분 읽기TaeyoungKim
LinkedInX

React 상태 배열에 push를 했는데 목록이 안 바뀌거나, 어떤 화면에서만 이전 값이 보이면 배열을 제자리에서 수정했을 수 있다. state는 변경 자체보다 새 상태값을 전달하는 방식으로 갱신해야 한다.

push 대신 새 배열을 만든다

도식의 두 경로 중 React가 변경을 쉽게 감지하는 쪽은 새 배열 참조를 만드는 방식이다. 기존 배열에 push하는 코드와 전개 문법을 사용한 코드를 비교해 보자.

jsx
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로 표현한다

jsx
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를 만든다.

원본을 바꾸지 않는 업데이트를 확인한다

js
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를 선택하자.

작성자

TaeyoungKim

기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

함께 읽으면 좋은 글