상태의 객체 업데이트하기

상태의 객체 업데이트하기

React 상태에는 객체를 포함해 모든 종류의 JavaScript 값을 넣을 수 있어요. 그런데 상태에 담긴 객체를 직접 변경(mutate)하면 안 돼요. 객체를 업데이트하고 싶다면 새 객체를 만들거나 기존 객체의 복사본을 만든 뒤, 그 복사본을 상태 설정 함수에 넘겨줘야 해요. 이 원칙을 지키면 재렌더링이 제대로 일어나고, 디버깅도 훨씬 수월해져요.

출처: https://react.dev/learn/updating-objects-in-state

상태를 읽기 전용으로 취급하기

재렌더링을 실제로 일으키려면 새 객체를 만들어 상태 설정 함수에 전달해야 해요.

onPointerMove={e => {
  setPosition({
    x: e.clientX,
    y: e.clientY
  });
}}

setPosition을 호출하면 React에게 "이 새 객체로 position을 교체하고 컴포넌트를 다시 렌더링하라"고 알리는 거예요. 이 예시에서는 빨간 점이 포인터를 따라 움직여요.

지역 변형(local mutation)은 괜찮아요

방금 만든 새 객체의 필드를 바꾸는 건 "지역 변형(local mutation)"이라고 불러요. 렌더링 중에도 할 수 있고 완전히 괜찮아요. 문제가 되는 건 이미 상태에 들어 있는 객체를 바꾸는 것이에요.

중첩 객체 업데이트하기

객체 안에 객체가 있는 중첩 구조도 마찬가지예요. 바꾸려는 지점부터 최상위까지 복사본을 만들어야 해요.

const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);

객체는 사실 "중첩"되어 있지 않아요

주의할 점이 있는데, "중첩(nested)"이라는 말은 실제 동작을 정확히 설명하지 못해요. 코드가 실행될 때 "중첩된 객체" 같은 건 없어요. 실제로는 서로 다른 두 객체를 보고 있는 거예요. 객체는 다른 객체를 "가리키는" 참조를 가지는데, 여러 변수가 같은 객체를 가리킬 수 있어요. 그래서 obj3.artwork.city를 변형하면 obj2.artwork.cityobj1.city까지 영향을 받을 수 있어요. 셋이 같은 객체를 바라보고 있기 때문이에요.

Immer로 간결하게 업데이트하기

중첩이 깊어지면 스프레드 문법이 장황해질 수 있어요. immer 라이브러리를 쓰면 draft를 직접 변형하는 것처럼 쓰면서도 상태는 안전하게 관리할 수 있어요.

updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});

일반 변형과 달리 이건 과거 상태를 덮어쓰지 않아요.

왜 상태 변형을 권장하지 않나

  • 디버깅: console.log를 쓰는데 상태를 변형하지 않으면, 과거 로그가 최신 상태 변경에 덮어써지지 않아요. 따라서 렌더링 사이 상태가 어떻게 바뀌었는지 명확히 볼 수 있어요.
  • 의도하지 않은 공유: 같은 객체를 가리키는 변수가 여러 개일 때 변형하면 예상 밖의 부작용이 생겨요.

정리

  • 객체를 변형하는 대신 새 버전을 만들고 상태를 그걸로 설정해 재렌더링을 일으켜요.
  • {...obj, something: 'newValue'} 같은 객체 스프레드 문법으로 복사본을 만들 수 있어요.
  • 방금 만든 객체만 변형하는 게 원칙이고, 이미 상태에 있는 객체를 다룰 때는 복사본을 만들어야 해요.

더 알아보기