안녕하세요! YB 이윤지 입니다 😊 뭔가 이번 주는 시간이 후루루룩 지나가는 듯한... 느낌이 드네요!
날씨가 부쩍 추워졌어요. 모두 감기 조심하시고 건강 유의하시고!
이번 주차 나리스 커리큘럼은 state 업데이트 큐. 객체 state 업데이트, 배열 state 업데이트와 props 였는데요.
state 업데이트 큐는 저번 주차 아티클에 내용이 꽤 있던 것 같아서 이번 주차는 객체 state 업데이트와 배열 state 업데이트에 대해 중점적으로 다뤄보겠습니다!
*이 아티클은 React 공식 문서를 기반으로 작성되었습니다.
객체 State 업데이트 하기
State는 객체를 포함한 모든 종류의 자바스크립트 값을 가질 수 있습니다.
하지만 React state가 가진 객체를 직접 변경해서는 안 됩니다.
객체를 업데이트하고 싶을 때는 새로운 객체를 생성하여 (또는 기존 객체의 복사본을 만들어),
state가 복사본을 사용하도록 해야 합니다.
변형 mutation ?
state에는 아무 JS 값을 넣을 수 있는데...
const [x, setX] = useState(0);
여태까지는 number, string, boolean 들만 다뤘다. 이런 JS 값들은 "Immutable" 하다. "읽기 전용"이다. - 바꿀 수 없다는 뜻.
값을 바꾸려면 리렌더링을 해야 합니다.
setX(5);
x state는 0 에서 5로 바뀌었지만, 사실 숫자 0 이 바뀐 것은 아닙니다.
number, string, boolean 같은 내장 type들은 바꿀 수가 없죠,
이제 state에 객체 object 를 넣을 때를 보자.
const [position, setPosition] = useState({ x: 0, y :0 });
엄밀히 말하면, 객체 그 자체 의 내용을 바꿀 수는 있습니다! 이것이 바로 mutation.
position.x = 5;
그러나, 리액트 state의 객체들이 엄밀히 mutable 함에도 불구하고, immutable 한 것처럼 다뤄야 하는데요!!
절대 mutate 직접 바꾸지 말고, replace 하자!
state는 읽기 전용으로 다루자
다시 말해, state에 담는 모든 JS 객체는 읽기 전용으로 다루자는 말이다.
아래 예시는 state에 객체를 담는다. 이 객체는 현재 포인터의 위치. 영역에서 커서를 움직이면 빨간 점도 같이 움직여야 하지만, 첫 위치에 가만히 있고 안움직인다.
아래 예시는 state에 객체를 담습니다.
이 객체는 현재 포인터의 위치.
영역에서 커서를 움직이면 빨간 점도 같이 움직여야 하지만, 첫 위치에 가만히 있고 안움직인다.
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 state에 할당된 객체를 바꾼다. 그러나 setState()를 사용하지 않으면 리액트는 그 객체가 바뀌었는지 알 수가 없고, 결론적으로 리액트는 아무것도 안하는거죠. 음식을 다 먹고 나서 주문을 바꾸려고 하는 것과 같다. 가끔 state를 mutate 하는 시도가 작동할 때가 있지만, 리액트는 권장하지 않는다.
렌더 안에서 state는 읽기 전용으로만 접근하는 게 맞습니다.
리렌더링을 불러오려면,
새로운 객체를 만들고 setState 인자로 넘겨줄것.
onPointerMove = {e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
setPosition에서, 리액트에게 이렇게 말해주는 것 :
- position state를 이 새로운 객체로 replace 해줘
- 그리고 나서 컴포넌트 리렌더링 해줘
이렇게 하면 빨간 점이 hover 시에 포인터를 따라오는 것을 볼 수 있습니다!
전개 문법으로 객체 복사하기
그렇다면, 폼에서 단 한 개의 필드만 수정하고, 나머지 모든 필드는 이전 값을 유지하고 싶을 때는 어떻게 할까요?
원하는 동작을 정확히 얻기 위해서는 새로운 객체를 생성해서 전달해야 합니다.
하지만! 단 하나의 필드를 수정하고 싶기 때문에 기존에 존재하는 다른 데이터를 복사해야 하는데
... ← 전개 구문 spread operator 사용하여 모든 프로퍼티를 복사할 필요가 없게끔 편하게 사용할 수 있습니다!
... spread operator는 "shallow" 얕은 복사라는 것을 기억합시다!
그러나, 중첩된 프로퍼티를 업데이트하고 싶을 때 여러번 사용해야 한다는 말이기도 하다.
객체는 엄밀히 말하면 중첩되지 않는다.
이 객체는 코드로 보기에 "nested" 된것처럼 보이지만:
let obj = {
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
};
객체의 작동을 설명할 때 'nesting'은 정확한 표현은 아님.
사실 두 개의 서로 다른 객체를 보고 있는 것임:
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1 // obj1을 point 하고 있다
};
obj1 객체는 obj2 객체 안에 있지 않다. 예를 들어, obj3 은 obj1를 가리킬 수 있다 :
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
let obj3 = {
name: 'Copycat',
artwork: obj1
};
obj1, obj2.artwork, obj3.artwork는 사실 다 같은 객체임. 그래서 obj3.artwork.city를 바꾸면 obj2.artwork.city와 obj1.city가 전부 바뀐다.
결론 : 객체는 nested 된 것이라기 보다는, 각 객체가 있고 서로를 프로퍼티로 "가리키고" 있는 것이다.
Immer로 간결한 갱신 로직 작성하기
State 가 깊이 중첩되어 있다면 평탄화를 고려해보자.
만약 State 구조를 바꾸고 싶지 않다면, 중첩 전개할 수 있는 간단한 방법이 있는데 그게 바로 Immer!
한 마디로 복사본 생성을 도와주는 라이브러리다.
useImmer 의 특징은
- 복잡한 상태 업데이트: 상태가 복잡하고 중첩된 객체나 배열을 포함하는 경우, 일반적인 상태 업데이트 로직을 작성하기가 까다로울 수 있다. 이 경우 useImmer를 사용하여 더 간단하고 가독성 있는 코드를 작성할 수 있다.
- 불변성 유지: React에서 상태를 업데이트할 때는 불변성을 유지해야 한다. Immer를 사용하면 불변성을 유지하면서도 마치 가변 객체를 직접 수정하는 것처럼 상태를 업데이트할 수 있다.
- 컴포넌트의 상태 업데이트 성능 최적화: Immer는 내부적으로 변경 사항을 추적하고 필요한 경우에만 실제로 새로운 상태를 생성한다. 이는 React 컴포넌트의 불필요한 리렌더링을 방지하고 상태 업데이트의 성능을 최적화할 수 있다.
이렇게 3 가지가 있다.
설치 방법
npm install immer use-immer
사용법
import React, { useState } from 'react';
import { useImmer } from 'use-immer';
function ExampleComponent() {
const [state, setState] = useImmer({ count: 0 });
const increment = () => {
setState(draft => {
draft.count += 1;
});
};
const decrement = () => {
setState(draft => {
draft.count -= 1;
});
};
return (
<View>
<Text>{state.count}</Text>
<Button title="+" onPress={increment} />
<Button title="-" onPress={decrement} />
</View>
);
}
위 코드에서는 useImmer를 사용하여 상태를 선언하고,
increment 함수와 decrement 함수를 만들어 상태를 변경한다.
상태를 변경할 때 draft라는 매개 변수를 받아서 새로운 상태를 반환한다.
draft는 변경될 수 있는 가상의 상태로, 변경 작업을 직접 수행한다.
배열 State 업데이트하기
배열은 JavaScript에서는 변경이 가능하지만, state로 저장할 때에는 변경할 수 없도록 처리해야 합니다. 객체와 마찬가지로, state에 저장된 배열을 업데이트하고 싶을 때에는, 새 배열을 생성(혹은 기존 배열의 복사본을 생성)한 뒤,
이 새 배열을 state로 두어 업데이트해야 합니다.
이 파트에서는,
React state 에서 배열의 항목을 추가, 삭제 또는 변경하는 방법
배열 내부의 객체를 업데이트 하는 방법
Immer로 덜 반복해서 배열을 복사하는 방법
이렇게 세 가지를 알아보도록 하겠습니다~!
아래의 참조표는 자주 사용되는 배열 연산자입니다.
리액트 상태 안에서 배열을 다룰 때, 왼쪽 열에 있는 메서드는 피하고 오른쪽 열에 있는 메서드를 사용하세요.
사용 지양(배열 변이) | 사용 추천(새 배열 반환) | |
추가 | push, unshift | concat, [...arr] 스프레드 구문 |
삭제 | pop, shift, splice | filter, slice |
대체 | splice, arr[i] = ... 할당 | map |
정렬 | reverse, sort | 먼저 배열을 복사하기 |
아니면, Immer를 사용해서 양 쪽 열 모두의 메서드를 사용할 수 있단 사실~!
1. 배열에 항목 추가하기
push() 는 배열을 변이시킵니다. 아마 다들 원하지 않겠죠?
대신 새로운 배열을 만드는 가장 쉬운 방법은 ... 배열 스프레드 구문을 사용하는 것!
setArtists( // 상태를 대체.
[ // 새로운 배열로 대체.
...artists, // 모든 기존 항목들을 갖고 있고
{ id: nextId++, name: name } // 끝에는 새로운 항목을 가진 새로운 배열로!
]
);
2. 배열에서 항목 제거하기
배열에서 항목을 삭제하는 가장 쉬운 방법은 해당 항목을 필터로 걸러내는 거예요.
즉, 해당 아이템을 갖고 있지 않은 새로운 배열을 만들기 위해 filter 메서드를 사용해야 합니다.
3. 배열 변환하기
만약 배열의 일부 또는 모든 항목을 바꾸고 싶다면 map()을 사용하여 새로운 배열을 생성할 수 있습니다.
map에 전달한 함수는 데이터 또는 인덱스(혹은 둘 다)에 기반 하여 어떤 아이템으로 무엇을 할 것인지를 결정하죠.
4. 배열 내 항목 교체하기
하나 또는 그 이상의 항목들을 배열에서 다른 값으로 대체하고픈 경우는 특히나 흔한 경우죠.
arr[0] = 'bird'와 같은 할당은 기존 배열을 변이시키기 때문에 여기서도 map을 사용할 수 있습니다.
항목을 대체하려면 새로운 배열을 map을 사용해서 만듭시다.
map 호출 안에서 두번에 인자로 항목의 인덱스를 받고 이를 사용하여 기존 항목(첫 번째 인자)를 반환해야하는지
혹은 다른 것들을 반환해야하는지를 결정하면 돼요.
5. 배열에 항목 삽입하기
때때로 처음도 끝도 아닌 임의의 위치에 항목을 추가하고 싶을 수도 있겠죠?
이를 위해서는 ... 배열 스프레드 구문을 slice() 메서드와 함꼐 사용해야 합니다.
slice() 메서드는 배열을 조각으로 잘라야해요!!
6. 배열 안에서 객체 업데이트 하기
객체는 실제로 배열 "안에" 있는 것이 아닙니다.
코드 "안에서는" 그렇게 보이겠지만 배열에 있는 각각의 객체는 배열이 "가리키는" 별개의 값!!
*기존 항목을 변이 없이 업데이트 된 버전으로 대체하려면 map 을 사용하자.
setMyList(myList.map(artwork => {
if (artwork.id === artworkId) {
// 바뀐 값을 넣어서 새로운 객체 생성하기
return { ...artwork, seen: nextSeen };
} else {
// 변화 없음
return artwork;
}
}));
like this
여기서 ... 은 객체의 복사본을 만드는데 사용된 객체 스프레드 문법이란 것, 이제 다들 아시죠?
7. Immer 로 간결하게 로직 업데이트 하기
중첩된 배열을 변이 없이 업데이트하는 것은... 좀 반복적이죠?
- 일반적으로 몇 단계 깊이 이상으로 상태를 업데이트 하지는 않습니다. 만약 상태 객체가 너무 깊다면, 이들을 다르게 재구조화 하여 평평하게 만들고 싶을 거예요.
- 반약 상태 구조를 바꾸고 싶지 않다면 Immer를 사용하는 것을 추천해요. 앞에서 아까 말했듯이, Immer는 편리하지만 변형 구문을 사용하여 작성하고 복사본을 생성할 수 있도록 만들어줍니다.
Recap | 요약
- 상태에 배열을 넣을 수 있지만 바꿀 수는 없어요.
- 배열을 변이하는 대신 새로운 배열을 생성하고 그 배열로 상태를 업데이트하세요.
- [...arr, newItem] 배열 스프레드 구문을 사용하여 새로운 항목으로 배열을 생성하세요.
- 걸러진 또는 변형된 항목들로 새로운 배열을 만들기 위해서는 filter()나 map()을 사용하세요.
- Immer를 사용하여 코드를 간결하게 유지할 수 있어요.

이것으로 4주차 아티클 마무리 하겠습니다!
다들 고생 많으셨어요 ㅎㅎ
개인적으로 이번 주차를 공부하면서 Immer을 처음 봤습니다.ㅎㅎ
Immer 을 사용하면 업데이트가 훨씬 간편해지는걸 알겠는데...
그렇다면 과제에서 왜 한 번도 Immer 을 보지 못했던 것일까? 싶기도 하고
언제 쓰는게 효과적일지, 아니면 되려 사용하지 않는 쪽이 더 나은 것인지....🧐
가 이번 주차의 궁금증이겠습니다!
읽어주셔서 감사합니다 ㅎㅎ
'나야, 리액트 스터디' 카테고리의 다른 글
[week5] State 관리하기 - State를 사용해 Input 다루기, State 구조 선택하기, 컴포넌트 간 State 공유하기, State를 보존하고 초기화하기 (3) | 2024.11.25 |
---|---|
[week4] ... state 왜 파도파도 계속 나와요? (batch, immer 등 리액트 state 변경에 대해 알아보자) (9) | 2024.11.18 |
[week4] - 상호작용성 더하기 - State 업데이트 큐, 객체 state 업데이트하기, 배열 state 업데이트하기 (3) | 2024.11.17 |
[week 4] 객체 state 업데이트하기, 배열 state 업데이트하기 (3) | 2024.11.17 |
[week4] 객체 state 업데이트 하기, 배열 state 업데이트하기 (3) | 2024.11.17 |