React - 객체 state 업데이트하기

Sally·2026년 6월 15일

React 공식문서

목록 보기
11/11

React에서 객체 State 업데이트하기

React의 state는 숫자, 문자열뿐만 아니라 객체도 저장할 수 있다.

const [person, setPerson] = useState({
  firstName: 'Barbara',
  lastName: 'Hepworth',
  email: 'bhepworth@sculpture.com'
});

하지만 React state에 저장된 객체는 직접 수정하면 안 된다.

React는 객체 내부의 값이 바뀌었는지를 자동으로 추적하지 않는다.
React는 setState 함수를 통해 새로운 값이 전달되었을 때 상태가 바뀌었다고 판단한다.

즉, 객체 state는 읽기 전용처럼 다뤄야 한다.


변경(Mutation)이란?

먼저 숫자 state를 생각해보자.

const [x, setX] = useState(0);

x0에서 5로 바뀌었다고 해서 숫자 0 자체가 변한 것은 아니다.
숫자, 문자열, 불리언 같은 원시값은 자바스크립트에서 변경할 수 없는 값이다.

let x = 0;
x = 5;

위 코드는 숫자 05로 바꾼 것이 아니라, 변수 x가 새로운 값 5를 가리키도록 바꾼 것이다.

반면 객체는 내부 값을 직접 수정할 수 있다.

const position = { x: 0, y: 0 };

position.x = 100;
position.y = 200;

이처럼 기존 객체의 속성을 직접 바꾸는 것을 변경(Mutation)이라고 한다.
React state에서는 이런 방식으로 객체를 직접 변경하면 안 된다.


객체 State를 직접 수정하면 안 되는 이유

잘못된 코드

const [position, setPosition] = useState({ x: 0, y: 0 });

onPointerMove={e => {
  position.x = e.clientX;
  position.y = e.clientY;
}}

위 코드는 position 객체의 x, y 값을 직접 수정하고 있다.
하지만 React 입장에서는 setPosition이 호출되지 않았기 때문에 state가 변경되었다는 사실을 알 수 없다.

결과적으로 리렌더링이 발생하지 않고, 화면도 업데이트되지 않는다.


새로운 객체를 만들어 업데이트하기

객체 state를 업데이트할 때는 기존 객체를 직접 수정하지 않고, 새 객체를 만들어 state 설정 함수에 전달해야 한다.

올바른 코드

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

setPosition을 호출하면 React에게 다음 두 가지를 요청하는 것이다.

  1. position state를 새로운 객체로 교체한다.
  2. 컴포넌트를 다시 렌더링한다.

전개 문법으로 객체 복사하기

객체 state에서 특정 필드 하나만 바꾸고 싶을 때가 많다.
예를 들어 다음과 같은 state가 있다고 하자.

const [person, setPerson] = useState({
  firstName: 'Barbara',
  lastName: 'Hepworth',
  email: 'bhepworth@sculpture.com'
});

이때 firstName만 바꾸고 싶다고 해서 객체를 직접 수정하면 안 된다.


잘못된 방법

function handleFirstNameChange(e) {
  person.firstName = e.target.value;
}

function handleLastNameChange(e) {
  person.lastName = e.target.value;
}

function handleEmailChange(e) {
  person.email = e.target.value;
}

위 코드는 person 객체를 직접 수정한다.
값은 바뀌지만 React가 이를 감지하지 못하기 때문에 리렌더링이 일어나지 않는다.


올바른 방법

전개 문법을 사용해서 기존 객체를 복사한 뒤, 바꾸고 싶은 필드만 덮어쓴다.

function handleFirstNameChange(e) {
  setPerson({
    ...person,
    firstName: e.target.value
  });
}

function handleLastNameChange(e) {
  setPerson({
    ...person,
    lastName: e.target.value
  });
}

function handleEmailChange(e) {
  setPerson({
    ...person,
    email: e.target.value
  });
}

핵심은 다음과 같다.

setPerson({
  ...person,
  firstName: e.target.value
});

...person은 기존 객체의 모든 필드를 복사한다.
그 아래에 작성한 firstName은 기존 값을 새 값으로 덮어쓴다.

즉, 나머지 필드는 유지하고 원하는 필드 하나만 바꾸는 방식이다.


Form 예제

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: e.target.value
    });
  }

  function handleLastNameChange(e) {
    setPerson({
      ...person,
      lastName: e.target.value
    });
  }

  function handleEmailChange(e) {
    setPerson({
      ...person,
      email: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>

      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>

      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>

      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

[참고] 전개 문법은 얕은 복사다

전개 문법은 객체를 복사할 때 유용하지만, 한 단계만 복사한다.

const copiedPerson = { ...person };

위 코드는 person 객체의 가장 바깥쪽 필드만 복사한다.
객체 안에 또 다른 객체가 들어있는 중첩 구조라면 안쪽 객체까지 자동으로 새로 복사되지는 않는다.


중첩된 객체 State 업데이트하기

다음과 같은 state가 있다고 하자.

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
});

이때 city 값을 바꾸고 싶다면 다음처럼 직접 수정하면 안 된다.


잘못된 방법

person.artwork.city = 'New Delhi';

person.artwork 객체를 직접 수정하고 있기 때문이다.


올바른 방법

중첩된 객체를 수정할 때는 바꾸려는 안쪽 객체부터 바깥쪽 객체까지 전부 새로 만들어야 한다.

단계별로 작성하기

const nextArtwork = {
  ...person.artwork,
  city: 'New Delhi'
};

const nextPerson = {
  ...person,
  artwork: nextArtwork
};

setPerson(nextPerson);

위 코드는 다음 순서로 동작한다.

  1. 기존 person.artwork를 복사한다.
  2. city만 새 값으로 덮어쓴다.
  3. 기존 person을 복사한다.
  4. artwork를 새 객체로 교체한다.
  5. setPerson으로 state를 업데이트한다.

[추가] 한 번에 작성하기

setPerson({
  ...person,
  artwork: {
    ...person.artwork,
    city: 'New Delhi'
  }
});

이 코드는 위의 단계별 코드와 같은 의미다.
중첩 객체를 업데이트할 때는 기존 객체를 직접 수정하지 않고, 변경되는 경로에 있는 객체들을 새로 만들어야 한다.


중첩 객체 Form 예제

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>

      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>

      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>

      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>

      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        located in {person.artwork.city}
      </p>

      <img
        src={person.artwork.image}
        alt={person.artwork.title}
      />
    </>
  );
}

[추가] Immer로 더 간결하게 쓰기

중첩 객체를 수정할 때 전개 문법을 계속 쓰면 코드가 길어진다.

setPerson({
  ...person,
  artwork: {
    ...person.artwork,
    city: e.target.value
  }
});

이럴 때 Immer를 사용하면 더 간결하게 작성할 수 있다.

npm install use-immer
import { useImmer } from 'use-immer';

useState 대신 useImmer를 사용한다.

const [person, updatePerson] = useImmer({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://react.dev/images/docs/scientists/Sd1AgUOm.jpg',
  }
});

Immer에서는 updatePerson 안에서 draft를 수정한다.

function handleCityChange(e) {
  updatePerson(draft => {
    draft.artwork.city = e.target.value;
  });
}

겉으로 보기에는 객체를 직접 수정하는 것처럼 보이지만, 실제 원본 state를 직접 바꾸는 것은 아니다. Immer가 변경 내용을 바탕으로 새로운 객체를 만들어주기 때문에 React의 불변성 원칙은 지켜진다.

단, 아래처럼 원본 state를 직접 수정하면 안 된다.

// 잘못된 방식
person.artwork.city = e.target.value;

반드시 updatePerson 안에서 draft를 수정해야 한다.


Immer 전체 예제

import { useImmer } from 'use-immer';

export default function Form() {
  const [person, updatePerson] = useImmer({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: 'https://react.dev/images/docs/scientists/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    updatePerson(draft => {
      draft.name = e.target.value;
    });
  }

  function handleTitleChange(e) {
    updatePerson(draft => {
      draft.artwork.title = e.target.value;
    });
  }

  function handleCityChange(e) {
    updatePerson(draft => {
      draft.artwork.city = e.target.value;
    });
  }

  function handleImageChange(e) {
    updatePerson(draft => {
      draft.artwork.image = e.target.value;
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>

      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>

      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>

      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>

      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>

      <img
        src={person.artwork.image}
        alt={person.artwork.title}
      />
    </>
  );
}
profile
sally

0개의 댓글