1. 변이란 무엇인가?
2. state를 읽기 전용으로 취급하라
3. 전개 구문을 사용하여 객체 복사하기
4. 중첩된 객체 업데이트하기
어떤 종류의 JavaScript 값이든 state에 저장할 수 있다.
const [x, setX] = useState(0);
지금까지 숫자, 문자열, 불리언값으로 작업해 보았다.
이러한 종류의 JavaScript 값은 “불변”, 즉,변이할 수 없거나 “읽기 전용”이다.
다시 렌더링을 트리거하여 값을 바꿀 수 있다.
setX(5);
x
state가 0
에서 5
로 변경 되었지만 숫자 0
자체는 변경되지 않았다.
JavaScript에서는 숫자, 문자열, 불리언과 같은 빌트인 원시 자료형 값을 변경할 수 없다.
객체 state를 살펴보면
const [position, setPosition] = useState({ x: 0, y: 0 });
기술적으로 객체 자체의 내용을 변경하는 것은 가능하다.
이를 변이(mutation) 라고 한다.
position.x = 5;
React state의 객체는 기술적으로는 변이할 수 있지만, 숫자, 불리언(boolean), 문자열과 같이 불변하는 것처럼 취급해야 한다.
객체를 직접 변이하는 대신, 항상 교체해야 한다.
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 설정자 함수를 사용하지 않으면 React는 객체가 변이되었다는 사실을 알지 못한다.
그래서 React는 아무 반응도 하지 않는다.
이미 음식을 다 먹은 후에 주문을 바꾸려고 하는 것과 같다.
state 변이는 경우에 따라 작동할 수 있지만 권장하지 않는다.
렌더링에서 접근할 수 있는 state 값은 읽기 전용으로 취급해야 합니다.
이 경우 실제로 리렌더링을 촉발하려면 새 객체를 생성하고 state 설정자 함수에 전달하라.
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
setPosition
으로 React에 다음을 지시한다position
을 이 새 객체로 바꿔라.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>
</>
);
}
예를 들어, 이 줄은 이전 렌더링 할때의 state를 변이한다.
person.firstName = e.target.value;
원하는 동작을 얻을 수 있는 가장 안정적인 방법은 새 객체를 생성하고 이를 setPerson에 전달하는 것이다.
하지만 여기서는 필드 중 하나만 변경되었으므로 기존 데이터도 복사하고 싶을 때
setPerson({
firstName: e.target.value, // New first name from the input
// input에서 받아온 새로운 first name
lastName: person.lastName,
email: person.email
});
모든 속성을 개별적으로 복사할 필요가 없도록 ... 객체 전개 구문을 사용할 수 있다.
setPerson({
...person, // Copy the old fields
// 이전 필드를 복사합니다.
firstName: e.target.value // But override this one
// 단, first name만 덮어씌웁니다.
});
각 입력 필드에 대해 별도의 state 변수를 선언하지 않은 것을 주목하자.
큰 양식의 경우 올바르게 업데이트하기만 하면 모든 데이터를 객체에 그룹화하여 보관하는 것이 매우 편리하다.
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>
</>
);
}
...
전개 구문은 “얕은” 구문으로, 한 단계 깊이만 복사한다는 점에 유의하자.
속도는 빠르지만 중첩된 프로퍼티를 업데이트하려면 두 번 이상 사용해야 한다는 뜻이기도 하다.
4-1. Immer로 간결한 업데이트 로직 작성
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
하지만 일반 변이와 달리 이전 state를 덮어쓰지 않는다.
Immer를 사용해 보려면
npm install use-immer
를 실행하여 Immer를 의존성으로 추가합니다.import { useState } from 'react'
를 import { useImmer } from 'use-immer'
로 바꿉니다.아래는 위의 예제를 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://i.imgur.com/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;
});
}
useState
와 useImmer
를 원하는 만큼 맞춰 사용할 수 있다.{...obj, something: 'newValue'}
를 사용하여 객체 사본을 만들 수 있다.https://developer.mozilla.org/ko/