Choosing the State StructureState 구조 선택

Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs. Here are some tips you should consider when structuring state. state를 잘 구조화하면 수정과 디버깅이 편한 컴포넌트와 버그가 끊임없이 발생하는 컴포넌트의 차이를 만들 수 있습니다. 다음은 state를 구조화할 때 고려해야 할 몇 가지 팁입니다.

You will learn학습 내용

  • When to use a single vs multiple state variables
  • What to avoid when organizing state
  • How to fix common issues with the state structure
  • 단일 state 변수를 사용해야 하는 경우 vs. 다중 state 변수를 사용해야 하는 경우
  • state 정리시 피해야 할 사항
  • state 구조와 관련된 일반적인 문제를 해결하는 방법

Principles for structuring statestate 구조화 원칙

When you write a component that holds some state, you’ll have to make choices about how many state variables to use and what the shape of their data should be. While it’s possible to write correct programs even with a suboptimal state structure, there are a few principles that can guide you to make better choices: 어떤 state를 보유하는 컴포넌트를 작성할 때엔 얼마나 많은 state 변수를 사용할지, 데이터의 모양은 어떻게 할지에 대해 선택해야 합니다. 최적화되지 않은 state 구조를 사용하더라도 올바른 프로그램을 작성할 수 있지만, 더 나은 선택을 할 수 있도록 안내하는 몇 가지 원칙이 있습니다:

  1. Group related state. If you always update two or more state variables at the same time, consider merging them into a single state variable.
  2. Avoid contradictions in state. When the state is structured in a way that several pieces of state may contradict and “disagree” with each other, you leave room for mistakes. Try to avoid this.
  3. Avoid redundant state. If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state.
  4. Avoid duplication in state. When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can.
  5. Avoid deeply nested state. Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way.
  1. 관련 state를 그룹화합니다. 항상 두 개 이상의 state 변수를 동시에 업데이트하는 경우 단일 state 변수로 병합하는 것이 좋습니다.
  2. state의 모순을 피하세요. 여러 state 조각이 서로 모순되거나 ‘불일치’할 수 있는 방식으로 state를 구성하면 실수가 발생할 여지가 생깁니다. 이를 피하세요.
  3. 불필요한 state를 피하세요. 렌더링 중에 컴포넌트의 props나 기존 state 변수에서 일부 정보를 계산할 수 있다면 해당 정보를 해당 컴포넌트의 state에 넣지 않아야 합니다.
  4. state 중복을 피하세요. 동일한 데이터가 여러 state 변수 간에 또는 중첩된 객체 내에 중복되면 동기화 state를 유지하기가 어렵습니다. 가능하면 중복을 줄이세요.
  5. 깊게 중첩된 state는 피하세요. 깊게 계층화된 state는 업데이트하기 쉽지 않습니다. 가능하면 state를 평평한 방식으로 구성하는 것이 좋습니다.

The goal behind these principles is to make state easy to update without introducing mistakes. Removing redundant and duplicate data from state helps ensure that all its pieces stay in sync. This is similar to how a database engineer might want to “normalize” the database structure to reduce the chance of bugs. To paraphrase Albert Einstein, “Make your state as simple as it can be—but no simpler.” 이러한 원칙의 목표는 실수 없이 state를 쉽게 업데이트할 수 있도록 하는 것입니다. state에서 불필요하거나 중복된 데이터를 제거하면 모든 데이터가 동기화 상태를 유지하는 데 도움이 됩니다. 이는 데이터베이스 엔지니어가 버그 발생 가능성을 줄이기 위해 데이터베이스 구조를 ‘정규화’하는 것과 유사합니다. 알버트 아인슈타인의 말을 빌리자면, “state를 최대한 단순하게 만들되, 그보다 더 단순해서는 안 됩니다.”

Now let’s see how these principles apply in action. 이제 이러한 원칙이 실제로 어떻게 적용되는지 살펴보겠습니다.

You might sometimes be unsure between using a single or multiple state variables. 단일 state 변수를 사용할지 다중 state 변수를 사용할지 고민할 때가 있습니다.

Should you do this? 아래처럼 할까요?

const [x, setX] = useState(0);
const [y, setY] = useState(0);

Or this? 아니면 이렇게 할까요?

const [position, setPosition] = useState({ x: 0, y: 0 });

Technically, you can use either of these approaches. But if some two state variables always change together, it might be a good idea to unify them into a single state variable. Then you won’t forget to always keep them in sync, like in this example where moving the cursor updates both coordinates of the red dot: 기술적으로는 이 두 가지 접근 방식 중 하나를 사용할 수 있습니다. 하지만 두 개의 state 변수가 항상 함께 변경되는 경우에는 하나의 state 변수로 통합하는 것이 좋습니다. 그러면 커서를 움직이면 빨간색 점의 좌표가 모두 업데이트되는 이 예제에서처럼 항상 동기화 상태를 유지하는 것을 잊지 않을 수 있습니다:

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>
  )
}

Another case where you’ll group data into an object or an array is when you don’t know how many pieces of state you’ll need. For example, it’s helpful when you have a form where the user can add custom fields. 데이터를 객체나 배열로 그룹화하는 또 다른 경우는 필요한 state의 조각 수를 모를 때입니다. 예를 들어, 사용자가 사용자 정의 필드를 추가할 수 있는 양식이 있을 때 유용합니다.

Pitfall | 함정

If your state variable is an object, remember that you can’t update only one field in it without explicitly copying the other fields. For example, you can’t do setPosition({ x: 100 }) in the above example because it would not have the y property at all! Instead, if you wanted to set x alone, you would either do setPosition({ ...position, x: 100 }), or split them into two state variables and do setX(100). state 변수가 객체인 경우 다른 필드를 명시적으로 복사하지 않고는 그 안의 한 필드만 업데이트할 수는 없다는 점을 기억하세요. 예를 들어, 위 예제에서는 y 속성이 전혀 없기 때문에 setPosition({ x: 100 })을 수행할 수 없습니다! 대신 x만 설정하려면 setPosition({ ...position, x: 100 })을 수행하거나 두 개의 state 변수로 분할하여 setX(100)을 수행해야 합니다.

Avoid contradictions in statestate의 모순을 피하세요

Here is a hotel feedback form with isSending and isSent state variables: 다음은 isSendingisSent state 변수가 있는 호텔 피드백 양식입니다:

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);
  });
}

While this code works, it leaves the door open for “impossible” states. For example, if you forget to call setIsSent and setIsSending together, you may end up in a situation where both isSending and isSent are true at the same time. The more complex your component is, the harder it is to understand what happened. 이 코드는 작동하긴 하지만, “불가능한” state의 설정을 허용하고 있습니다. 예를 들어, setIsSentsetIsSending 중 어느 하나만 호출하면, isSendingisSent가 동시에 true가 되는 상황이 발생할 수 있습니다. 컴포넌트가 복잡할수록 무슨 일이 일어났는지 파악하기가 더 어려워집니다.

Since isSending and isSent should never be true at the same time, it is better to replace them with one status state variable that may take one of three valid states: 'typing' (initial), 'sending', and 'sent': isSendingisSent는 동시에 true가 되어서는 안되므로, 세 가지 유효한 state 중 하나를 취할 수 있는 status 라는 state변수 하나로 대체하는 것이 좋습니다: statustyping(초기), sending, sent가 될 수 있습니다:

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);
  });
}

You can still declare some constants for readability: 가독성을 위해 일부 상수를 선언할 수 있습니다:

const isSending = status === 'sending';
const isSent = status === 'sent';

But they’re not state variables, so you don’t need to worry about them getting out of sync with each other. 하지만 state 변수가 아니므로 서로 동기화되지 않을까 걱정할 필요가 없습니다.

Avoid redundant state불필요한 state를 피하세요

If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state. 렌더링 중에 컴포넌트의 props나 기존 state 변수에서 일부 정보를 계산할 수 있는 경우 해당 정보를 컴포넌트의 state에 넣지 않아야 합니다.

For example, take this form. It works, but can you find any redundant state in it? 예를 들어, 이 양식을 살펴보세요. 작동하긴 하지만, 불필요한 state가 있진 않나요?

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>
    </>
  );
}

This form has three state variables: firstName, lastName, and fullName. However, fullName is redundant. You can always calculate fullName from firstName and lastName during render, so remove it from state. 이 양식에는 firstName, lastName, fullName의 세 가지 state 변수가 있습니다. 그러나 fullName은 불필요합니다. 렌더링 중에 언제든지 firstNamelastName에서 fullName을 계산할 수 있으므로 state에서 제거하세요.

This is how you can do it: 이렇게 하면 됩니다:

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>
    </>
  );
}

Here, fullName is not a state variable. Instead, it’s calculated during render: 여기서 fullName 은 state 변수가 아닙니다. 대신 렌더링 중에 계산됩니다:

const fullName = firstName + ' ' + lastName;

As a result, the change handlers don’t need to do anything special to update it. When you call setFirstName or setLastName, you trigger a re-render, and then the next fullName will be calculated from the fresh data. 따라서 변경 핸들러는 이를 업데이트하기 위해 특별한 작업을 수행할 필요가 없습니다. setFirstName 또는 setLastName을 호출하면 다시 렌더링을 촉발하고 다음 fullName이 새 데이터에서 계산됩니다.

Deep Dive | 심층 탐구

Don’t mirror props in stateprops를 state에 그대로 미러링하지 마세요

A common example of redundant state is code like this: 다음 코드는 중복 state의 일반적인 예시입니다:

function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);

Here, a color state variable is initialized to the messageColor prop. The problem is that if the parent component passes a different value of messageColor later (for example, 'red' instead of 'blue'), the color state variable would not be updated! The state is only initialized during the first render. 여기서 color state 변수는 messageColor props로 초기화됩니다. 문제는 부모 컴포넌트가 나중에 다른 messageColor 값(예: blue 대신 red)을 전달하면 color state 변수가 업데이트되지 않는다는 것입니다! state는 첫 번째 렌더링 중에만 초기화됩니다.

This is why “mirroring” some prop in a state variable can lead to confusion. Instead, use the messageColor prop directly in your code. If you want to give it a shorter name, use a constant: 그렇기 때문에 state 변수에 일부 prop을 ‘미러링’하면 혼란을 초래할 수 있습니다. 대신 코드에서 messageColor prop을 직접 사용하세요. 더 짧은 이름을 지정하려면 상수를 사용하세요:

function Message({ messageColor }) {
const color = messageColor;

This way it won’t get out of sync with the prop passed from the parent component. 이렇게 하면 부모 컴포넌트에서 전달된 prop과 동기화되지 않습니다.

”Mirroring” props into state only makes sense when you want to ignore all updates for a specific prop. By convention, start the prop name with initial or default to clarify that its new values are ignored: props를 state로 ‘미러링’하는 것은 특정 prop에 대한 모든 업데이트를 무시하려는 경우에만 의미가 있습니다. 관례에 따라 prop 이름을 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);

Avoid duplication in statestate 중복을 피하세요

This menu list component lets you choose a single travel snack out of several: 이 메뉴 목록 컴포넌트를 사용하면 여러 가지 여행용 간식 중 하나를 선택할 수 있습니다:

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>
    </>
  );
}

Currently, it stores the selected item as an object in the selectedItem state variable. However, this is not great: the contents of the selectedItem is the same object as one of the items inside the items list. This means that the information about the item itself is duplicated in two places. 현재 선택된 항목은 selectedItem state 변수에 객체로 저장됩니다. 그러나 이것은 좋지 않습니다. selectedItem의 내용은 items 목록 내의 항목 중 하나와 동일한 객체입니다. 즉, 항목 자체에 대한 정보가 두 곳에 중복됩니다.

Why is this a problem? Let’s make each item editable: 이것이 왜 문제가 될까요? 각 항목을 편집 가능하게 만들어 보겠습니다:

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>
    </>
  );
}

Notice how if you first click “Choose” on an item and then edit it, the input updates but the label at the bottom does not reflect the edits. This is because you have duplicated state, and you forgot to update selectedItem. 먼저 항목에서 “Choose”을 클릭한 다음 편집하면 입력은 업데이트되지만 하단의 레이블에 편집 내용이 반영되지 않는 것을 확인할 수 있습니다. 이는 state가 중복되어 있고 selectedItem을 업데이트하는 것을 잊었기 때문입니다.

Although you could update selectedItem too, an easier fix is to remove duplication. In this example, instead of a selectedItem object (which creates a duplication with objects inside items), you hold the selectedId in state, and then get the selectedItem by searching the items array for an item with that ID: selectedItem도 업데이트할 수 있지만 중복을 제거하는 것이 더 쉬운 수정 방법입니다. 이 예제에서는 selectedItem 객체(items 내부의 객체 복사본을 생성하는) 대신 selectedId를 state로 유지한 다음 items 배열에서 해당 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>
    </>
  );
}

The state used to be duplicated like this: 이전에는 state가 이렇게 복제되었습니다:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedItem = {id: 0, title: 'pretzels'}

But after the change it’s like this: 하지만 변경 후에는 이렇게 되었습니다:

  • items = [{ id: 0, title: 'pretzels'}, ...]
  • selectedId = 0

The duplication is gone, and you only keep the essential state! 중복은 사라지고 필수 state만 유지됩니다!

Now if you edit the selected item, the message below will update immediately. This is because setItems triggers a re-render, and items.find(...) would find the item with the updated title. You didn’t need to hold the selected item in state, because only the selected ID is essential. The rest could be calculated during render. 이제 선택한 항목을 편집하면 아래 메시지가 즉시 업데이트됩니다. 이는 setItems가 리렌더링을 촉발하고 items.find(...)가 업데이트된 제목의 항목을 찾기 때문입니다. 선택한 ID만 필수적이므로 선택한 항목을 state로 유지할 필요가 없습니다. 나머지는 렌더링 중에 계산할 수 있습니다.

Avoid deeply nested state깊게 중첩된 state는 피하세요

Imagine a travel plan consisting of planets, continents, and countries. You might be tempted to structure its state using nested objects and arrays, like in this example: 행성, 대륙, 국가로 구성된 여행 계획을 상상해 보세요. 이 예제에서처럼 중첩 객체와 배열을 사용하여 state를 구조화하고 싶을 수 있습니다:

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: 'India',
        childPlaces: []
      }, {
        id: 22,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 23,
        title: 'South Korea',
        childPlaces: []
      }, {
        id: 24,
        title: 'Thailand',
        childPlaces: []
      }, {
        id: 25,
        title: 'Vietnam',
        childPlaces: []
      }]
    }, {
      id: 26,
      title: 'Europe',
      childPlaces: [{
        id: 27,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 28,
        title: 'France',
        childPlaces: [],
      }, {
        id: 29,
        title: 'Germany',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Turkey',
        childPlaces: [],
      }]
    }, {
      id: 34,
      title: 'Oceania',
      childPlaces: [{
        id: 35,
        title: 'Australia',
        childPlaces: [],
      }, {
        id: 36,
        title: 'Bora Bora (French Polynesia)',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Easter Island (Chile)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Hawaii (the USA)',
        childPlaces: [],
      }, {
        id: 40,
        title: 'New Zealand',
        childPlaces: [],
      }, {
        id: 41,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 42,
    title: 'Moon',
    childPlaces: [{
      id: 43,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 44,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 45,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 46,
    title: 'Mars',
    childPlaces: [{
      id: 47,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 48,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

Now let’s say you want to add a button to delete a place you’ve already visited. How would you go about it? Updating nested state involves making copies of objects all the way up from the part that changed. Deleting a deeply nested place would involve copying its entire parent place chain. Such code can be very verbose. 이제 이미 방문한 장소를 삭제하는 버튼을 추가하고 싶다고 가정해 보겠습니다. 어떻게 해야 할까요? 중첩된 state를 업데이트하려면 변경된 부분부터 위쪽까지 객체의 복사본을 만들어야 합니다. 깊게 중첩된 장소를 삭제하려면 해당 장소의 상위 장소 체인 전체를 복사해야 합니다. 이러한 코드는 매우 장황할 수 있습니다.

If the state is too nested to update easily, consider making it “flat”. Here is one way you can restructure this data. Instead of a tree-like structure where each place has an array of its child places, you can have each place hold an array of its child place IDs. Then store a mapping from each place ID to the corresponding place. state가 너무 깊게 중첩되어 업데이트하기 어려운 경우 “flat”하게 만드는 것을 고려해 보세요. 다음은 이 데이터를 재구성할 수 있는 한 가지 방법입니다. 각 place가 하위 place의 배열을 갖는 트리 구조 대신, 각 place가 자식 place ID의 배열을 보유하도록 할 수 있습니다. 그런 다음 각 place ID에서 해당 place로의 매핑을 저장합니다.

This data restructuring might remind you of seeing a database table: 이 데이터 재구성은 데이터베이스 테이블을 떠올리게 할 수 있습니다:

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  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: [11, 12, 13, 14, 15, 16, 17, 18],   
  },
  11: {
    id: 11,
    title: 'Argentina',
    childIds: []
  },
  12: {
    id: 12,
    title: 'Brazil',
    childIds: []
  },
  13: {
    id: 13,
    title: 'Barbados',
    childIds: []
  }, 
  14: {
    id: 14,
    title: 'Canada',
    childIds: []
  },
  15: {
    id: 15,
    title: 'Jamaica',
    childIds: []
  },
  16: {
    id: 16,
    title: 'Mexico',
    childIds: []
  },
  17: {
    id: 17,
    title: 'Trinidad and Tobago',
    childIds: []
  },
  18: {
    id: 18,
    title: 'Venezuela',
    childIds: []
  },
  19: {
    id: 19,
    title: 'Asia',
    childIds: [20, 21, 22, 23, 24, 25],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'India',
    childIds: []
  },
  22: {
    id: 22,
    title: 'Singapore',
    childIds: []
  },
  23: {
    id: 23,
    title: 'South Korea',
    childIds: []
  },
  24: {
    id: 24,
    title: 'Thailand',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Vietnam',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Europe',
    childIds: [27, 28, 29, 30, 31, 32, 33],   
  },
  27: {
    id: 27,
    title: 'Croatia',
    childIds: []
  },
  28: {
    id: 28,
    title: 'France',
    childIds: []
  },
  29: {
    id: 29,
    title: 'Germany',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Italy',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Portugal',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Spain',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Turkey',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Oceania',
    childIds: [35, 36, 37, 38, 39, 40, 41],   
  },
  35: {
    id: 35,
    title: 'Australia',
    childIds: []
  },
  36: {
    id: 36,
    title: 'Bora Bora (French Polynesia)',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Easter Island (Chile)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Fiji',
    childIds: []
  },
  39: {
    id: 40,
    title: 'Hawaii (the USA)',
    childIds: []
  },
  40: {
    id: 40,
    title: 'New Zealand',
    childIds: []
  },
  41: {
    id: 41,
    title: 'Vanuatu',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Moon',
    childIds: [43, 44, 45]
  },
  43: {
    id: 43,
    title: 'Rheita',
    childIds: []
  },
  44: {
    id: 44,
    title: 'Piccolomini',
    childIds: []
  },
  45: {
    id: 45,
    title: 'Tycho',
    childIds: []
  },
  46: {
    id: 46,
    title: 'Mars',
    childIds: [47, 48]
  },
  47: {
    id: 47,
    title: 'Corn Town',
    childIds: []
  },
  48: {
    id: 48,
    title: 'Green Hill',
    childIds: []
  }
};

Now that the state is “flat” (also known as “normalized”), updating nested items becomes easier. 이제 state가 “flat”(“정규화”라고도 함)해졌으므로 중첩된 항목을 업데이트하는 것이 더 쉬워졌습니다.

In order to remove a place now, you only need to update two levels of state: 지금 장소를 제거하려면 두 단계의 state만 업데이트하면 됩니다:

  • The updated version of its parent place should exclude the removed ID from its childIds array.
  • The updated version of the root “table” object should include the updated version of the parent place.
  • 부모 장소의 업데이트된 버전은 제거된 ID를 childIds 배열에서 제외해야 합니다.
  • 루트 ‘테이블’ 객체의 업데이트된 버전에는 상위 위치의 업데이트된 버전이 포함되어야 합니다.

Here is an example of how you could go about it: 다음은 이를 수행하는 방법의 예입니다:

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>
  );
}

You can nest state as much as you like, but making it “flat” can solve numerous problems. It makes state easier to update, and it helps ensure you don’t have duplication in different parts of a nested object. state를 원하는 만큼 중첩할 수 있지만 “flat”하게 만들면 많은 문제를 해결할 수 있습니다. state를 더 쉽게 업데이트할 수 있고 중첩된 객체의 다른 부분에 중복이 생기지 않도록 할 수 있습니다.

Deep Dive | 심층 탐구

Improving memory usage메모리 사용량 개선하기

Ideally, you would also remove the deleted items (and their children!) from the “table” object to improve memory usage. This version does that. It also uses Immer to make the update logic more concise. 이상적으로는 ‘테이블’ 객체에서 삭제된 항목(및 그 하위 항목!)도 제거하여 메모리 사용량을 개선하는 것이 좋습니다. 이 버전은 그렇게 합니다. 또한 Immer를 사용하여 업데이트 로직을 더욱 간결하게 만들었습니다.

{
  "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"
  },
  "devDependencies": {}
}

Sometimes, you can also reduce state nesting by moving some of the nested state into the child components. This works well for ephemeral UI state that doesn’t need to be stored, like whether an item is hovered. 때로는 중첩된 state의 일부를 하위 컴포넌트로 이동하여 state 중첩을 줄일 수도 있습니다. 이 방법은 항목에 마우스 커서가 있는지 여부와 같이 저장할 필요가 없는 임시 UI state에 적합합니다.

Recap요약

  • If two state variables always update together, consider merging them into one.
  • Choose your state variables carefully to avoid creating “impossible” states.
  • Structure your state in a way that reduces the chances that you’ll make a mistake updating it.
  • Avoid redundant and duplicate state so that you don’t need to keep it in sync.
  • Don’t put props into state unless you specifically want to prevent updates.
  • For UI patterns like selection, keep ID or index in state instead of the object itself.
  • If updating deeply nested state is complicated, try flattening it.
  • 두 개의 state 변수가 항상 함께 업데이트되는 경우 두 변수를 하나로 병합하는 것이 좋습니다.
  • state 변수를 신중하게 선택하여 ‘불가능한’ state를 만들지 않도록 하세요.
  • state를 업데이트할 때 실수할 가능성을 줄이는 방식으로 state를 구성하세요.
  • 동기화 state를 유지할 필요가 없도록 불필요 및 중복 state를 피하세요.
  • 특별히 업데이트를 막으려는 경우가 아니라면 props를 state에 넣지 마세요.
  • 선택과 같은 UI 패턴의 경우 객체 자체 대신 ID 또는 인덱스를 state로 유지합니다.
  • 깊게 중첩된 state를 업데이트하는 것이 복잡하다면 state를 평평하게 만들어 보세요.

Challenge 1 of 4: Fix a component that’s not updating업데이트되지 않는 컴포넌트 수정하기

This Clock component receives two props: color and time. When you select a different color in the select box, the Clock component receives a different color prop from its parent component. However, for some reason, the displayed color doesn’t update. Why? Fix the problem. Clock 컴포넌트는 colortime이라는 두 가지 prop을 받습니다. 선택 상자에서 다른 색상을 선택하면 Clock 컴포넌트는 부모 컴포넌트에서 다른 color prop을 받습니다. 하지만 어떤 이유에서인지 표시된 색상이 업데이트되지 않습니다. 왜 그럴까요? 문제를 해결하세요.

import { useState } from 'react';

export default function Clock(props) {
  const [color, setColor] = useState(props.color);
  return (
    <h1 style={{ color: color }}>
      {props.time}
    </h1>
  );
}