- Published on
리액트 공식문서 스터디 4-1 주차
- Authors

- Name
- junyeol kim
Updating Objects in State
State는 객체를 포함한 모든 종류의 자바스크립트 값을 가질 수 있다.
React state를 가진 객체를 직접 변경해서는 안되고, 업데이트 하고 싶을 때는 새로운 객체 or 기존 객체의 복사본을 생성하여 state가 복사본을 사용하도록 해야한다.
What's a mutation?
// 원시 타입 VS 참조 타입
// 원시 타입
let a = 10
let b = a
b = 20
console.log(a) // 10
// 참조 타입
let obj1 = { name: 'A' }
let obj2 = obj1
obj2.name = 'B'
console.log(obj1.name) // "B"
- 원시 타입은 불변성(
값을 변경할 수 없다)을 가진다.원시타입이란? JS에서 가장 기본이 되는 데이터 타입으로, Number, String, Boolean, Undefined, Null, Symbol, BigInt가 있다.
원시 타입의 특징으로는 값 자체가 복사되며, 독립적이며, 불변성을 띠고있다.
const [x, setX] = useState(0)
setX(5) // 0을 5로 교체!
const [position, setPosition] = useState({ x: 0, y: 0 })
position.x = 5
객체는 기술적으로 변경(
mutation)이 가능하다.하지만 React에서는 하면 안된다. React는 객체의 참조(주소)를 비교하여 변경을 감지하는데, 직접 변경하면 참조가 같아서 변경을 감지하지 못하기 때문이다.
따라서 React state의 객체는 불변성을 가진 것처럼 다뤄야 한다.
Treat state as read-only
// 잘못된 예시
const [position, setPosition] = useState({ x: 0, y: 0 })
function handleMove(e) {
position.x = e.clientX
position.y = e.clientY
}
객체를 직접 변경하기에 React가 감지를 못한다.
리렌더링이 일어나지않아, 화면이 업데이트가 되지 않는다.
// 올바른 예시
const [position, setPosition] = useState({ x: 0, y: 0 })
function handleMove(e) {
setPosition({
x: e.clientX,
y: e.clientY,
})
}
새로운 객체를 생성해서 전달을 한다.
그러므로 React가 변경을 감지해 리렌더링 한 후, 화면이 정상적으로 업데이트 된다.
Copying objects with the spread syntax
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com',
})
- 위 객체 상황에서 일부 필드만 업데이트하고 나머지는 유지하고 싶을때 사용하는 방법이 있다.
// 번거로운 방법
setPerson({
firstName: person.firstName,
lastName: person.lastName,
email: 'newemail@sculpture.com', // 이것만 변경
})
// spread 구문 사용
setPerson({
...person, // 기존 필드 복사
email: 'newemail@sculpture.com', // 이것만 덮어쓰기
})
- spread 구문의 장점은 간결하고, 유지보수가 용이하며, 실수를 방지할 수 있다.
// 여러 필드를 업데이트 하고 싶을 때
// 핸들러를 여러개 사용하는 상황은 생략
function handleChange(e) {
setPerson({
...person,
[e.target.name]: e.target.value // 동적 속성명
});
}
<input name="firstName" onChange={handleChange} />
<input name="lastName" onChange={handleChange} />
<input name="email" onChange={handleChange} />
Updating a nested object
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://...',
},
})
- 중첩 객체 구조에서도 동일하게 spread 구문을 이용하여 객체를 변경을 해야한다.
setPerson({
...person, // 최상위 복사
artwork: {
...person.artwork, // artwork도 복사
city: 'New Delhi', // city만 변경
},
})
- 만약 더 깊은 중첩 구조를 변경해야한다면, 위와 같은 방식이 아니라
Immer을 사용하는 것을 권장한다.
Write concise update logic with Immer
state가 깊이 중첩되어있다면, flattening를 고려해야한다.
state 구조를 바꾸지않고, 중첩 전개를 할 수 있는 간편한 방법으로는 Immer 라이브러리가 있다.
Immer을 사용하면 코드의 법칙을 무시하고 객체를 변경하는 것 처럼 보일 수 있다.
import { useImmer } from 'use-immer'
const [person, updatePerson] = useImmer({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
},
})
// 일반적인 useState 방식
setPerson({
...person,
artwork: {
...person.artwork,
city: 'New Delhi',
},
})
// Immer 방식
updatePerson((draft) => {
draft.artwork.city = 'New Delhi'
// 직접 변경하는 것 처럼 보임
})
draft는Proxy특성을 가진 특별한 객체이다.draft가 변경되는 순간을 감지하여,Immer가 자동으로 새로운 객체를 생성한다.코드는 직접 변경하는 것처럼 보이지만, 내부적으로는 불변성이 유지된다.
그렇기에 원본 객체는 절대 변경되지 않으며, 변경된 부분만 새로 만들어 반환한다.
Updating Arrays in State
배열은 JavaScript에서 가변적이지만, state에 저장할 때는 불변성을 유지해야 한다.
객체와 마찬가지로, 배열을 업데이트하려면 새 배열을 생성하거나 기존 배열의 복사본을 만들어야 한다.
Updating arrays without mutation
- JavaScript에서 배열은 객체의 한 종류이므로 배열을 직접 변경하면 안 된다.
arr[0] = 'bird'같은 직접 할당을 피해야 한다.push(),pop()같은 변경 메서드를 피해야 한다.
배열 조작 방법 비교:
| 동작 | 비권장 (배열 변경) | 권장 (새 배열 반환) |
|---|---|---|
| 추가 | push, unshift | concat, [...arr] |
| 제거 | pop, shift, splice | filter, slice |
| 교체 | splice, arr[i] = ... | map |
| 정렬 | reverse, sort | 배열 복사 후 정렬 |
Adding to an array
const [artists, setArtists] = useState([])
// 잘못된 방법
// push는 원본 배열을 변경
artists.push({ id: nextId++, name: name })
// 올바른 방법
// 배열 끝에 추가
setArtists([...artists, { id: nextId++, name: name }])
// 배열 앞에 추가
setArtists([{ id: nextId++, name: name }, ...artists])
Removing from an array
const [artists, setArtists] = useState([
{ id: 0, name: 'Sarah' },
{ id: 1, name: 'Ben' },
{ id: 2, name: 'Clara' },
])
// id가 1이 아닌 항목만 남김
setArtists(artists.filter((a) => a.id !== 1))
filter 메서드:
- 조건을 만족하는 항목만 포함하는 새 배열 생성
- 원본 배열은 변경하지 않음
Transforming an array
const [shapes, setShapes] = useState([
{ id: 0, type: 'circle', x: 50, y: 100 },
{ id: 1, type: 'square', x: 150, y: 100 },
{ id: 2, type: 'circle', x: 250, y: 100 },
])
// 모든 원을 아래로 50px 이동
const nextShapes = shapes.map((shape) => {
if (shape.type === 'circle') {
return { ...shape, y: shape.y + 50 } // 새 객체 생성
} else {
return shape // 변경 없으면 그대로 반환
}
})
setShapes(nextShapes)
map 메서드:
- 배열의 일부 또는 모든 항목을 변경하고 싶을 때
- 각 항목을 변환한 새 배열 생성
Replacing items in an array
const [counters, setCounters] = useState([0, 0, 0])
// 인덱스 1의 값을 증가
const nextCounters = counters.map((c, i) => {
if (i === 1) {
return c + 1 // 새 값 반환
} else {
return c // 기존 값 유지
}
})
setCounters(nextCounters)
- 삼항 연산자를 사용하면 더 간결하게 작성할 수 있다:
counters.map((c, i) => i === 1 ? c + 1 : c)
Inserting into an array
const [artists, setArtists] = useState([
{ id: 0, name: 'Sarah' },
{ id: 1, name: 'Ben' },
{ id: 2, name: 'Clara' },
])
// 인덱스 1에 새 항목 삽입
const insertAt = 1
const nextArtists = [
...artists.slice(0, insertAt), // 앞부분 복사
{ id: nextId++, name: name }, // 새 항목
...artists.slice(insertAt), // 뒷부분 복사
]
setArtists(nextArtists)
slice 메서드 :
배열의 일부를 복사하여 새 배열 생성
slice(start, end): start부터 end 이전까지 복사원본 배열은 변경하지 않음
const arr = [1, 2, 3, 4, 5]
arr.slice(0, 2) // [1, 2]
arr.slice(2) // [3, 4, 5]
Making other changes to an array
const [list, setList] = useState([3, 1, 2])
// 잘못된 방법 - reverse()와 sort()는 원본 변경
list.reverse()
list.sort()
// 올바른 방법 - 먼저 복사, 그다음 변경
function handleClick() {
const nextList = [...list] // 배열 복사
nextList.reverse() // 복사본 변경
setList(nextList)
}
// 더 간단한 방법
function handleClick() {
setList([...list].reverse())
}
Updating objects inside arrays
const [myList, setMyList] = useState([
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
{ id: 2, title: 'Terracotta Army', seen: true },
])
// 잘못된 방법 - 중첩 객체를 직접 변경
myList[0].seen = true
// 올바른 방법 - map으로 새 배열, spread로 새 객체
function handleToggle(artworkId, nextSeen) {
setMyList(
myList.map((artwork) => {
if (artwork.id === artworkId) {
return { ...artwork, seen: nextSeen } // 새 객체 생성
} else {
return artwork // 변경 없으면 그대로
}
})
)
}
배열과 객체의 관계:
객체는 실제로 배열 "내부"에 위치하지 않는다.
배열은 각 객체의 참조(메모리 주소) 를 저장한다.
따라서 배열을 복사해도 내부 객체는 같은 객체를 가리킨다.
// 메모리 관점에서 이해
const obj1 = { name: 'A' } // 메모리 주소: 0x001
const obj2 = { name: 'B' } // 메모리 주소: 0x002
const arr = [obj1, obj2] // [0x001, 0x002] 참조만 저장
// 배열을 복사해도
const newArr = [...arr] // [0x001, 0x002] 같은 참조 복사!
newArr[0] === arr[0] // true (같은 객체!)
// 따라서 내부 객체도 새로 만들어야 함
const newArr = arr.map((obj) => ({ ...obj })) // 새 객체들로 구성된 새 배열
Write concise update logic with Immer
import { useImmer } from 'use-immer'
const [myList, updateMyList] = useImmer([
{ id: 0, title: 'Big Bellies', seen: false },
{ id: 1, title: 'Lunar Landscape', seen: false },
])
// useState VS useImmer
// useState
setMyList(
myList.map((artwork) => {
if (artwork.id === artworkId) {
return { ...artwork, seen: nextSeen }
} else {
return artwork
}
})
)
// useImmer
updateMyList((draft) => {
const artwork = draft.find((a) => a.id === artworkId)
artwork.seen = nextSeen
})
Immer로 배열 조작:
// 추가
updateMyList((draft) => {
draft.push({ id: nextId++, name: name })
})
// 제거
updateMyList((draft) => {
const index = draft.findIndex((a) => a.id === artworkId)
draft.splice(index, 1)
})
// 삽입
updateMyList((draft) => {
draft.splice(insertAt, 0, { id: nextId++, name: name })
})
// 정렬
updateMyList((draft) => {
draft.sort((a, b) => a.name.localeCompare(b.name))
})
Immer의 장점 (배열):
push,pop,splice같은 변경 메서드를 자유롭게 사용 가능복잡한 중첩 구조에서 특히 유용
코드가 직관적이고 간결함