Preserving and Resetting Statestate 보존 및 재설정

State is isolated between components. React keeps track of which state belongs to which component based on their place in the UI tree. You can control when to preserve state and when to reset it between re-renders.

state는 컴포넌트 간에 격리됩니다. React는 UI 트리에서 어떤 컴포넌트가 어떤 state에 속하는지를 추적합니다. state를 언제 보존하고 언제 초기화할지를 제어할 수 있습니다.

You will learn학습 내용

  • When React chooses to preserve or reset the state
  • How to force React to reset component’s state
  • How keys and types affect whether the state is preserved
  • React가 state를 유지하거나 재설정하도록 선택할 때
  • React가 컴포넌트의 state를 재설정하도록 강제하는 방법
  • key와 type이 state 보존 여부에 영향을 미치는 방법

State is tied to a position in the treestate는 트리의 한 위치에 묶입니다

React builds render trees for the component structure in your UI. React는 UI의 컴포넌트 구조에 대한 렌더 트리를 빌드합니다.

When you give a component state, you might think the state “lives” inside the component. But the state is actually held inside React. React associates each piece of state it’s holding with the correct component by where that component sits in the render tree. 컴포넌트에 state를 부여할 때, state가 컴포넌트 내부에 “존재”한다고 생각할 수 있습니다. 하지만 state는 실제로 React 내부에서 유지됩니다. React는 렌더링 트리에서 해당 컴포넌트의 위치에 따라 보유하고 있는 각 state를 올바른 컴포넌트와 연결합니다.

Here, there is only one <Counter /> JSX tag, but it’s rendered at two different positions: 여기에는 <Counter /> JSX 태그가 하나만 있지만 두 개의 다른 위치에서 렌더링됩니다:

import { useState } from 'react';

export default function App() {
  const counter = <Counter />;
  return (
    <div>
      {counter}
      {counter}
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Here’s how these look as a tree: 트리로 표시되는 모습은 다음과 같습니다:

Diagram of a tree of React components. The root node is labeled 'div' and has two children. Each of the children are labeled 'Counter' and both contain a state bubble labeled 'count' with value 0.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. Each of the children are labeled 'Counter' and both contain a state bubble labeled 'count' with value 0.

React tree

These are two separate counters because each is rendered at its own position in the tree. You don’t usually have to think about these positions to use React, but it can be useful to understand how it works. 이 카운터는 각 트리에서 고유한 위치에 렌더링되기 때문에 두 개의 개별 카운터입니다. 일반적으로 React를 사용하기 위해 이러한 위치에 대해 생각할 필요는 없지만, 작동 방식을 이해하는 것이 유용할 수 있습니다.

In React, each component on the screen has fully isolated state. For example, if you render two Counter components side by side, each of them will get its own, independent, score and hover states. React에서 화면의 각 컴포넌트는 완전히 분리된 state를 갖습니다. 예를 들어, 두 개의 Counter 컴포넌트를 나란히 렌더링하면 각각 독립적인 scorehover state를 갖게 됩니다.

Try clicking both counters and notice they don’t affect each other: 두 counter를 모두 클릭해 보면 서로 영향을 미치지 않는 것을 확인할 수 있습니다:

import { useState } from 'react';

export default function App() {
  return (
    <div>
      <Counter />
      <Counter />
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

As you can see, when one counter is updated, only the state for that component is updated: 보시다시피 counter 하나가 업데이트되면 해당 컴포넌트에 대한 state만 업데이트됩니다:

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 1. The state bubble of the right child is highlighted in yellow to indicate its value has updated.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 1. The state bubble of the right child is highlighted in yellow to indicate its value has updated.

Updating state

React will keep the state around for as long as you render the same component at the same position in the tree. To see this, increment both counters, then remove the second component by unchecking “Render the second counter” checkbox, and then add it back by ticking it again: React는 트리의 같은 위치에 같은 컴포넌트를 렌더링하는 한 그 state를 유지합니다. 이를 확인하려면 두 카운터를 모두 증가시킨 다음 “두 번째 counter 렌더링” 체크박스를 선택 해제하여 두 번째 컴포넌트를 제거한 다음 다시 선택하여 추가합니다:

import { useState } from 'react';

export default function App() {
  const [showB, setShowB] = useState(true);
  return (
    <div>
      <Counter />
      {showB && <Counter />} 
      <label>
        <input
          type="checkbox"
          checked={showB}
          onChange={e => {
            setShowB(e.target.checked)
          }}
        />
        Render the second counter
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Notice how the moment you stop rendering the second counter, its state disappears completely. That’s because when React removes a component, it destroys its state. 두 번째 counter 렌더링을 중지하는 순간 state가 완전히 사라지는 것에 주목하세요. React가 컴포넌트를 제거하면 그 state가 사라지기 때문입니다.

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is missing, and in its place is a yellow 'poof' image, highlighting the component being deleted from the tree.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is missing, and in its place is a yellow 'poof' image, highlighting the component being deleted from the tree.

Deleting a component

When you tick “Render the second counter”, a second Counter and its state are initialized from scratch (score = 0) and added to the DOM. “두 번째 counter 렌더링”을 선택하면 두 번째 Counter와 그 state가 처음부터 초기화되고(score = 0) DOM에 추가됩니다.

Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The entire right child node is highlighted in yellow, indicating that it was just added to the tree.
Diagram of a tree of React components. The root node is labeled 'div' and has two children. The left child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The entire right child node is highlighted in yellow, indicating that it was just added to the tree.

Adding a component

React preserves a component’s state for as long as it’s being rendered at its position in the UI tree. If it gets removed, or a different component gets rendered at the same position, React discards its state. React는 컴포넌트가 UI 트리의 해당 위치에서 렌더링되는 동안 컴포넌트의 state를 유지합니다. 컴포넌트가 제거되거나 같은 위치에 다른 컴포넌트가 렌더링되면 React는 해당 컴포넌트의 state를 삭제합니다.

Same component at the same position preserves state동일한 위치의 동일한 컴포넌트는 state를 유지합니다

In this example, there are two different <Counter /> tags: 다음 예제에는 두 개의 서로 다른 <Counter /> 태그가 있습니다:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

When you tick or clear the checkbox, the counter state does not get reset. Whether isFancy is true or false, you always have a <Counter /> as the first child of the div returned from the root App component: 체크박스를 선택하거나 선택 취소해도 카운터 state는 재설정되지 않습니다. isFancytrue이든 false이든, 루트 App 컴포넌트에서 반환된 div의 첫 번째 자식에는 항상 <Counter />가 있습니다:

Diagram with two sections separated by an arrow transitioning between them. Each section contains a layout of components with a parent labeled 'App' containing a state bubble labeled isFancy. This component has one child labeled 'div', which leads to a prop bubble containing isFancy (highlighted in purple) passed down to the only child. The last child is labeled 'Counter' and contains a state bubble with label 'count' and value 3 in both diagrams. In the left section of the diagram, nothing is highlighted and the isFancy parent state value is false. In the right section of the diagram, the isFancy parent state value has changed to true and it is highlighted in yellow, and so is the props bubble below, which has also changed its isFancy value to true.
Diagram with two sections separated by an arrow transitioning between them. Each section contains a layout of components with a parent labeled 'App' containing a state bubble labeled isFancy. This component has one child labeled 'div', which leads to a prop bubble containing isFancy (highlighted in purple) passed down to the only child. The last child is labeled 'Counter' and contains a state bubble with label 'count' and value 3 in both diagrams. In the left section of the diagram, nothing is highlighted and the isFancy parent state value is false. In the right section of the diagram, the isFancy parent state value has changed to true and it is highlighted in yellow, and so is the props bubble below, which has also changed its isFancy value to true.

Updating the App state does not reset the Counter because Counter stays in the same position Counter가 동일 위치에 있으므로 App의 state를 업데이트해도 Counter는 재설정되지 않음

It’s the same component at the same position, so from React’s perspective, it’s the same counter. 같은 위치에 있는 같은 컴포넌트이므로 React의 관점에서 보면 같은 카운터입니다.

Pitfall | 함정

Remember that it’s the position in the UI tree—not in the JSX markup—that matters to React! This component has two return clauses with different <Counter /> JSX tags inside and outside the if: React에서 중요한 것은 JSX 마크업이 아니라 UI 트리에서의 위치라는 것을 기억하세요! 이 컴포넌트에는 if 내부와 외부에 서로 다른 <Counter /> JSX 태그가 있는 두 개의 return절이 있습니다:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  if (isFancy) {
    return (
      <div>
        <Counter isFancy={true} />
        <label>
          <input
            type="checkbox"
            checked={isFancy}
            onChange={e => {
              setIsFancy(e.target.checked)
            }}
          />
          Use fancy styling
        </label>
      </div>
    );
  }
  return (
    <div>
      <Counter isFancy={false} />
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

You might expect the state to reset when you tick checkbox, but it doesn’t! This is because both of these <Counter /> tags are rendered at the same position. React doesn’t know where you place the conditions in your function. All it “sees” is the tree you return. checkbox를 선택하면 state가 재설정될 것으로 예상할 수 있지만 그렇지 않습니다! 이 <Counter /> 태그가 모두 같은 위치에 렌더링되기 때문입니다. React는 함수에서 조건을 어디에 배치했는지 알지 못합니다. 단지 여러분이 반환하는 트리만 볼 수 있을 뿐입니다.

In both cases, the App component returns a <div> with <Counter /> as a first child. To React, these two counters have the same “address”: the first child of the first child of the root. This is how React matches them up between the previous and next renders, regardless of how you structure your logic. 두 경우 모두 App 컴포넌트는 <Counter />를 첫 번째 자식으로 가진 <div>를 반환합니다. React에서 이 두 카운터는 루트의 첫 번째 자식의 첫 번째 자식이라는 동일한 “주소”를 갖습니다. React는 로직을 어떻게 구성하든 상관없이 이전 렌더링과 다음 렌더링 사이에서 이 방법으로 이들을 일치시킬 수 있습니다.

Different components at the same position reset state동일한 위치의 다른 컴포넌트는 state를 초기화합니다

In this example, ticking the checkbox will replace <Counter> with a <p>: 아래 예제에서 확인란을 선택하면 <Counter/><p>로 바뀝니다:

import { useState } from 'react';

export default function App() {
  const [isPaused, setIsPaused] = useState(false);
  return (
    <div>
      {isPaused ? (
        <p>See you later!</p> 
      ) : (
        <Counter /> 
      )}
      <label>
        <input
          type="checkbox"
          checked={isPaused}
          onChange={e => {
            setIsPaused(e.target.checked)
          }}
        />
        Take a break
      </label>
    </div>
  );
}

function Counter() {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Here, you switch between different component types at the same position. Initially, the first child of the <div> contained a Counter. But when you swapped in a p, React removed the Counter from the UI tree and destroyed its state. 여기서는 같은 위치에서 서로 다른 컴포넌트 유형 사이를 전환합니다. 처음에 <div>의 첫 번째 자식에는 Counter가 있었습니다. 하지만 p를 넣었을 때 React는 UI 트리에서 Counter를 제거하고 그 state를 소멸시켰습니다.

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'p', highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'p', highlighted in yellow.

When Counter changes to p, the Counter is deleted and the p is added Counterp로 변경되면 Counter가 삭제되고 p가 추가됨

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'p'. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'p'. The middle section has the same 'div' parent, but the child component has now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, highlighted in yellow.

When switching back, the p is deleted and the Counter is added 다시 전환하면 p가 삭제되고 Counter가 추가됨

Also, when you render a different component in the same position, it resets the state of its entire subtree. To see how this works, increment the counter and then tick the checkbox: 또한 같은 위치에 다른 컴포넌트를 렌더링하면 전체 하위 트리의 state가 재설정됩니다. 어떻게 작동하는지 확인하려면 카운터를 증가시킨 다음 확인란을 선택합니다:

import { useState } from 'react';

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}
      <label>
        <input
          type="checkbox"
          checked={isFancy}
          onChange={e => {
            setIsFancy(e.target.checked)
          }}
        />
        Use fancy styling
      </label>
    </div>
  );
}

function Counter({ isFancy }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }
  if (isFancy) {
    className += ' fancy';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

The counter state gets reset when you click the checkbox. Although you render a Counter, the first child of the div changes from a div to a section. When the child div was removed from the DOM, the whole tree below it (including the Counter and its state) was destroyed as well. 확인란을 클릭하면 counter state가 재설정됩니다. Counter를 렌더링하더라도 div의 첫 번째 자식은 div에서 section으로 변경됩니다. 자식 div가 DOM에서 제거되면 그 아래의 전체 트리(카운터 및 해당 state 포함)도 함께 제거됩니다.

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'section', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'div', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'section', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 3. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'div', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.

When section changes to div, the section is deleted and the new div is added sectiondiv로 변경되면 section이 삭제되고 새 div가 추가됨

Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'div', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 0. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'section', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.
Diagram with three sections, with an arrow transitioning each section in between. The first section contains a React component labeled 'div' with a single child labeled 'div', which has a single child labeled 'Counter' containing a state bubble labeled 'count' with value 0. The middle section has the same 'div' parent, but the child components have now been deleted, indicated by a yellow 'proof' image. The third section has the same 'div' parent again, now with a new child labeled 'section', highlighted in yellow, also with a new child labeled 'Counter' containing a state bubble labeled 'count' with value 0, all highlighted in yellow.

When switching back, the div is deleted and the new section is added 다시 전환하면 div가 삭제되고 새 section이 추가됨

As a rule of thumb, if you want to preserve the state between re-renders, the structure of your tree needs to “match up” from one render to another. If the structure is different, the state gets destroyed because React destroys state when it removes a component from the tree. 경험칙상 리렌더링 사이에 state를 유지하려면 트리의 구조가 “일치”해야 합니다. 구조가 다르면 React는 트리에서 컴포넌트를 제거할 때 state를 파괴하기 때문입니다.

Pitfall | 함정

This is why you should not nest component function definitions. 그렇기 때문에 컴포넌트 함수 정의를 중첩해서는 안 됩니다.

Here, the MyTextField component function is defined inside MyComponent: 여기서는 MyTextField 컴포넌트 함수가 MyComponent 안에 정의되어 있습니다:

import { useState } from 'react';

export default function MyComponent() {
  const [counter, setCounter] = useState(0);

  function MyTextField() {
    const [text, setText] = useState('');

    return (
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
    );
  }

  return (
    <>
      <MyTextField />
      <button onClick={() => {
        setCounter(counter + 1)
      }}>Clicked {counter} times</button>
    </>
  );
}

Every time you click the button, the input state disappears! This is because a different MyTextField function is created for every render of MyComponent. You’re rendering a different component in the same position, so React resets all state below. This leads to bugs and performance problems. To avoid this problem, always declare component functions at the top level, and don’t nest their definitions.

버튼을 클릭할 때마다 입력 state가 사라집니다! 이는 MyComponent를 렌더링할 때마다 다른 MyTextField 함수가 생성되기 때문입니다. 같은 위치에 다른 컴포넌트를 렌더링하기 때문에 React는 아래의 모든 state를 초기화합니다. 이로 인해 버그와 성능 문제가 발생합니다. 이 문제를 방지하려면 항상 컴포넌트 함수를 최상위 수준에서 선언하고 정의를 중첩하지 마세요.

Resetting state at the same position동일한 위치에서 state 재설정하기

By default, React preserves state of a component while it stays at the same position. Usually, this is exactly what you want, so it makes sense as the default behavior. But sometimes, you may want to reset a component’s state. Consider this app that lets two players keep track of their scores during each turn: 기본적으로 React는 컴포넌트가 같은 위치에 있는 동안 컴포넌트의 state를 보존합니다. 일반적으로 이것은 사용자가 원하는 것이므로 기본 동작으로 적합합니다. 하지만 때로는 컴포넌트의 state를 리셋하고 싶을 때가 있습니다. 두 명의 플레이어가 각 턴 동안 점수를 추적할 수 있는 이 앱을 예로 들어보겠습니다:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter person="Taylor" />
      ) : (
        <Counter person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Currently, when you change the player, the score is preserved. The two Counters appear in the same position, so React sees them as the same Counter whose person prop has changed. 현재 플레이어를 변경하면 점수가 보존됩니다. 두 Counter는 같은 위치에 표시되므로 React는 person prop이 변경된 동일한 Counter로 간주합니다.

But conceptually, in this app they should be two separate counters. They might appear in the same place in the UI, but one is a counter for Taylor, and another is a counter for Sarah. 하지만 개념적으로 이 앱에서는 두 개의 별도 카운터가 있어야 합니다. UI에서 같은 위치에 표시될 수도 있지만 하나는 Taylor의 카운터이고 다른 하나는 Sarah의 카운터입니다.

There are two ways to reset state when switching between them: 전환할 때 state를 재설정하는 방법에는 두 가지가 있습니다:

  1. Render components in different positions
  2. Give each component an explicit identity with key
  1. 컴포넌트를 다른 위치에 렌더링하기
  2. 각 컴포넌트에 key로 명시적인 아이덴티티를 부여합니다.

Option 1: Rendering a component in different positions컴포넌트를 다른 위치에 렌더링하기

If you want these two Counters to be independent, you can render them in two different positions: 이 두 Counter를 독립적으로 만들려면 두 개의 다른 위치에 렌더링하면 됩니다:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA &&
        <Counter person="Taylor" />
      }
      {!isPlayerA &&
        <Counter person="Sarah" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

  • Initially, isPlayerA is true. So the first position contains Counter state, and the second one is empty.
  • When you click the “Next player” button the first position clears but the second one now contains a Counter.
  • 처음에는 isPlayerAtrue입니다. 따라서 첫 번째 위치에는 Counter state가 포함되고 두 번째 위치는 비어 있습니다.
  • ”Next player” 버튼을 클릭하면 첫 번째 위치는 지워지지만 두 번째 위치에는 이제 Counter가 포함됩니다.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The only child, arranged to the left, is labeled Counter with a state bubble labeled 'count' and value 0. All of the left child is highlighted in yellow, indicating it was added.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The only child, arranged to the left, is labeled Counter with a state bubble labeled 'count' and value 0. All of the left child is highlighted in yellow, indicating it was added.

Initial state

Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'false'. The state bubble is highlighted in yellow, indicating that it has changed. The left child is replaced with a yellow 'poof' image indicating that it has been deleted and there is a new child on the right, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'false'. The state bubble is highlighted in yellow, indicating that it has changed. The left child is replaced with a yellow 'poof' image indicating that it has been deleted and there is a new child on the right, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0.

Clicking “next”

Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The state bubble is highlighted in yellow, indicating that it has changed. There is a new child on the left, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is replaced with a yellow 'poof' image indicating that it has been deleted.
Diagram with a tree of React components. The parent is labeled 'Scoreboard' with a state bubble labeled isPlayerA with value 'true'. The state bubble is highlighted in yellow, indicating that it has changed. There is a new child on the left, highlighted in yellow indicating that it was added. The new child is labeled 'Counter' and contains a state bubble labeled 'count' with value 0. The right child is replaced with a yellow 'poof' image indicating that it has been deleted.

Clicking “next” again

Each Counter’s state gets destroyed each time it’s removed from the DOM. This is why they reset every time you click the button. Counter의 state는 DOM에서 제거될 때마다 소멸됩니다. 그렇기 때문에 버튼을 클릭할 때마다 초기화됩니다.

This solution is convenient when you only have a few independent components rendered in the same place. In this example, you only have two, so it’s not a hassle to render both separately in the JSX. 이 솔루션은 같은 위치에 몇 개의 독립적인 컴포넌트만 렌더링할 때 편리합니다. 이 예시에서는 두 개만 있으므로 JSX에서 두 컴포넌트를 별도로 렌더링하는 것이 번거롭지 않습니다.

Option 2: Resetting state with a keykey로 state 재설정하기

There is also another, more generic, way to reset a component’s state. 컴포넌트의 state를 재설정하는 더 일반적인 방법도 있습니다.

You might have seen keys when rendering lists. Keys aren’t just for lists! You can use keys to make React distinguish between any components. By default, React uses order within the parent (“first counter”, “second counter”) to discern between components. But keys let you tell React that this is not just a first counter, or a second counter, but a specific counter—for example, Taylor’s counter. This way, React will know Taylor’s counter wherever it appears in the tree! 목록을 렌더링할 때 key를 본 적이 있을 것입니다. key는 목록에만 사용되는 것이 아닙니다! key를 사용해 React가 모든 컴포넌트를 구분하도록 할 수 있습니다. 기본적으로 React는 부모 내의 순서(“첫 번째 counter”, “두 번째 counter”)를 사용해 컴포넌트를 구분합니다. 하지만 key를 사용하면 이것이 첫 번째 counter나 두 번째 counter가 아니라 특정 counter(예: Taylor의 counter)임을 React에 알릴 수 있습니다. 이렇게 하면 React는 테일러의 counter가 트리에 어디에 나타나든 알 수 있습니다!

In this example, the two <Counter />s don’t share state even though they appear in the same place in JSX: 다음 예제에서는 두 <Counter />가 JSX에서 같은 위치에 표시되지만 state를 공유하지 않습니다:

import { useState } from 'react';

export default function Scoreboard() {
  const [isPlayerA, setIsPlayerA] = useState(true);
  return (
    <div>
      {isPlayerA ? (
        <Counter key="Taylor" person="Taylor" />
      ) : (
        <Counter key="Sarah" person="Sarah" />
      )}
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

function Counter({ person }) {
  const [score, setScore] = useState(0);
  const [hover, setHover] = useState(false);

  let className = 'counter';
  if (hover) {
    className += ' hover';
  }

  return (
    <div
      className={className}
      onPointerEnter={() => setHover(true)}
      onPointerLeave={() => setHover(false)}
    >
      <h1>{person}'s score: {score}</h1>
      <button onClick={() => setScore(score + 1)}>
        Add one
      </button>
    </div>
  );
}

Switching between Taylor and Sarah does not preserve the state. This is because you gave them different keys: 테일러와 사라 사이를 전환해도 state가 유지되지 않습니다. 서로 다른 key를 부여했기 때문입니다:

{isPlayerA ? (
<Counter key="Taylor" person="Taylor" />
) : (
<Counter key="Sarah" person="Sarah" />
)}

Specifying a key tells React to use the key itself as part of the position, instead of their order within the parent. This is why, even though you render them in the same place in JSX, React sees them as two different counters, and so they will never share state. Every time a counter appears on the screen, its state is created. Every time it is removed, its state is destroyed. Toggling between them resets their state over and over. key를 지정하면 React가 부모 내 순서가 아닌 key 자체를 위치의 일부로 사용하도록 지시합니다. 그렇기 때문에 JSX에서 같은 위치에 렌더링하더라도 React의 관점에서 보면 두 카운터는 서로 다른 카운터입니다. 결과적으로 state를 공유하지 않습니다. 카운터가 화면에 나타날 때마다 그 state가 생성됩니다. 카운터가 제거될 때마다 그 state는 소멸됩니다. 두 카운터 사이를 토글하면 state가 계속 초기화됩니다.

Note

Remember that keys are not globally unique. They only specify the position within the parent. 키는 전역으로 고유하지는 않다는 점을 기억하세요. 키는 부모 내에서의 위치만 지정합니다.

Resetting a form with a key키로 form 재설정하기

Resetting state with a key is particularly useful when dealing with forms. 키로 state를 재설정하는 것은 form을 다룰 때 특히 유용합니다

In this chat app, the <Chat> component contains the text input state: 이 채팅 앱에서 <Chat> 컴포넌트는 텍스트 input state를 포함합니다:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

Try entering something into the input, and then press “Alice” or “Bob” to choose a different recipient. You will notice that the input state is preserved because the <Chat> is rendered at the same position in the tree. 입력란에 무언가를 입력한 다음 “앨리스” 또는 “밥”을 눌러 다른 수신자를 선택하세요. <Chat>이 트리의 동일한 위치에 렌더링되므로 입력 state가 유지되는 것을 알 수 있습니다.

In many apps, this may be the desired behavior, but not in a chat app! You don’t want to let the user send a message they already typed to a wrong person due to an accidental click. To fix it, add a key: 많은 앱에서 이러한 동작이 바람직할 수 있지만, 채팅 앱에서는 그렇지 않습니다! 사용자가 실수로 클릭해서 이미 입력한 메시지를 엉뚱한 사람에게 보내는 것을 원치 않을 것입니다. 이 문제를 해결하려면 key를 추가하세요:

<Chat key={to.id} contact={to} />

This ensures that when you select a different recipient, the Chat component will be recreated from scratch, including any state in the tree below it. React will also re-create the DOM elements instead of reusing them. 이렇게 하면 다른 수신자를 선택하면 Chat 컴포넌트가 그 아래 트리의 모든 state를 포함하여 처음부터 다시 생성됩니다. 또한 React는 DOM 엘리먼트를 재사용하는 대신 다시 생성합니다.

Now switching the recipient always clears the text field: 이제 수신자를 전환하면 항상 텍스트 필드가 지워집니다:

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.id} contact={to} />
    </div>
  )
}

const contacts = [
  { id: 0, name: 'Taylor', email: 'taylor@mail.com' },
  { id: 1, name: 'Alice', email: 'alice@mail.com' },
  { id: 2, name: 'Bob', email: 'bob@mail.com' }
];

Deep Dive | 심층 탐구

Preserving state for removed components제거된 컴포넌트에 대한 state 보존

In a real chat app, you’d probably want to recover the input state when the user selects the previous recipient again. There are a few ways to keep the state “alive” for a component that’s no longer visible: 실제 채팅 앱에서는 사용자가 이전 수신자를 다시 선택할 때 입력 state를 복구하고 싶을 것입니다. 더 이상 표시되지 않는 컴포넌트의 state를 ‘살아있게’ 유지하는 몇 가지 방법이 있습니다:

  • You could render all chats instead of just the current one, but hide all the others with CSS. The chats would not get removed from the tree, so their local state would be preserved. This solution works great for simple UIs. But it can get very slow if the hidden trees are large and contain a lot of DOM nodes.
  • You could lift the state up and hold the pending message for each recipient in the parent component. This way, when the child components get removed, it doesn’t matter, because it’s the parent that keeps the important information. This is the most common solution.
  • You might also use a different source in addition to React state. For example, you probably want a message draft to persist even if the user accidentally closes the page. To implement this, you could have the Chat component initialize its state by reading from the localStorage, and save the drafts there too.
  • 현재 채팅만 렌더링하는 것이 아니라 모든 채팅을 렌더링하되 다른 모든 채팅은 CSS로 숨길 수 있습니다. 채팅은 트리에서 제거되지 않으므로 로컬 state가 유지됩니다. 이 솔루션은 간단한 UI에 적합합니다. 하지만 숨겨진 트리가 크고 많은 DOM 노드를 포함하는 경우 속도가 매우 느려질 수 있습니다.
  • 부모 컴포넌트에서 각 수신자에 대한 보류 중인 메시지를 state를 끌어올려서 보관할 수 있습니다. 이렇게 하면 자식 컴포넌트가 제거되더라도 중요한 정보를 보관하는 것은 부모 컴포넌트이므로 문제가 되지 않습니다. 이것이 가장 일반적인 해결책입니다.
  • React state 외에 다른 소스를 사용할 수도 있습니다. 예를 들어, 사용자가 실수로 페이지를 닫아도 메시지 초안이 유지되기를 원할 수 있습니다. 이를 구현하기 위해 Chat 컴포넌트가 localStorage에서 읽어서 state를 초기화하고 초안도 저장하도록 할 수 있습니다.

No matter which strategy you pick, a chat with Alice is conceptually distinct from a chat with Bob, so it makes sense to give a key to the <Chat> tree based on the current recipient. 어떤 전략을 선택하든 앨리스와의 채팅은 밥과의 채팅과 개념적으로 구별되므로 현재 수신자를 기준으로 <Chat> 트리에 key를 부여하는 것이 합리적입니다.

Recap요약

  • React keeps state for as long as the same component is rendered at the same position.
  • State is not kept in JSX tags. It’s associated with the tree position in which you put that JSX.
  • You can force a subtree to reset its state by giving it a different key.
  • Don’t nest component definitions, or you’ll reset state by accident.
  • React는 동일한 컴포넌트가 동일한 위치에서 렌더링되는 한 state를 유지합니다.
  • state는 JSX 태그에 보관되지 않습니다. JSX를 넣은 트리 위치와 연관되어 있습니다.
  • 하위 트리에 다른 key를 지정하여 강제로 state를 재설정할 수 있습니다.
  • 컴포넌트 정의를 중첩하지 마세요. 실수로 state가 초기화될 수 있습니다.

Challenge 1 of 5: Fix disappearing input text사라지는 입력 텍스트 수정하기

This example shows a message when you press the button. However, pressing the button also accidentally resets the input. Why does this happen? Fix it so that pressing the button does not reset the input text. 이 예는 버튼을 누르면 메시지가 표시되는 것을 보여줍니다. 그러나 버튼을 누르면 실수로 입력이 재설정됩니다. 왜 이런 일이 발생하나요? 버튼을 눌러도 입력 텍스트가 재설정되지 않도록 수정하세요.

import { useState } from 'react';

export default function App() {
  const [showHint, setShowHint] = useState(false);
  if (showHint) {
    return (
      <div>
        <p><i>Hint: Your favorite city?</i></p>
        <Form />
        <button onClick={() => {
          setShowHint(false);
        }}>Hide hint</button>
      </div>
    );
  }
  return (
    <div>
      <Form />
      <button onClick={() => {
        setShowHint(true);
      }}>Show hint</button>
    </div>
  );
}

function Form() {
  const [text, setText] = useState('');
  return (
    <textarea
      value={text}
      onChange={e => setText(e.target.value)}
    />
  );
}