상태 구조 선택하기
상태를 잘 구조화하면 수정과 디버깅이 편한 컴포넌트와 버그가 끊임없이 발생하는 컴포넌트의 차이를 만들 수 있습니다. 다음은 state를 구조화할 때 고려해야 할 몇 가지 팁입니다.
학습 내용
- 단일 상태 변수와 다중 상태 변수를 사용해야 하는 경우
- 상태 구성 시 피해야 할 사항
- 상태 구조와 관련된 일반적인 문제를 해결하는 방법
상태 구조화 원칙
어떤 상태를 보유하는 컴포넌트를 작성할 때는 얼마나 많은 상태 변수를 사용할지, 데이터의 형태는 어떤 것이 좋을지 선택해야 합니다. 차선의 상태 구조로도 올바른 프로그램을 작성할 수 있지만, 더 나은 선택을 할 수 있도록 안내하는 몇 가지 원칙이 있습니다:
- 관련 상태 그룹화. 항상 두 개 이상의 상태 변수를 동시에 업데이트하는 경우 하나의 상태 변수로 병합하는 것을 고려하세요.
- 상태의 모순을 피하세요. 여러 개의 상태가 서로 모순되거나 '불일치'할 수 있는 방식으로 상태가 구성되어 있으면 실수가 발생할 여지가 있습니다. 이를 피하세요.
- 중복(대안이 있거나 불필요한) 상태를 피하세요. 렌더링 중에 컴포넌트의 props나 기존 상태 변수에서 일부 정보를 계산할 수 있는 경우 해당 정보를 해당 컴포넌트의 상태에 넣지 않아야 합니다.
- 상태 중복(사본)을 피하세요. 동일한 데이터가 여러 상태 변수 간에 또는 중첩된 오브젝트 내에 중복되면 동기화 상태를 유지하기가 어렵습니다. 가능하면 중복을 줄이세요.
- 깊게 중첩된 상태는 피하세요. 깊게 계층화된 상태는 업데이트하기가 쉽지 않습니다. 가능하면 상태를 평평한 방식으로 구조화하는 것이 좋습니다.
이러한 원칙의 목표는 실수 없이 상태를 쉽게 업데이트할 수 있도록 하는 것입니다. state에서 중복 및 중복 데이터를 제거하면 모든 데이터가 동기화 상태를 유지하는 데 도움이 됩니다. 이는 데이터베이스 엔지니어가 버그 발생 가능성을 줄이기 위해 데이터베이스 구조를 '정규화'하는 것과 유사합니다. 알버트 아인슈타인의 말을 빌리자면, "상태를 최대한 단순하게 만들되, 그보다 더 단순해서는 안 됩니다."입니다.
이제 이러한 원칙이 실제로 어떻게 적용되는지 살펴보겠습니다.
그룹 관련 상태
단일 상태 변수를 사용할지 여러 상태 변수를 사용할지 고민될 때가 있습니다.
이렇게 해야 할까요?
const [x, setX] = useState(0);
const [y, setY] = useState(0);
아니면 이렇게?
const [position, setPosition] = useState({ x: 0, y: 0 });
기술적으로는 이 두 가지 접근 방식 중 하나를 사용할 수 있습니다. 하지만 두 개의 상태 변수가 항상 함께 변경되는 경우에는 하나의 상태 변수로 통합하는 것이 좋습니다. 그러면 커서를 움직이면 빨간색 점의 좌표가 모두 업데이트되는 이 예제에서처럼 항상 동기화 상태를 유지하는 것을 잊지 않을 수 있습니다:
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>
)
}
데이터를 객체나 배열로 그룹화하는 또 다른 경우는 얼마나 많은 다른 상태가 필요한지 모를 때입니다. 예를 들어, 사용자가 사용자 정의 필드를 추가할 수 있는 양식이 있을 때 유용합니다.
함정
상태 변수가 객체인 경우 다른 필드를 명시적으로 복사하지 않고는 그 안의 한 필드만 업데이트할 수 없다는 점을 기억하세요. 예를 들어, 위 예제에서는 y 속성이 전혀 없기 때문에 setPosition({ x: 100 })을 수행할 수 없습니다! 대신 x만 설정하려면 setPosition({ ...position, x: 100 })을 수행하거나 두 개의 상태 변수로 분할하여 setX(100)을 수행해야 합니다.
상태의 모순 피하기
다음은 isSending 및 isSent 상태 변수가 있는 호텔 피드백 양식입니다:
import { useState } from 'react';
export default function FeedbackForm() {
const [text, setText] = useState('');
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);
async function handleSubmit(e) {
e.preventDefault();
setIsSending(true);
await sendMessage(text);
setIsSending(false);
setIsSent(true);
}
if (isSent) {
return <h1>Thanks for feedback!</h1>
}
return (
<form onSubmit={handleSubmit}>
<p>How was your stay at The Prancing Pony?</p>
<textarea
disabled={isSending}
value={text}
onChange={e => setText(e.target.value)}
/>
<br />
<button
disabled={isSending}
type="submit"
>
Send
</button>
{isSending && <p>Sending...</p>}
</form>
);
}
// Pretend to send a message.
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
이 코드는 작동하지만 "불가능한" state에 대한 문을 열어두는 것입니다. 예를 들어, setIsSent와 setIsSending을 함께 호출하는 것을 잊어버리면 isSending과 isSent가 동시에 true가 되는 상황이 발생할 수 있습니다. 컴포넌트가 복잡할수록 무슨 일이 일어났는지 파악하기가 더 어려워집니다.
isSending과 isSent는 동시에 참이 되어서는 안 되므로 세 가지 유효한 상태 중 하나를 취할 수 있는 하나의 상태 변수로 대체하는 것이 좋습니다: '입력 중'(초기), '전송 중', '전송됨':
import { useState } from 'react';
export default function FeedbackForm() {
const [text, setText] = useState('');
const [status, setStatus] = useState('typing');
async function handleSubmit(e) {
e.preventDefault();
setStatus('sending');
await sendMessage(text);
setStatus('sent');
}
const isSending = status === 'sending';
const isSent = status === 'sent';
if (isSent) {
return <h1>Thanks for feedback!</h1>
}
return (
<form onSubmit={handleSubmit}>
<p>How was your stay at The Prancing Pony?</p>
<textarea
disabled={isSending}
value={text}
onChange={e => setText(e.target.value)}
/>
<br />
<button
disabled={isSending}
type="submit"
>
Send
</button>
{isSending && <p>Sending...</p>}
</form>
);
}
// Pretend to send a message.
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
가독성을 위해 일부 상수를 선언할 수 있습니다:
const isSending = status === 'sending';
const isSent = status === 'sent';
하지만 이건 상태 변수가 아니므로 서로 동기화되지 않을까 걱정할 필요가 없습니다.
중복 상태 방지
렌더링 중에 컴포넌트의 프롭이나 기존 상태 변수에서 일부 정보를 계산할 수 있다면 해당 정보를 해당 컴포넌트의 상태에 넣지 않아야 합니다.
예를 들어 이 폼을 생각해 보세요. 작동하지만 중복 상태를 찾을 수 있습니까?
import { useState } from 'react';
export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value);
}
return (
<>
<h2>Let’s check you in</h2>
<label>
First name:{' '}
<input
value={firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:{' '}
<input
value={lastName}
onChange={handleLastNameChange}
/>
</label>
<p>
Your ticket will be issued to: <b>{fullName}</b>
</p>
</>
);
}
이 양식에는 첫 번째 이름, 성, 전체 이름의 세 가지 상태 변수가 있습니다. 그러나 fullName은 중복됩니다. 렌더링 중에 언제든지 firstName과 lastName에서 fullName을 계산할 수 있으므로 state에서 제거하세요.
이렇게 하면 됩니다:
import { useState } from 'react';
export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = firstName + ' ' + lastName;
function handleFirstNameChange(e) {
setFirstName(e.target.value);
}
function handleLastNameChange(e) {
setLastName(e.target.value);
}
return (
<>
<h2>Let’s check you in</h2>
<label>
First name:{' '}
<input
value={firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:{' '}
<input
value={lastName}
onChange={handleLastNameChange}
/>
</label>
<p>
Your ticket will be issued to: <b>{fullName}</b>
</p>
</>
);
}
여기서 풀네임은 상태 변수가 아닙니다. 대신 렌더링 중에 계산됩니다:
const fullName = firstName + ' ' + lastName;
따라서 change 핸들러는 이를 업데이트하기 위해 특별한 작업을 수행할 필요가 없습니다. setFirstName 또는 setLastName을 호출하면 다시 렌더링이 트리거되고 다음 전체 이름이 새 데이터에서 계산됩니다.
Deep Dive - props를 상태 그대로 미러링하지 않기
중복 상태의 일반적인 예는 다음과 같은 코드입니다:
function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);
여기서 color 상태 변수는 messageColor props로 초기화됩니다. 문제는 나중에 부모 컴포넌트가 다른 값(예: '파란색' 대신 '빨간색'을 전달하면 색상 상태 변수가 업데이트되지 않는다는 것입니다! 상태는 첫 번째 렌더링 중에만 초기화됩니다.
그렇기 때문에 state 변수에 일부 props를 '미러링'하면 혼동을 일으킬 수 있습니다. 대신 코드에서 메시지컬러 프로퍼티를 직접 사용하세요. 더 짧은 이름을 지정하려면 상수를 사용하세요:
function Message({ messageColor }) {
const color = messageColor;
이렇게 하면 부모 컴포넌트에서 전달된 props와 동기화되지 않습니다.
"props를 state로 '미러링'하는 것은 특정 props에 대한 모든 업데이트를 무시하고 싶을 때만 적합합니다. 관례에 따라 props 이름을 initial 또는 default로 시작하여 새 값이 무시됨을 명확히 하세요:
function Message({ initialColor }) {
// The `color` state variable holds the *first* value of `initialColor`.
// Further changes to the `initialColor` prop are ignored.
const [color, setColor] = useState(initialColor);
상태 중복 방지
이 메뉴 목록 구성 요소를 사용하면 여러 가지 여행용 간식 중에서 하나의 간식을 선택할 수 있습니다:
import { useState } from 'react';
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
export default function Menu() {
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(
items[0]
);
return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map(item => (
<li key={item.id}>
{item.title}
{' '}
<button onClick={() => {
setSelectedItem(item);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}
현재 선택된 항목은 selectedItem 상태 변수에 객체로 저장됩니다. 그러나 이것은 좋지 않습니다. selectedItem의 내용은 항목 목록 내의 항목 중 하나와 동일한 객체입니다. 즉, 항목 자체에 대한 정보가 두 곳에 중복됩니다.
이것이 왜 문제가 될까요? 각 항목을 편집 가능하게 만들어 봅시다:
import { useState } from 'react';
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
export default function Menu() {
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(
items[0]
);
function handleItemChange(id, e) {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
title: e.target.value,
};
} else {
return item;
}
}));
}
return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map((item, index) => (
<li key={item.id}>
<input
value={item.title}
onChange={e => {
handleItemChange(item.id, e)
}}
/>
{' '}
<button onClick={() => {
setSelectedItem(item);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}
먼저 항목에서 "선택"을 클릭한 다음 편집하면 입력은 업데이트되지만 하단의 레이블에 편집 내용이 반영되지 않는 것을 확인할 수 있습니다. 이는 상태가 중복되어 선택된 항목을 업데이트하는 것을 잊었기 때문입니다.
선택한 항목도 업데이트할 수 있지만 중복을 제거하는 것이 더 쉬운 수정 방법입니다. 이 예제에서는 selectedItem 객체(항목 내부의 객체와 중복을 생성함) 대신 selectedId를 상태로 유지한 다음 항목 배열에서 해당 ID를 가진 항목을 검색하여 selectedItem을 가져옵니다:
import { useState } from 'react';
const initialItems = [
{ title: 'pretzels', id: 0 },
{ title: 'crispy seaweed', id: 1 },
{ title: 'granola bar', id: 2 },
];
export default function Menu() {
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);
const selectedItem = items.find(item =>
item.id === selectedId
);
function handleItemChange(id, e) {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
title: e.target.value,
};
} else {
return item;
}
}));
}
return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map((item, index) => (
<li key={item.id}>
<input
value={item.title}
onChange={e => {
handleItemChange(item.id, e)
}}
/>
{' '}
<button onClick={() => {
setSelectedId(item.id);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}.</p>
</>
);
}
(또는 선택한 인덱스를 상태로 유지할 수도 있습니다.)
상태는 다음과 같이 복제되었습니다:
- items = [{ id: 0, title: 'pretzels'}, ...]
- selectedItem = {id: 0, title: 'pretzels'}
하지만 변경 후에는 다음과 같이 됩니다: - items = [{ id: 0, title: 'pretzels'}, ...]
- selectedId = 0
중복이 사라지고 필수 상태만 유지됩니다!
이제 선택한 항목을 편집하면 아래 메시지가 즉시 업데이트됩니다. setItems가 다시 렌더링을 트리거하고 items.find(...)가 업데이트된 제목을 가진 항목을 찾기 때문입니다. 선택한 ID만 필수적이므로 선택한 항목을 상태로 유지할 필요가 없습니다. 나머지는 렌더링 중에 계산할 수 있습니다.
깊게 중첩된 상태 피하기
행성, 대륙, 국가로 구성된 여행 계획을 상상해 보세요. 이 예제에서처럼 중첩된 객체와 배열을 사용하여 상태를 구조화하고 싶을 수 있습니다:
export const initialTravelPlan = {
id: 0,
title: '(Root)',
childPlaces: [{
id: 1,
title: 'Earth',
childPlaces: [{
id: 2,
title: 'Africa',
childPlaces: [{
id: 3,
title: 'Botswana',
childPlaces: []
}, {
id: 4,
title: 'Egypt',
childPlaces: []
}, {
id: 5,
title: 'Kenya',
childPlaces: []
}, {
id: 6,
title: 'Madagascar',
childPlaces: []
}, {
id: 7,
title: 'Morocco',
childPlaces: []
}, {
id: 8,
title: 'Nigeria',
childPlaces: []
}, {
id: 9,
title: 'South Africa',
childPlaces: []
}]
}, {
id: 10,
title: 'Americas',
childPlaces: [{
id: 11,
title: 'Argentina',
childPlaces: []
}, {
id: 12,
title: 'Brazil',
childPlaces: []
}, {
id: 13,
title: 'Barbados',
childPlaces: []
}, {
id: 14,
title: 'Canada',
childPlaces: []
}, {
id: 15,
title: 'Jamaica',
childPlaces: []
}, {
id: 16,
title: 'Mexico',
childPlaces: []
}, {
id: 17,
title: 'Trinidad and Tobago',
childPlaces: []
}, {
id: 18,
title: 'Venezuela',
childPlaces: []
}]
}, {
id: 19,
title: 'Asia',
childPlaces: [{
id: 20,
title: 'China',
childPlaces: []
}, {
id: 21,
title: 'Hong Kong',
childPlaces: []
}, {
id: 22,
title: 'India',
childPlaces: []
}, {
id: 23,
title: 'Singapore',
childPlaces: []
}, {
id: 24,
title: 'South Korea',
childPlaces: []
}, {
id: 25,
title: 'Thailand',
childPlaces: []
}, {
id: 26,
title: 'Vietnam',
childPlaces: []
}]
}, {
id: 27,
title: 'Europe',
childPlaces: [{
id: 28,
title: 'Croatia',
childPlaces: [],
}, {
id: 29,
title: 'France',
childPlaces: [],
}, {
id: 30,
title: 'Germany',
childPlaces: [],
}, {
id: 31,
title: 'Italy',
childPlaces: [],
}, {
id: 32,
title: 'Portugal',
childPlaces: [],
}, {
id: 33,
title: 'Spain',
childPlaces: [],
}, {
id: 34,
title: 'Turkey',
childPlaces: [],
}]
}, {
id: 35,
title: 'Oceania',
childPlaces: [{
id: 36,
title: 'Australia',
childPlaces: [],
}, {
id: 37,
title: 'Bora Bora (French Polynesia)',
childPlaces: [],
}, {
id: 38,
title: 'Easter Island (Chile)',
childPlaces: [],
}, {
id: 39,
title: 'Fiji',
childPlaces: [],
}, {
id: 40,
title: 'Hawaii (the USA)',
childPlaces: [],
}, {
id: 41,
title: 'New Zealand',
childPlaces: [],
}, {
id: 42,
title: 'Vanuatu',
childPlaces: [],
}]
}]
}, {
id: 43,
title: 'Moon',
childPlaces: [{
id: 44,
title: 'Rheita',
childPlaces: []
}, {
id: 45,
title: 'Piccolomini',
childPlaces: []
}, {
id: 46,
title: 'Tycho',
childPlaces: []
}]
}, {
id: 47,
title: 'Mars',
childPlaces: [{
id: 48,
title: 'Corn Town',
childPlaces: []
}, {
id: 49,
title: 'Green Hill',
childPlaces: []
}]
}]
};
이제 이미 방문한 장소를 삭제하는 버튼을 추가하고 싶다고 가정해 보겠습니다. 어떻게 해야 할까요? 중첩된 상태를 업데이트하려면 변경된 부분부터 위쪽까지 개체의 복사본을 만들어야 합니다. 깊게 중첩된 장소를 삭제하려면 해당 장소의 상위 장소 체인 전체를 복사해야 합니다. 이러한 코드는 매우 장황할 수 있습니다.
상태가 너무 중첩되어 쉽게 업데이트할 수 없는 경우 상태를 "플랫"하게 만드는 것이 좋습니다. 다음은 이 데이터를 재구성할 수 있는 한 가지 방법입니다. 각 장소가 하위 장소의 배열을 갖는 트리와 같은 구조 대신 각 장소가 하위 장소 ID의 배열을 보유하도록 할 수 있습니다. 그런 다음 각 장소 ID에서 해당 장소로의 매핑을 저장할 수 있습니다.
이러한 데이터 재구조화는 데이터베이스 테이블을 보는 것과 비슷할 수 있습니다:
export const initialTravelPlan = {
0: {
id: 0,
title: '(Root)',
childIds: [1, 2, 4],
},
1: {
id: 1,
title: 'Earth',
childIds: [2, 1, 3, 4, 6]
},
2: {
id: 2,
title: 'Africa',
childIds: [3, 4, 5, 6]
},
3: {
id: 3,
title: 'Botswana',
childIds: []
},
4: {
id: 4,
title: 'Egypt',
childIds: []
},
5: {
id: 5,
title: 'Kenya',
childIds: []
},
6: {
id: 6,
title: 'Madagascar',
childIds: []
},
7: {
id: 7,
title: 'Morocco',
childIds: []
},
8: {
id: 8,
title: 'Nigeria',
childIds: []
},
9: {
id: 9,
title: 'South Africa',
childIds: []
},
10: {
id: 10,
title: 'Americas',
childIds: [],
}
};
이제 상태가 "플랫"("정규화 (normalized)"라고도 함)이 되었으므로 중첩된 항목을 업데이트하는 것이 더 쉬워졌습니다.
이제 장소를 제거하려면 두 단계의 상태만 업데이트하면 됩니다:
부모 장소의 업데이트된 버전은 childIds 배열에서 제거된 ID를 제외해야 합니다.
루트 '테이블' 객체의 업데이트된 버전에는 상위 장소의 업데이트된 버전이 포함되어야 합니다.
다음은 이를 수행하는 방법에 대한 예시입니다:
import { useState } from 'react';
import { initialTravelPlan } from './places.js';
export default function TravelPlan() {
const [plan, setPlan] = useState(initialTravelPlan);
function handleComplete(parentId, childId) {
const parent = plan[parentId];
// Create a new version of the parent place
// that doesn't include this child ID.
const nextParent = {
...parent,
childIds: parent.childIds
.filter(id => id !== childId)
};
// Update the root state object...
setPlan({
...plan,
// ...so that it has the updated parent.
[parentId]: nextParent
});
}
const root = plan[0];
const planetIds = root.childIds;
return (
<>
<h2>Places to visit</h2>
<ol>
{planetIds.map(id => (
<PlaceTree
key={id}
id={id}
parentId={0}
placesById={plan}
onComplete={handleComplete}
/>
))}
</ol>
</>
);
}
function PlaceTree({ id, parentId, placesById, onComplete }) {
const place = placesById[id];
const childIds = place.childIds;
return (
<li>
{place.title}
<button onClick={() => {
onComplete(parentId, id);
}}>
Complete
</button>
{childIds.length > 0 &&
<ol>
{childIds.map(childId => (
<PlaceTree
key={childId}
id={childId}
parentId={id}
placesById={placesById}
onComplete={onComplete}
/>
))}
</ol>
}
</li>
);
}
상태를 원하는 만큼 중첩할 수 있지만 "플랫"하게 만들면 많은 문제를 해결할 수 있습니다. 상태를 더 쉽게 업데이트할 수 있고 중첩된 객체의 다른 부분에 중복이 생기지 않도록 할 수 있습니다.
Deep Dive - 메모리 사용량 개선
이상적으로는 '테이블' 객체에서 삭제된 항목(및 그 하위 항목!)도 제거하여 메모리 사용량을 개선하는 것이 좋습니다. 이 버전은 그렇게 합니다. 또한 Immer를 사용하여 업데이트 로직을 더욱 간결하게 만들었습니다.
import { useImmer } from 'use-immer';
import { initialTravelPlan } from './places.js';
export default function TravelPlan() {
const [plan, updatePlan] = useImmer(initialTravelPlan);
function handleComplete(parentId, childId) {
updatePlan(draft => {
// Remove from the parent place's child IDs.
const parent = draft[parentId];
parent.childIds = parent.childIds
.filter(id => id !== childId);
// Forget this place and all its subtree.
deleteAllChildren(childId);
function deleteAllChildren(id) {
const place = draft[id];
place.childIds.forEach(deleteAllChildren);
delete draft[id];
}
});
}
const root = plan[0];
const planetIds = root.childIds;
return (
<>
<h2>Places to visit</h2>
<ol>
{planetIds.map(id => (
<PlaceTree
key={id}
id={id}
parentId={0}
placesById={plan}
onComplete={handleComplete}
/>
))}
</ol>
</>
);
}
function PlaceTree({ id, parentId, placesById, onComplete }) {
const place = placesById[id];
const childIds = place.childIds;
return (
<li>
{place.title}
<button onClick={() => {
onComplete(parentId, id);
}}>
Complete
</button>
{childIds.length > 0 &&
<ol>
{childIds.map(childId => (
<PlaceTree
key={childId}
id={childId}
parentId={id}
placesById={placesById}
onComplete={onComplete}
/>
))}
</ol>
}
</li>
);
}
때로는 중첩된 상태의 일부를 하위 컴포넌트로 이동하여 상태 중첩을 줄일 수도 있습니다. 이 방법은 항목에 마우스 커서가 있는지 여부와 같이 저장할 필요가 없는 임시 UI 상태에 적합합니다.
요약
- 두 상태 변수가 항상 함께 업데이트되는 경우, 두 변수를 하나로 병합하는 것을 고려하세요.
- "불가능한" 상태를 만들지 않도록 상태 변수를 신중하게 선택하세요.
- 업데이트 실수를 줄일 수 있는 방식으로 상태를 구조화하세요.
- 상태를 동기화할 필요가 없도록 중복 및 중복 상태를 피하세요.
- 특별히 업데이트를 방지하려는 경우가 아니라면 props를 state에 넣지 마세요.
- 선택과 같은 UI 패턴의 경우 개체 자체 대신 ID 또는 인덱스를 상태로 유지하세요.
- 깊게 중첩된 상태를 업데이트하는 것이 복잡하다면 플랫화하세요.
과제 1 / 4: 업데이트되지 않는 컴포넌트 수정하기
이 시계 컴포넌트는 색상과 시간이라는 두 가지 소품을 받습니다. 선택 상자에서 다른 색상을 선택하면 시계 컴포넌트는 부모 컴포넌트에서 다른 색상의 소품을 받습니다. 하지만 어떤 이유로 표시된 색상이 업데이트되지 않습니다. 왜 그럴까요? 문제를 해결하세요.
https://codesandbox.io/s/fix-a-component-thats-not-updating-dpzgqj?file=/Clock.js
과제 2 / 4: 작동하지 않는 포장 목록 수정하기
이 포장 목록에는 포장된 품목 수와 전체 품목 수를 보여주는 바닥글이 있습니다. 처음에는 정상적으로 작동하는 것 같지만 버그가 있습니다. 예를 들어, 품목을 포장 완료로 표시했다가 삭제하면 카운터가 올바르게 업데이트되지 않습니다. 카운터가 항상 올바르게 업데이트되도록 수정하세요.
https://codesandbox.io/s/fix-a-broken-packing-list-3gq6v5?file=/App.js
과제 3 / 4: 사라지는 선택 항목 수정하기
상태의 문자 목록이 있습니다. 특정 문자를 가리키거나 초점을 맞추면 해당 문자가 강조 표시됩니다. 현재 강조 표시된 글자는 highlightedLetter 상태 변수에 저장됩니다. 개별 문자에 "별표"를 표시하거나 "별표 해제"하여 상태의 문자 배열을 업데이트할 수 있습니다.
이 코드는 작동하지만 UI에 사소한 결함이 있습니다. "별표" 또는 "별표 해제"를 누르면 잠시 동안 강조 표시가 사라집니다. 그러나 포인터를 이동하거나 키보드로 다른 문자로 전환하면 곧바로 다시 나타납니다. 왜 이런 일이 발생하나요? 버튼 클릭 후 강조 표시가 사라지지 않도록 수정하세요.
https://codesandbox.io/s/fix-the-disappearing-selection-45m34p?file=/App.js
과제 4 / 4: 다중 선택 구현하기
이 예제에서 각 문자에는 선택된 것으로 표시하는 isSelected 프로퍼티와 onToggle 핸들러가 있습니다. 이 방법은 작동하지만 상태는 선택된Id(null 또는 ID)로 저장되므로 주어진 시간에 하나의 문자만 선택될 수 있습니다.
다중 선택을 지원하도록 상태 구조를 변경하세요. (어떻게 구조화할지 코드를 작성하기 전에 생각해 보세요.) 각 확인란은 다른 확인란과 독립적이어야 합니다. 선택한 문자를 클릭하면 선택이 해제되어야 합니다. 마지막으로 바닥글에 선택한 항목의 정확한 개수가 표시되어야 합니다.
https://codesandbox.io/s/implement-multiple-selection-2st5o6?file=/App.js