본문 바로가기

카테고리 없음

[번역] Preserving and Resetting State

https://react.dev/learn/preserving-and-resetting-state

 

Preserving and Resetting State – React

The library for web and native user interfaces

react.dev

 

상태는 컴포넌트간에 격리된다. React는 UI 트리에서 어떤 컴포넌트가 어떤 상태에 속하는지를 추적한다. 언제 상태를 보존할지, 언제 리렌더링할지 제어할 수 있다. 

State is tied to a position in the render tree 

React는 UI 컴포넌트 구조에 대한 렌더 트리를 빌드한다. 

 

컴포넌트 state를 전달할 때, state가 컴포넌트 내부에 "존재"한다고 생각할 수 있다. 하지만 state는 실제로 React 내부에 보관된다. React는 렌더 트리에서 해당 컴포넌트의 위치에 따라 보유하고 있는 각 state를 올바른 컴포넌트와 연결한다. 

 

React에서 화면의 각 컴포넌트는 완전히 분리된 상태를 갖는다. 예를 들어 두 개의 카운터 컴포넌트를 나란히 렌더링하면 각각 독립적인 점수 및 호버 상태를 갖게 된다.

React는 컴포넌트가 UI 트리에서 해당 위치에 렌더링되는 동안에는 컴포넌트의 상태를 유지한다. 컴포넌트가 제거되거나 다른 컴포넌트가 같은 위치에 렌더링되면 React는 해당 컴포넌트의 상태를 제거한다. 

 

Same component at the same position preserves state 

이 예제에는 두개의 서로 다른 <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>
  );
}

 

isFancy가 참이든 거짓이든, 루트 앱 컴포넌트에서 반환된 div의 첫번째 자식에는 항상 <Counter/>가 존재한다. 

 

 

 

같은 위치에 있는 같은 컴포넌트이므로 React의 관점에서 보면 같은 카운터이다. 

 

Different components at the same position reset state 

...생략

 

리렌더링 사이에 상태를 유지하려면 트리의 구조가 한 렌더링에서 다른 렌더링간에 일치해야한다. 구조가 다르면 React는 트리에서 컴포넌트를 제거할 때 state를 삭제하기 때문에 state가 파괴된다.

 

Resetting state at the same position 

기본적으로 React는 컴포넌트가 같은 위치에 있는 동안 상태를 보존한다. 일반적으로 이것은 사용자가 원하는 것이므로 기본 동작에 적합하다. 하지만 때로는 컴포넌트의 상태를 재설정하고 싶을 때가 있다. 두 명의 플레이거가 각 턴 동안 점수를 추적할 수 있는 이 앱을 예로 들어보자.

 

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

 

 

현재 플레이어를 변경하면 점수가 유지된다. 두 카운터는 같은 위치에 표시되므로 React는 두 카운터를 사람 prop이 변경된 동일한 카운터로 간주한다. 하지만 개념적으로 이 앱에서는 두 카운터는 별개의 카운터이어야 한다. UI에서는 같은 위치에 표시될 수 있지만 하나는 테일러의 카운터이고 하나는 사라의 카운터이다. 

 

두 카운터 사이를 전환할 때 상태를 초기화하는 방법은 두가지가 있다. 

 

1. 컴포넌트를 서로 다른 위치에 렌더링하기

2. 각 컴포넌트에 키 부여

 

list를 렌더링할 때 키를 본적이 있을 것이다. 키는 list에만 사용되는 것이 아니다. 키를 사용하여 리액트가 모든 컴포넌트를 구분하도록 할 수 있다. 기본적으로 리액트는 상위 컴포넌트 내의 순서("첫 번째 카운터", "두 번째 카운터")를 사용하여 컴포넌트를 구분한다. 하지만 키를 사용하면 이것이 첫 번째 카운터나 두 번째 카운터가 아니라 특정 카운터(예: Taylor의 카운터)임을 리액트에 알릴 수 있다.

 

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

 

키를 지정하면 리액트는 부모 내의 순서 대신 키 자체를 위치의 일부로 사용하도록 지시한다. 그렇기 때문에 JSX에서 같은 위치에 렌더링하더라도 React는 이를 두 개의 다른 카운터로 간주하므로 상태를 공유하지 않는다. 

 

키는 전역적으로 고유하지 않다. 키는 부모 내에서의 위치만 지정한다. 

 

Preserving state for removed components

채팅앱에서는 사용자가 이전 수신자를 다시 선택할 때 입력 상태를 복구하고 싶을 것이다. 

더 이상 표시되지 않는 컴포넌트의 상태를 '살아있게'유지하는 몇가지 방법이 있다. 

- 현재 채팅만 렌더링하는 대신 모든 채팅을 렌더링하고 모든 채팅은 CSS로 숨길 수 있다. 채팅은 트리에서 제거되지 않으므로 로컬 상태가 유지된다. 이 솔루션은 단순한 UI에 적합하다. 하지만 숨겨진 트리가 크고 많은 DOM 노드를 포함하는 경우 속도가 매우 느려질 수 있다. 

- 부모 컴포넌트에서 각 수신자에 대한 보류 중인 메시지를 상태를 끌어올려서 보관할 수 있다. 이렇게 하면 하위 컴포넌트가 제거되더라도 중요한 정보를 보관하는 것은 상위 컴포넌트이기 때문에 문제가 되지 않는다. 이것이 가장 일반적인 해결책이다. 

- 리액트 상태 외에 다른 소스를 사용할 수도 있다. 예를 들어 사용자가 실수로 페이지를 닫아도 메시지 초안이 유지되기를 원할 수 있다. 이를 구현하기 위해 Chat 컴포넌트가 localStorage에서 읽어서 상태를 초기화하고 초안도 저장하도록 할 수 있다.