객체 State 업데이트하기
State는 객체를 포함한 모든 종류의 자바스크립트 값을 가질 수 있음
ㄴ React state가 가진 객체를 직접 변경해서는 안 됨
--> 객체를 업데이트하고 싶을 때에는 새로운 객체를 생성하여 (또는 기존 객체의 복사본을 만들어) 사용
✔️ 변경이란?
State에는 모든 종류의 자바스크립트 값을 저장 가능
ㄴ 이러한 자바스크립트 값들은 변경할 수 없거나, 읽기 전용을 의미하는 불변성을 가짐
const [x, setX] = useState(0);
값을 교체하기 위해서는 리렌더링이 필요
setX(5);
^ X state는 0에서 5로 바뀌었지만, 숫자 0 자체는 바뀌지 않았음
== x가 가리키는 대상(상태)은 바뀌었지만, 메모리에 존재하는 0이라는 숫자 값 그 자체가 5로 변한 것은 아님
ㄴ 숫자, 문자열, 불리언과 같이 자바스크립트에 정의되어 있는 원시 값들은 변경할 수 없음
기술적으로 객체 자체의 내용은 바꿀 수 있음
== 변경(mutation)
const [position, setPosition] = useState({ x: 0, y: 0});
position.x = 5;
* React state의 객체들이 기술적으로 변경 가능할지라도, 불변성을 가진 것처럼 다루어야 함
--> 객체를 변경하는 대신 교체해야 함
✔️ State를 읽기 전용인 것처럼 다루기
state에 저장한 자바스크립트 객체는 어떤 것이라도 읽기 전용인 것처럼 다루어야 함
// App.js
import { useState } from 'react';
export default function MovingDot() {
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}
style={{
position: 'relative',
width: '100vw',
height: '100vh',
}}>
<div style={{
position: 'absolute',
backgroundColor: 'red',
borderRadius: '50%',
transform: `translate(${position.x}px, ${position.y}px)`,
left: -10,
top: -10,
width: 20,
height: 20,
}} />
</div>
)
}
onPointerMove={e => {
position.x = e.clientX;
position.y = e.clientY;
}}
^ position에 할당된 객체를 이전 렌더링에서 수정
그러나 React는 state 설정 함수가 없으면 객체가 변경되었는지 알 수 없음
--> React는 아무것도 하지 않음
리렌더링을 발생시키기 위해서는, 새 객체를 생성하여 state 설정 함수로 전달해야 함
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
setPosition은 React에게 다음과 같이 요청
- position을 이 새로운 객체로 교체하라
- 그리고 이 컴포넌트를 다시 렌더링하라
✔️ 전개 문법으로 객체 복사하기
// App.js
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com'
});
function handleFirstNameChange(e) {
person.firstName = e.target.value;
}
function handleLastNameChange(e) {
person.lastName = e.target.value;
}
function handleEmailChange(e) {
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>
</>
);
}
person.firstName = e.target.value;
^ 이전 렌더의 state를 변경
원하는 동작을 정확히 얻기 위해서는 새로운 객체를 생성하여 setPerson으로 전달해야 함
단 하나의 필드가 바뀌었기 때문에 기존에 존재하는 다른 데이터를 복사해야 함
setPerson({
firstName: e.target.value, // input의 새로운 first name
lastName: person.lastName,
email: person.email
});
... 객체 전개 문구를 사용하면 모든 프로퍼티를 각각 복사하지 않아도 됨
setPerson({
...person, // 이전 필드를 복사
firstName: e.target.value // 새로운 부분은 덮어쓰기
});
✔️ 중첩된 객체 갱신하기
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://react.dev/images/docs/scientists/Sd1AgUOm.jpg',
}
});
person.artwork.city를 업데이트 하고 싶다면?
1. 새로운 artwork 객체를 생성한 뒤, 그것을 가리키는 새로운 person 객체를 만들기
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);
2. 단순하게 함수 호출
setPerson({
...person, // 다른 필드 복사
artwork: { // artwork 교체
...person.artwork, // 동일한 값 사용
city: 'New Delhi' // 하지만 New Delhi!
}
});
✔️ 객체들은 사실 중첩되어 있지 않음
let obj = {
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://react.dev/images/docs/scientists/Sd1AgUOm.jpg',
}
};
^ 이러한 객체는 코드에서 중첩되어 나타남
사실 코드가 실행될 때, 중첩된 객체라는 것은 없음
ㄴ 우리는 두 개의 다른 객체를 보는 것
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://react.dev/images/docs/scientists/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
^ obj1 객체는 obj2 안에 존재하는 것이 아님
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://react.dev/images/docs/scientists/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
let obj3 = {
name: 'Copycat',
artwork: obj1
};
obj3.artwork.city를 변경하려 했다면, obj2.artwork.city와 obj1.city 둘 모두에게 영향을 미치게 됨
--> obj3.artwork, obj2.artwork와 obj1이 같은 객체이기 때문
∴ 객체는 중첩된 것이 아니라, 프로퍼티를 통해 서로를 가리키는 것임
✔️ Immer로 간결한 갱신 로직 작성하기
state가 깊이 중첩되어 있다면 평탄화를 고려
ㄴ state 구조를 바꾸고 싶지 않다면, 중첩 전개할 수 있는 Immer가 있음
Immer : 변경 구문을 사용할 수 있게 해주며 복사본 생성을 도와주는 라이브러리
Immer 사용 방법
- package.json에 dependencies로 use-immer를 추가
- npm install 명령어 실행
- import { useState } from 'react'를 import { useImmer } from 'use-immer'로 교체
// App.js
import { useImmer } from 'use-immer';
export default function Form() {
const [person, updatePerson] = useImmer({ // useImmer
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://react.dev/images/docs/scientists/Sd1AgUOm.jpg',
}
});
/*
function handleNameChange(e) {
setPerson({
...person,
name: e.target.value
});
}
*/
function handleNameChange(e) {
updatePerson(draft => { // setPerson() 대신 사용
draft.name = e.target.value;
});
}
/*
function handleTitleChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
title: e.target.value
}
});
}
*/
function handleTitleChange(e) {
updatePerson(draft => {
draft.artwork.title = e.target.value;
});
}
/*
function handleCityChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
city: e.target.value
}
});
}
*/
function handleCityChange(e) {
updatePerson(draft => {
draft.artwork.city = e.target.value;
});
}
/*
function handleImageChange(e) {
setPerson({
...person,
artwork: {
...person.artwork,
image: 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}
/>
</>
);
}
Immer는 업데이트 핸들러를 간결하게 관리 가능
특히 state가 중첩되어 있고 객체를 중복으로 복사해야 할 때 유용
배열 State 업데이트하기
배열은 자바스크립트에서는 변경이 가능하지만, state로 저장할 때에는 변경할 수 없도록 처리해야 함
state에 저장된 배열을 업데이트 하고 싶을 때에는 새로운 배열을 생성하여 (또는 기존 배열의 복사본을 만들어) 사용
✔️ 변경하지 않고 배열 업데이트하기
자바스크립트에서 배열은 다른 종류의 객체
--> 객체와 마찬가지로 React state에서 배열은 읽기 전용으로 처리해야 함
ㄴ arr[0] = 'bird'처럼 배열 내부의 항목을 재할당하거나, push()나 pop() 같은 함수로 배열을 변경해서는 안 됨
배열을 업데이트할 때마다 새 배열을 state 설정 함수에 전달해야 함
ㄴ state의 원본 배열을 변경시키지 않는 filter()와 map() 같은 함수를 사용하여 원본 배열로부터 새 배열을 만들 수 있음
| 비선호 (배열을 변경) | 선호 (새 배열을 반환) | |
| 추가 | push, unshift | concat, [...arr] 전개 연산자 |
| 제거 | pop, shift, splice | filter, slice |
| 교체 | splice, arr[i]=... 할당 | map |
| 정렬 | reverse, sort | 배열을 복사한 이후 처리 |
* slice와 splice 함수
- slice를 사용하면 배열 또는 그 일부를 복사할 수 있음
- splice는 배열을 변경함
- 항목을 추가하거나 제거
✔️ 배열에 항목 추가하기
push()는 배열을 변경
--> 권장하지 않음
기존에 존재하던 항목들 뒤에 새 항목을 포함하는 새로운 배열을 만들기
--> ... 배열 전개 구문을 사용
// App.js
import { useState } from 'react';
let nextId = 0;
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState([]);
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={() => {
setArtists([
...artists,
{ id: nextId++, name: name }
]);
}}>Add</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
setArtists( // 아래의 새로운 배열로 state를 변경
[
...artists, // 기존 배열의 모든 항목에,
{ id: nextId++, name: name } // 마지막에 새 항목을 추가
]
);
✔️ 배열에서 항목 제거하기
해당 항목을 포함하지 않는 새 배열을 만들기
--> filter 함수를 사용
// App.js
import { useState } from 'react';
let initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [artists, setArtists] = useState(
initialArtists
);
return (
<>
<h1>Inspiring sculptors:</h1>
<ul>
{artists.map(artist => (
<li key={artist.id}>
{artist.name}{' '}
<button onClick={() => {
setArtists(
artists.filter(a =>
a.id !== artist.id
)
);
}}>
Delete
</button>
</li>
))}
</ul>
</>
);
}
setArtists(
artists.filter(a => a.id !== artist.id)
);
^ artist.id와 id가 다른 artists로 구성된 배열을 생성한다는 의미
* filter가 원본 배열을 수정하지는 않음
✔️ 배열 반환하기
배열의 일부 또는 전체 항목을 변경하여 새 배열을 만들기
--> map()을 사용
// App.js
import { useState } from 'react';
let initialShapes = [
{ id: 0, type: 'circle', x: 50, y: 100 },
{ id: 1, type: 'square', x: 150, y: 100 },
{ id: 2, type: 'circle', x: 250, y: 100 },
];
export default function ShapeEditor() {
const [shapes, setShapes] = useState(
initialShapes
);
function handleClick() {
const nextShapes = shapes.map(shape => {
if (shape.type === 'square') {
// 변경시키지 않고 반환
return shape;
} else {
// 50px 아래로 이동한 새로운 원을 반환
return {
...shape,
y: shape.y + 50,
};
}
});
// 새로운 배열로 리렌더링
setShapes(nextShapes);
}
return (
<>
<button onClick={handleClick}>
Move circles down!
</button>
{shapes.map(shape => (
<div style={{
background: 'purple',
position: 'absolute',
left: shape.x,
top: shape.y,
borderRadius:
shape.type === 'circle'
? '50%' : '',
width: 20,
height: 20,
}} />
))}
</>
);
}
✔️ 배열 내 항목 교체하기
배열에서 하나 이상의 항목을 교체하는 경우가 흔하지만,
arr[0] = 'bird'와 같은 할당은 원본 배열을 변경시키므로 map()을 사용하는 편이 좋음
// App.js
import { useState } from 'react';
let initialCounters = [
0, 0, 0
];
export default function CounterList() {
const [counters, setCounters] = useState(
initialCounters
);
function handleIncrementClick(index) {
const nextCounters = counters.map((c, i) => {
if (i === index) {
// 클릭된 counter를 증가시킴
return c + 1;
} else {
// 변경되지 않은 나머지를 반환
return c;
}
});
setCounters(nextCounters);
}
return (
<ul>
{counters.map((counter, i) => (
<li key={i}>
{counter}
<button onClick={() => {
handleIncrementClick(i);
}}>+1</button>
</li>
))}
</ul>
);
}
✔️ 배열에 항목 삽입하기
시작도, 끝도 아닌 위치에 항목을 삽입하기
--> ... 배열 전개 구문과 slice() 함수를 함께 사용
slice() 함수를 사용하면 배열의 일부분을 잘라낼 수 있음
삽입 지점 앞에 자른 배열을 전개하고, 새 항목과 원본 배열의 나머지 부분을 전개하는 배열을 만듦
// App.js
import { useState } from 'react';
let nextId = 3;
const initialArtists = [
{ id: 0, name: 'Marta Colvin Andrade' },
{ id: 1, name: 'Lamidi Olonade Fakeye'},
{ id: 2, name: 'Louise Nevelson'},
];
export default function List() {
const [name, setName] = useState('');
const [artists, setArtists] = useState(
initialArtists
);
function handleClick() {
const insertAt = 1; // 모든 인덱스가 될 수 있음
const nextArtists = [
// 삽입 지점 이전 항목
...artists.slice(0, insertAt),
// 새 항목
{ id: nextId++, name: name },
// 삽입 지점 이후 항목
...artists.slice(insertAt)
];
setArtists(nextArtists);
setName('');
}
return (
<>
<h1>Inspiring sculptors:</h1>
<input
value={name}
onChange={e => setName(e.target.value)}
/>
<button onClick={handleClick}>
Insert
</button>
<ul>
{artists.map(artist => (
<li key={artist.id}>{artist.name}</li>
))}
</ul>
</>
);
}
✔️ 배열에 기타 변경 적용하기
자바스크립트의 reverse() 및 sort() 함수는 원본 배열을 변경
--> 직접 사용하는 것은 권장하지 않음
먼저 배열을 복사한 뒤 변경 가능
// App.js
import { useState } from 'react';
const initialList = [
{ id: 0, title: 'Big Bellies' },
{ id: 1, title: 'Lunar Landscape' },
{ id: 2, title: 'Terracotta Army' },
];
export default function List() {
const [list, setList] = useState(initialList);
function handleClick() {
const nextList = [...list];
nextList.reverse();
setList(nextList);
}
return (
<>
<button onClick={handleClick}>
Reverse
</button>
<ul>
{list.map(artwork => (
<li key={artwork.id}>{artwork.title}</li>
))}
</ul>
</>
);
}
^ [...list] 전개 구문을 사용해 원본 배열의 복사본을 만듦
복사본이 있으므로 nextList.reverse() 또는 nextList.sort()와 같은 변경 함수를 사용하거나 nextList[0] = "something"과 같이 개별 항목 할당 가능
* 배열을 복사하더라도 배열 내부에 기존 항목을 직접 변경해서는 안 됨
ㄴ 얕은 복사이기 때문에 복사한 새 배열에는 원본 배열과 동일한 항목이 포함됨
--> 복사된 배열 내부의 객체를 수정하면 기존 state가 변경됨
const nextList = [...list];
nextList[0].seen = true; // 문제: list[0]을 변경시킴
setList(nextList);
^ nextList와 list는 서로 다른 배열이지만, nextList[0]과 list[0]은 동일한 객체를 가리킴
--> nextList[0].seen을 변경하면 list[0].seen도 변경됨
✔️ 배열 내부의 객체 업데이트하기
객체는 실제로 배열 내부에 위치해있지 않음
코드에서는 내부로 나타날 수 있지만, 배열의 각 객체는 배열이 가리키는 별도의 값
중첩된 state를 업데이트할 때, 업데이트하려는 지점부터 최상위 레벨까지의 복사본을 만들어야 함
// App.js
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, setMyList] = useState(initialList);
const [yourList, setYourList] = useState(
initialList
);
function handleToggleMyList(artworkId, nextSeen) {
const myNextList = [...myList];
const artwork = myNextList.find(
a => a.id === artworkId
);
artwork.seen = nextSeen;
setMyList(myNextList);
}
function handleToggleYourList(artworkId, nextSeen) {
const yourNextList = [...yourList];
const artwork = yourNextList.find(
a => a.id === artworkId
);
artwork.seen = nextSeen;
setYourList(yourNextList);
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>Your list of art to see:</h2>
<ItemList
artworks={yourList}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
const myNextList = [...myList];
const artwork = myNextList.find(a => a.id === artworkId);
artwork.seen = nextSeen; // 문제: 기존 항목을 변경시킵니다.
setMyList(myNextList);
^ myNextList 배열 자체는 새로운 배열이지만, 항목 자체는 myList 원본 배열과 동일
--> artwork.seen을 변경하면 원본 artwork 항목이 변경됨
ㄴ 해당 artwork 항목은 yourArtWorks에도 존재하므로 버그가 발생함
* map을 사용하면 이전 항목의 변경 없이 업데이트된 버전으로 대체 가능
// App.js
import { useState } from 'react';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, setMyList] = useState(initialList);
const [yourList, setYourList] = useState(
initialList
);
function handleToggleMyList(artworkId, nextSeen) {
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// 변경된 *새* 객체를 만들어 반환합니다.
return { ...artwork, seen: nextSeen };
} else {
// 변경시키지 않고 반환합니다.
return artwork;
}
}));
}
function handleToggleYourList(artworkId, nextSeen) {
setYourList(yourList.map(artwork => {
if (artwork.id === artworkId) {
// 변경된 *새* 객체를 만들어 반환합니다.
return { ...artwork, seen: nextSeen };
} else {
// 변경시키지 않고 반환합니다.
return artwork;
}
}));
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>Your list of art to see:</h2>
<ItemList
artworks={yourList}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// 변경된 *새* 객체를 만들어 반환
return { ...artwork, seen: nextSeen };
} else {
// 변경시키지 않고 반환
return artwork;
}
}));
✔️ Immer로 간결한 업데이트 로직 작성하기
변경 없이 중첩된 배열을 업데이트하는 것은 객체와 마찬가지로 반복적일 수 있음
- 일반적으로 깊은 레벨까지의 state를 업데이트할 필요는 없음
- state 객체가 매우 깊다면 다르게 재구성하여 평평하게 만들 수 있음
- state 구조를 변경하고 싶지 않다면 Immer을 사용할 수 있음
- 손쉽게 변경 문법을 사용하여 작성할 수 있고 복사본을 생성하여 처리 가능
// App.js
import { useState } from 'react';
import { useImmer } from 'use-immer';
let nextId = 3;
const initialList = [
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
];
export default function BucketList() {
const [myList, updateMyList] = useImmer(
initialList
);
const [yourArtworks, updateYourList] = useImmer(
initialList
);
function handleToggleMyList(id, nextSeen) {
updateMyList(draft => {
const artwork = draft.find(a =>
a.id === id
);
artwork.seen = nextSeen;
});
}
function handleToggleYourList(artworkId, nextSeen) {
updateYourList(draft => {
const artwork = draft.find(a =>
a.id === artworkId
);
artwork.seen = nextSeen;
});
}
return (
<>
<h1>Art Bucket List</h1>
<h2>My list of art to see:</h2>
<ItemList
artworks={myList}
onToggle={handleToggleMyList} />
<h2>Your list of art to see:</h2>
<ItemList
artworks={yourArtworks}
onToggle={handleToggleYourList} />
</>
);
}
function ItemList({ artworks, onToggle }) {
return (
<ul>
{artworks.map(artwork => (
<li key={artwork.id}>
<label>
<input
type="checkbox"
checked={artwork.seen}
onChange={e => {
onToggle(
artwork.id,
e.target.checked
);
}}
/>
{artwork.title}
</label>
</li>
))}
</ul>
);
}
updateMyTodos(draft => {
const artwork = draft.find(a => a.id === artworkId);
artwork.seen = nextSeen;
});
^ 원본 state를 변경하는 것이 아니라, Immer에서 제공하는 특수 draft 객체를 변경함
내부적으로 Immer는 항상 draft에서 수행한 변경 사항에 따라 처음부터 다음 state를 구성함
'React > 공식 문서' 카테고리의 다른 글
| useReducer (0) | 2026.05.21 |
|---|---|
| 상태 구조 설계와 공유 (0) | 2026.05.07 |
| 렌더링과 State 업데이트 (0) | 2026.04.08 |
| 이벤트 처리와 State (0) | 2026.04.02 |
| Props와 리스트 렌더링 (0) | 2026.03.25 |