State는 객체를 포함한 모든 종류의 JavaScript 값을 담을 수 있어요. 하지만 React state에 있는 객체를 직접 변경해서는 안 돼요. 대신에, 객체를 업데이트하고 싶을 때는 새로운 객체를 생성하거나 (또는 기존 객체의 복사본을 만들어서), state가 그 복사본을 사용하도록 설정해야 해요.
State에는 어떤 종류의 JavaScript 값이든 저장할 수 있어요.
const [x, setX] = useState(0);
지금까지 여러분은 숫자, 문자열, 그리고 불리언(boolean) 값을 다뤄왔어요. 이런 종류의 JavaScript 값들은 "불변(immutable)"이에요. 즉, 변경할 수 없거나 "읽기 전용"이라는 뜻이죠. 값을 교체하기 위해 리렌더링을 트리거할 수 있어요:
setX(5);
x state가 0에서 5로 바뀌었지만, 숫자 0 자체는 바뀌지 않았어요. JavaScript에서 숫자, 문자열, 불리언 같은 내장된 원시 값들은 변경할 수 없어요.
이제 state에 있는 객체를 생각해봅시다:
const [position, setPosition] = useState({ x: 0, y: 0 });
기술적으로, 객체 자체의 내용을 변경하는 것은 가능해요. 이것을 변경(mutation)이라고 부릅니다:
position.x = 5;
하지만, React state에 있는 객체들이 기술적으로는 변경 가능(mutable)하더라도, 여러분은 그것들을 숫자, 불리언, 문자열처럼 불변인 것처럼 취급해야 해요. 변경하는 대신에, 항상 교체해야 합니다.
다시 말해서, state에 넣는 모든 JavaScript 객체를 읽기 전용으로 취급해야 해요.
이 예제는 현재 포인터 위치를 나타내는 객체를 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>
);
}
body { margin: 0; padding: 0; height: 250px; }
문제는 이 코드 부분에 있어요.
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 MovingDot() {
const [position, setPosition] = useState({
x: 0,
y: 0
});
return (
<div
onPointerMove={e => {
setPosition({
x: e.clientX,
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>
);
}
body { margin: 0; padding: 0; height: 250px; }
이런 코드는 문제가 돼요. state에 있는 기존 객체를 수정하기 때문이죠:
position.x = e.clientX;
position.y = e.clientY;
하지만 이런 코드는 완전히 괜찮아요. 방금 생성한 새로운 객체를 변경하는 것이기 때문이에요:
const nextPosition = {};
nextPosition.x = e.clientX;
nextPosition.y = e.clientY;
setPosition(nextPosition);
사실, 이것은 다음과 같이 쓰는 것과 완전히 동일해요:
setPosition({
x: e.clientX,
y: e.clientY
});
변경이 문제가 되는 것은 state에 이미 있는 기존 객체를 변경할 때뿐이에요. 방금 생성한 객체를 변경하는 것은 괜찮아요. 왜냐하면 아직 다른 코드가 그것을 참조하지 않기 때문이에요. 그것을 변경해도 그것에 의존하는 무언가에 실수로 영향을 주지 않아요. 이것을 "지역 변경(local mutation)"이라고 불러요. 렌더링 중에도 지역 변경을 할 수 있어요. 매우 편리하고 완전히 괜찮아요!
이전 예제에서, position 객체는 항상 현재 커서 위치로부터 새롭게 생성되었어요. 하지만 종종, 여러분이 생성하는 새로운 객체의 일부로 기존 데이터를 포함하고 싶을 거예요. 예를 들어, 폼에서 하나의 필드만 업데이트하고 다른 모든 필드에 대해서는 이전 값을 유지하고 싶을 수 있어요.
이 입력 필드들은 작동하지 않아요. onChange 핸들러들이 state를 변경하고 있기 때문이에요:
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>
</>
);
}
label { display: block; }
input { margin-left: 5px; margin-bottom: 5px; }
예를 들어, 이 라인은 과거 렌더링의 state를 변경하고 있어요:
person.firstName = e.target.value;
여러분이 원하는 동작을 얻는 확실한 방법은 새로운 객체를 생성하고 setPerson에 전달하는 것이에요. 하지만 여기서는, 하나의 필드만 변경되었기 때문에 기존 데이터도 그 안에 복사하고 싶어요:
setPerson({
firstName: e.target.value, // 입력에서 얻은 새로운 first name
lastName: person.lastName,
email: person.email
});
... 객체 전개(spread) 문법을 사용하면 모든 프로퍼티를 개별적으로 복사할 필요가 없어요.
setPerson({
...person, // 이전 필드들을 복사
firstName: e.target.value // 하지만 이것은 덮어쓰기
});
이제 폼이 작동해요!
각 입력 필드에 대해 별도의 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>
</>
);
}
label { display: block; }
input { margin-left: 5px; margin-bottom: 5px; }
... 전개 문법은 "얕다(shallow)"는 점을 주의하세요--한 레벨 깊이만 복사해요. 이것은 빠르지만, 중첩된 프로퍼티를 업데이트하고 싶다면 여러 번 사용해야 한다는 뜻이에요.
객체 정의 내에서 [와 ] 중괄호를 사용하여 동적인 이름을 가진 프로퍼티를 지정할 수도 있어요. 여기 같은 예제인데, 세 개의 서로 다른 이벤트 핸들러 대신 하나의 이벤트 핸들러를 사용하고 있어요:
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>
</>
);
}
label { display: block; }
input { margin-left: 5px; margin-bottom: 5px; }
여기서 e.target.name은 <input> DOM 요소에 주어진 name 프로퍼티를 참조해요.
다음과 같은 중첩된 객체 구조를 생각해보세요:
const [person, setPerson] = useState({
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
});
person.artwork.city를 업데이트하고 싶다면, 변경으로 하는 방법은 명확해요:
person.artwork.city = 'New Delhi';
하지만 React에서는, state를 불변으로 취급해야 해요! city를 변경하기 위해서는, 먼저 새로운 artwork 객체를 (이전 것의 데이터로 미리 채워서) 생성하고, 그런 다음 새로운 artwork을 가리키는 새로운 person 객체를 생성해야 해요:
const nextArtwork = { ...person.artwork, city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);
또는, 단일 함수 호출로 작성하면:
setPerson({
...person, // 다른 필드들 복사
artwork: { // 하지만 artwork는 교체
...person.artwork, // 같은 것으로
city: 'New Delhi' // 하지만 New Delhi에!
}
});
이것은 좀 장황해지지만, 많은 경우에 잘 작동해요:
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}
/>
</>
);
}
label { display: block; }
input { margin-left: 5px; margin-bottom: 5px; }
img { width: 200px; height: 200px; }
코드에서 이런 객체는 "중첩된" 것처럼 보여요:
let obj = {
name: 'Niki de Saint Phalle',
artwork: {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
}
};
하지만, "중첩"은 객체가 어떻게 동작하는지 생각하는 부정확한 방법이에요. 코드가 실행될 때, "중첩된" 객체 같은 것은 없어요. 실제로는 두 개의 서로 다른 객체를 보고 있는 거예요:
let obj1 = {
title: 'Blue Nana',
city: 'Hamburg',
image: 'https://i.imgur.com/Sd1AgUOm.jpg',
};
let obj2 = {
name: 'Niki de Saint Phalle',
artwork: obj1
};
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
};
만약 obj3.artwork.city를 변경한다면, obj2.artwork.city와 obj1.city 모두에 영향을 줄 거예요. 왜냐하면 obj3.artwork, obj2.artwork, 그리고 obj1은 같은 객체이기 때문이에요. 이것은 객체를 "중첩된" 것으로 생각할 때 이해하기 어려워요. 대신에, 그것들은 프로퍼티로 서로를 "가리키는" 별개의 객체들이에요.
만약 여러분의 state가 깊게 중첩되어 있다면, 평평하게 만드는 것을 고려할 수 있어요. 하지만, state 구조를 변경하고 싶지 않다면, 중첩된 전개의 단축키를 선호할 수 있어요. Immer는 편리하지만 변경하는 문법을 사용하여 작성할 수 있게 해주고 복사본 생성을 대신 처리해주는 인기 있는 라이브러리예요. Immer를 사용하면, 작성하는 코드가 "규칙을 어기고" 객체를 변경하는 것처럼 보여요:
updatePerson(draft => {
draft.artwork.city = 'Lagos';
});
하지만 일반적인 변경과 달리, 과거 state를 덮어쓰지 않아요!
Immer가 제공하는 draft는 Proxy라고 불리는 특별한 종류의 객체로, 여러분이 무엇을 하는지 "기록"해요. 그래서 원하는 만큼 자유롭게 변경할 수 있어요! 내부적으로, Immer는 draft의 어떤 부분이 변경되었는지 파악하고, 편집 내용을 포함하는 완전히 새로운 객체를 생성해요.
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;
});
}
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}
/>
</>
);
}
{
"dependencies": {
"immer": "1.7.3",
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest",
"use-immer": "0.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
label { display: block; }
input { margin-left: 5px; margin-bottom: 5px; }
img { width: 200px; height: 200px; }
이벤트 핸들러들이 얼마나 더 간결해졌는지 주목하세요. 단일 컴포넌트에서 useState와 useImmer를 원하는 만큼 섞어서 사용할 수 있어요. Immer는 업데이트 핸들러를 간결하게 유지하는 좋은 방법이에요, 특히 state에 중첩이 있고 객체를 복사하는 것이 반복적인 코드로 이어질 때 말이에요.
몇 가지 이유가 있어요:
console.log를 사용하고 state를 변경하지 않으면, 과거 로그가 최근 state 변경에 의해 지워지지 않아요. 따라서 렌더링 사이에 state가 어떻게 변경되었는지 명확하게 볼 수 있어요.prevObj === obj라면, 그 안에서 아무것도 변경되지 않았다고 확신할 수 있어요.실제로, React에서 state를 변경하는 것으로 종종 "잘 넘어갈" 수 있지만, 이 접근 방식을 염두에 두고 개발된 새로운 React 기능을 사용할 수 있도록 그렇게 하지 않기를 강력히 권장해요. 미래의 기여자들 그리고 어쩌면 미래의 여러분 자신도 고마워할 거예요!
{...obj, something: 'newValue'} 객체 전개 문법을 사용하여 객체의 복사본을 생성할 수 있어요.이 폼에는 몇 가지 버그가 있어요. 점수를 증가시키는 버튼을 몇 번 클릭해보세요. 증가하지 않는 것을 볼 수 있어요. 그런 다음 이름을 편집하면, 점수가 갑자기 변경 사항을 "따라잡는" 것을 볼 수 있어요. 마지막으로, 성을 편집하면 점수가 완전히 사라지는 것을 볼 수 있어요.
여러분의 과제는 이 모든 버그를 수정하는 것이에요. 수정하면서, 각각이 왜 발생하는지 설명하세요.
import { useState } from 'react';
export default function Scoreboard() {
const [player, setPlayer] = useState({
firstName: 'Ranjani',
lastName: 'Shettar',
score: 10,
});
function handlePlusClick() {
player.score++;
}
function handleFirstNameChange(e) {
setPlayer({
...player,
firstName: e.target.value,
});
}
function handleLastNameChange(e) {
setPlayer({
lastName: e.target.value
});
}
return (
<>
<label>
Score: <b>{player.score}</b>
{' '}
<button onClick={handlePlusClick}>
+1
</button>
</label>
<label>
First name:
<input
value={player.firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:
<input
value={player.lastName}
onChange={handleLastNameChange}
/>
</label>
</>
);
}
label { display: block; margin-bottom: 10px; }
input { margin-left: 5px; margin-bottom: 5px; }
다음은 두 버그가 모두 수정된 버전이에요:
import { useState } from 'react';
export default function Scoreboard() {
const [player, setPlayer] = useState({
firstName: 'Ranjani',
lastName: 'Shettar',
score: 10,
});
function handlePlusClick() {
setPlayer({
...player,
score: player.score + 1,
});
}
function handleFirstNameChange(e) {
setPlayer({
...player,
firstName: e.target.value,
});
}
function handleLastNameChange(e) {
setPlayer({
...player,
lastName: e.target.value
});
}
return (
<>
<label>
Score: <b>{player.score}</b>
{' '}
<button onClick={handlePlusClick}>
+1
</button>
</label>
<label>
First name:
<input
value={player.firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:
<input
value={player.lastName}
onChange={handleLastNameChange}
/>
</label>
</>
);
}
label { display: block; }
input { margin-left: 5px; margin-bottom: 5px; }
handlePlusClick의 문제는 player 객체를 변경했다는 거예요. 결과적으로, React는 리렌더링할 이유가 있다는 것을 알지 못했고, 화면의 점수를 업데이트하지 않았어요. 이것이 이름을 편집했을 때, state가 업데이트되어 리렌더링을 트리거했고 그것이 또한 화면의 점수를 업데이트한 이유예요.
handleLastNameChange의 문제는 기존 ...player 필드들을 새로운 객체로 복사하지 않았다는 거예요. 이것이 성을 편집한 후 점수가 사라진 이유예요.
정적 배경 위에 드래그 가능한 상자가 있어요. 선택 입력을 사용하여 상자의 색상을 변경할 수 있어요.
하지만 버그가 있어요. 상자를 먼저 움직인 다음 색상을 변경하면, 배경이 (움직이면 안 되는데!) 상자 위치로 "점프"할 거예요. 하지만 이것은 일어나면 안 돼요: Background의 position prop은 initialPosition으로 설정되어 있고, 이것은 { x: 0, y: 0 }이에요. 색상 변경 후에 왜 배경이 움직이나요?
버그를 찾아서 수정하세요.
예상치 못한 무언가가 변경되면, 변경이 있는 거예요. App.js에서 변경을 찾아서 수정하세요.
import { useState } from 'react';
import Background from './Background.js';
import Box from './Box.js';
const initialPosition = {
x: 0,
y: 0
};
export default function Canvas() {
const [shape, setShape] = useState({
color: 'orange',
position: initialPosition
});
function handleMove(dx, dy) {
shape.position.x += dx;
shape.position.y += dy;
}
function handleColorChange(e) {
setShape({
...shape,
color: e.target.value
});
}
return (
<>
<select
value={shape.color}
onChange={handleColorChange}
>
<option value="orange">orange</option>
<option value="lightpink">lightpink</option>
<option value="aliceblue">aliceblue</option>
</select>
<Background
position={initialPosition}
/>
<Box
color={shape.color}
position={shape.position}
onMove={handleMove}
>
Drag me!
</Box>
</>
);
}
import { useState } from 'react';
export default function Box({
children,
color,
position,
onMove
}) {
const [
lastCoordinates,
setLastCoordinates
] = useState(null);
function handlePointerDown(e) {
e.target.setPointerCapture(e.pointerId);
setLastCoordinates({
x: e.clientX,
y: e.clientY,
});
}
function handlePointerMove(e) {
if (lastCoordinates) {
setLastCoordinates({
x: e.clientX,
y: e.clientY,
});
const dx = e.clientX - lastCoordinates.x;
const dy = e.clientY - lastCoordinates.y;
onMove(dx, dy);
}
}
function handlePointerUp(e) {
setLastCoordinates(null);
}
return (
<div
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
style={{
width: 100,
height: 100,
cursor: 'grab',
backgroundColor: color,
position: 'absolute',
border: '1px solid black',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transform: `translate(
${position.x}px,
${position.y}px
)`,
}}
>{children}</div>
);
}
export default function Background({
position
}) {
return (
<div style={{
position: 'absolute',
transform: `translate(
${position.x}px,
${position.y}px
)`,
width: 250,
height: 250,
backgroundColor: 'rgba(200, 200, 0, 0.2)',
}} />
);
};
body { height: 280px; }
select { margin-bottom: 10px; }
문제는 handleMove 안의 변경이었어요. 이것이 shape.position을 변경했지만, 그것은 initialPosition이 가리키는 것과 같은 객체예요. 이것이 도형과 배경이 모두 움직이는 이유예요. (변경이기 때문에, 관련 없는 업데이트--색상 변경--가 리렌더링을 트리거할 때까지 화면에 반영되지 않아요.)
수정은 handleMove에서 변경을 제거하고, 전개 문법을 사용하여 도형을 복사하는 거예요. +=는 변경이므로, 일반 + 연산을 사용하도록 다시 작성해야 해요.
import { useState } from 'react';
import Background from './Background.js';
import Box from './Box.js';
const initialPosition = {
x: 0,
y: 0
};
export default function Canvas() {
const [shape, setShape] = useState({
color: 'orange',
position: initialPosition
});
function handleMove(dx, dy) {
setShape({
...shape,
position: {
x: shape.position.x + dx,
y: shape.position.y + dy,
}
});
}
function handleColorChange(e) {
setShape({
...shape,
color: e.target.value
});
}
return (
<>
<select
value={shape.color}
onChange={handleColorChange}
>
<option value="orange">orange</option>
<option value="lightpink">lightpink</option>
<option value="aliceblue">aliceblue</option>
</select>
<Background
position={initialPosition}
/>
<Box
color={shape.color}
position={shape.position}
onMove={handleMove}
>
Drag me!
</Box>
</>
);
}
import { useState } from 'react';
export default function Box({
children,
color,
position,
onMove
}) {
const [
lastCoordinates,
setLastCoordinates
] = useState(null);
function handlePointerDown(e) {
e.target.setPointerCapture(e.pointerId);
setLastCoordinates({
x: e.clientX,
y: e.clientY,
});
}
function handlePointerMove(e) {
if (lastCoordinates) {
setLastCoordinates({
x: e.clientX,
y: e.clientY,
});
const dx = e.clientX - lastCoordinates.x;
const dy = e.clientY - lastCoordinates.y;
onMove(dx, dy);
}
}
function handlePointerUp(e) {
setLastCoordinates(null);
}
return (
<div
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
style={{
width: 100,
height: 100,
cursor: 'grab',
backgroundColor: color,
position: 'absolute',
border: '1px solid black',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transform: `translate(
${position.x}px,
${position.y}px
)`,
}}
>{children}</div>
);
}
export default function Background({
position
}) {
return (
<div style={{
position: 'absolute',
transform: `translate(
${position.x}px,
${position.y}px
)`,
width: 250,
height: 250,
backgroundColor: 'rgba(200, 200, 0, 0.2)',
}} />
);
};
body { height: 280px; }
select { margin-bottom: 10px; }
이것은 이전 챌린지와 같은 버그가 있는 예제예요. 이번에는, Immer를 사용하여 변경을 수정하세요. 편의를 위해, useImmer가 이미 임포트되어 있으니, shape state 변수를 그것을 사용하도록 변경해야 해요.
import { useState } from 'react';
import { useImmer } from 'use-immer';
import Background from './Background.js';
import Box from './Box.js';
const initialPosition = {
x: 0,
y: 0
};
export default function Canvas() {
const [shape, setShape] = useState({
color: 'orange',
position: initialPosition
});
function handleMove(dx, dy) {
shape.position.x += dx;
shape.position.y += dy;
}
function handleColorChange(e) {
setShape({
...shape,
color: e.target.value
});
}
return (
<>
<select
value={shape.color}
onChange={handleColorChange}
>
<option value="orange">orange</option>
<option value="lightpink">lightpink</option>
<option value="aliceblue">aliceblue</option>
</select>
<Background
position={initialPosition}
/>
<Box
color={shape.color}
position={shape.position}
onMove={handleMove}
>
Drag me!
</Box>
</>
);
}
import { useState } from 'react';
export default function Box({
children,
color,
position,
onMove
}) {
const [
lastCoordinates,
setLastCoordinates
] = useState(null);
function handlePointerDown(e) {
e.target.setPointerCapture(e.pointerId);
setLastCoordinates({
x: e.clientX,
y: e.clientY,
});
}
function handlePointerMove(e) {
if (lastCoordinates) {
setLastCoordinates({
x: e.clientX,
y: e.clientY,
});
const dx = e.clientX - lastCoordinates.x;
const dy = e.clientY - lastCoordinates.y;
onMove(dx, dy);
}
}
function handlePointerUp(e) {
setLastCoordinates(null);
}
return (
<div
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
style={{
width: 100,
height: 100,
cursor: 'grab',
backgroundColor: color,
position: 'absolute',
border: '1px solid black',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transform: `translate(
${position.x}px,
${position.y}px
)`,
}}
>{children}</div>
);
}
export default function Background({
position
}) {
return (
<div style={{
position: 'absolute',
transform: `translate(
${position.x}px,
${position.y}px
)`,
width: 250,
height: 250,
backgroundColor: 'rgba(200, 200, 0, 0.2)',
}} />
);
};
body { height: 280px; }
select { margin-bottom: 10px; }
{
"dependencies": {
"immer": "1.7.3",
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest",
"use-immer": "0.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
이것은 Immer로 다시 작성된 솔루션이에요. 이벤트 핸들러들이 변경하는 방식으로 작성되었지만 버그가 발생하지 않는 것을 주목하세요. 이것은 내부적으로, Immer가 결코 기존 객체를 변경하지 않기 때문이에요.
import { useImmer } from 'use-immer';
import Background from './Background.js';
import Box from './Box.js';
const initialPosition = {
x: 0,
y: 0
};
export default function Canvas() {
const [shape, updateShape] = useImmer({
color: 'orange',
position: initialPosition
});
function handleMove(dx, dy) {
updateShape(draft => {
draft.position.x += dx;
draft.position.y += dy;
});
}
function handleColorChange(e) {
updateShape(draft => {
draft.color = e.target.value;
});
}
return (
<>
<select
value={shape.color}
onChange={handleColorChange}
>
<option value="orange">orange</option>
<option value="lightpink">lightpink</option>
<option value="aliceblue">aliceblue</option>
</select>
<Background
position={initialPosition}
/>
<Box
color={shape.color}
position={shape.position}
onMove={handleMove}
>
Drag me!
</Box>
</>
);
}
import { useState } from 'react';
export default function Box({
children,
color,
position,
onMove
}) {
const [
lastCoordinates,
setLastCoordinates
] = useState(null);
function handlePointerDown(e) {
e.target.setPointerCapture(e.pointerId);
setLastCoordinates({
x: e.clientX,
y: e.clientY,
});
}
function handlePointerMove(e) {
if (lastCoordinates) {
setLastCoordinates({
x: e.clientX,
y: e.clientY,
});
const dx = e.clientX - lastCoordinates.x;
const dy = e.clientY - lastCoordinates.y;
onMove(dx, dy);
}
}
function handlePointerUp(e) {
setLastCoordinates(null);
}
return (
<div
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
style={{
width: 100,
height: 100,
cursor: 'grab',
backgroundColor: color,
position: 'absolute',
border: '1px solid black',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transform: `translate(
${position.x}px,
${position.y}px
)`,
}}
>{children}</div>
);
}
export default function Background({
position
}) {
return (
<div style={{
position: 'absolute',
transform: `translate(
${position.x}px,
${position.y}px
)`,
width: 250,
height: 250,
backgroundColor: 'rgba(200, 200, 0, 0.2)',
}} />
);
};
body { height: 280px; }
select { margin-bottom: 10px; }
{
"dependencies": {
"immer": "1.7.3",
"react": "latest",
"react-dom": "latest",
"react-scripts": "latest",
"use-immer": "0.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}