state
는 객체를 포함해서, 어떤 종류의 JavaScript
값이든 저장할 수 있다.- 하지만
React state
에 있는 객체를 직접 변경해서는 안된다. - 대신 객체를 업데이트하려면 새 객체를 생성하고(또는 기존 복사본을 만들고), 그 객체를 사용하도록
state
를 설정해야 한다.
mutation
은 무엇인가?- 기술적으로 객체 자체의 내용을 변경하는 것은 가능하다. 이를
mutation
이라고 부른다.
React state
의 객체는 기술적으로 mutation
할 수 있지만 JavaScript
기본형처럼 불변하는 것으로 취급해야 한다.- 객체를 직접
mutation
하는 대신 항상 교체해야 한다.
state
를 읽기 전용으로 다루자- 즉,
state
에 넣는 모든 JavaScript
객체를 읽기 전용으로 취급해야 한다는 것이다.
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
값은 읽기 전용으로 취급해야 한다.
onPointerMove={e => {
setPosition({
x: e.clientX,
y: e.clientY
});
}}
Local mutation은 괜찮다.
const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);
setPosition({
x: e.clientX,
y: e.clientY
});
mutation
은 이미 state
가 있는 기존 객체를 변경할 때만 문제가 된다.
- 객체를 변경해도 해당 객체에 의존하는 다른 객체에 실수로 영향을 미치지 않는다.
- 이를
Local mutation
이라고 한다. - 렌더링하는 동안에도
Local mutation
을 수행할 수 있다.
- 전개 구문을 사용하여 객체 복사하기
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>
</>
);
}
- 이 경우 새 객체를 생성하고 이를
setPerson
에 전달해야 정상 동작한다. - 큰
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>
</>
);
}
여러 필드에 단일 이벤트 핸들러 사용하기
- 위의 코드는 아래처럼 동적 프로퍼티(계산된 프로퍼티 속성)를 지정해서 단일 이벤트 핸들러를 사용하도록 수정할 수도 있다.
import { useState } from 'react';
export default function Form() {
const [person, setPerson] = useState({
firstName: 'Barbara',
lastName: 'Hepworth',
email: 'bhepworth@sculpture.com'
});
function handleChange(e) {
setPerson({
...person,
[e.target.name]: e.target.value
});
}
return (
<>
<label>
First name:
<input
name="firstName"
value={person.firstName}
onChange={handleChange}
/>
</label>
<label>
Last name:
<input
name="lastName"
value={person.lastName}
onChange={handleChange}
/>
</label>
<label>
Email:
<input
name="email"
value={person.email}
onChange={handleChange}
/>
</label>
<p>
{person.firstName}{' '}
{person.lastName}{' '}
({person.email})
</p>
</>
);
}
- 중첩된 객체 업데이트하기
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
로 간결한 업데이트 로직 작성하기Immer
는 mutation
구문을 사용하여도 사본을 생성해주는 라이브러리이다.
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
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;
});
}
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}
/>
</>
);
}
React에서 state mutation을 권장하지 않는 이유
- 디버깅
console.log
를 사용하고 state
를 mutate
하지 않으면 과거 기록이 최근 mutate
에 지워지지 않는다.- 렌더링 사이에
state
가 어떻게 변경되는 지 명확하게 확인할 수 있다.
- 최적화
React
의 일반적인 최적화 전략은 이전 프로퍼티나 state
가 다음 프로퍼티나 state
와 동일한 경우 작업을 건너 뛰는 것에 의존한다.state
를 mutate
하지 않는다면 변경이 있었는지 확인하는 것이 매우 빠르다.
- 새롭게 추가될 React의 기능들
- 앞으로 추가될 기능들에서
state
는 스냅샷처럼 취급되는 것에 의존하기 때문에, 이를 잘 사용해야 한다.