(준)공식 문서/React

[ React 공식문서 ] state 관리하기 (5) : state 보존 및 재설정

Je-chan 2024. 3. 11. 12:59

 

1. UI 트리

  • 브라우저는 UI 를 모델링하기 위해 많은 트리 구조를 사용한다.
  • React 도 트리 구조를 사용해 사용자가 만든 UI 를 관리하고 모델링한다.

 

2. state 는 트리의 한 위치에 묶인다

  • 컴포넌트에 state 를 부여할 때, state 가 컴포넌트 내부에 존재한다고 생각할 수 있다
  • 하지만, state 는 실제로 React 내부에서 유지된다. React UI 트리에서 해당 컴포넌트가 어디에 위치하는지에 따라 보유하고 있는 각 state 를 올바른 컴포넌트와 연결할 뿐이다
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>
  );
}
  • 트리로 표현하면 다음의 이미지와 같다.

 

 

  • 이 카운터는 각 트리에서 고유한 위치에 렌더링되기에 개별 카운터다.
  • React 에서 화면의 각 컴포넌트는 완전히 분리된 state 를 갖는다.
  • React 는 같은 컴포넌트를 같은 위치에 렌더링하면 그 state 를 유지한다.
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>
  );
}
  • 두 번째 counter 렌더링을 중지하면 순간 state 가 완전히 사라진다.
  • Render the second counter 버튼을 클릭하면 두 번째 Counter 와 그 state 가 처음부터 초기화되어 DOM 에 추가된다.
  • React 는 컴포넌트가 UI 트리의 해당 위치에 렌더링되는 동안 컴포넌트의 state 를 유지한다.

3. 동일한 위치의 동일한 컴포넌트는 state 를 유지한다.

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>
  );
}
  • 위에서 체크 박스를 선택하거나 취소해도 카운터 state 는 재설정되지 않는다.
  • 이는 isFancy 값이 true 이든 false 이든 루트 APP 컴포넌트에서 반환된 첫 째 자식에 항상 <Counter /> 가 있기 때문이다.

중요한 것은 UI 트리에서 위치다.

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>
  );
  • 확인해보면, App 에서 JSX 마크업으로 return 되는 <Counter /> 가 동일하지 않아 보일 수 있다. 하지만, <Counter /> 태그는 결국 같은 위치에서 렌더링되기 때문에 state 는 재설정되지 않는다.

4. 동일한 위치의 다른 컴포넌트는 state 를 초기화한다.

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

  • Counter 였던 컴포넌트를 빌트인 <p> 컴포넌트로 변경했다.
  • 같은 위치라도 다른 컴포넌트가 렌더링되면 해당 state 를 소멸시킨다.
  • 마찬가지로, 같은 위치에 다른 컴포넌트를 렌더링하면 전체 하위 트리의 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>
  );
}

  • 경험칙상 리렌더링 사이의 state 를 유지하기 위해서는 트리의 구조가 일치해야 한다. 구조가 다르면 React 는 트리에서 컴포넌트를 제거할 때 state 를 파괴한다.

5. 동일한 위치에서 state 재설정하기

  • 기본적으로 React 는 컴포넌트가 같은 위치에 있는 동안 컴포넌트의 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>
  );
}
  • 여기서 현재 플레이어를 변경하면 점수가 보존된다.
  • 두 Counter 는 같은 위치에 표시되기에 React 는 person prop 이 변경되면 동일한 Counter 로 간주한다.
  • 하지만 개념적으로 이 앱에서는 두 개의 구별된 카운터가 있어야 한다.
  • 전환할 때 state 를 재설정하는 방법은 두 가지가 있다.

Option 1 : 컴포넌트를 다른 위치에 렌더링하기

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

  • 이렇게 표현하면, 각 Counter 의 state 는 DOM 에서 제거될 때마다 소멸되고, 버튼을 클릭할 때마다 초기화 된다.
  • 이 방식은 같은 위치에 몇 개의 독립적인 컴포넌트만 렌더링할 때 편리하다.

Option 2 : key 로 state 재설정하기

  • React 는 부모 내의 순서를 사용해 컴포넌트를 구분한다. 하지만, key 를 사용하면 이것이 첫 번째인지, 두 번째인지를 명시할 수 있다.
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>
  );
}
  • 이렇게 코드를 작성하면 테일러와 사라 사이를 전환해도 state 가 유지되지 않는다.
  • key 의 쓰임은 React 가 부모 내 순서가 아닌 key 자체를 위치의 일부로 사용하도록 지시하는 것이다.

key 로 form 재설정하기

  • key 로 state 를 재설정하는 것은 form 을 사용할 때 특히 유용하다.
// App.js

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' }
];

// ContactList.js
export default function ContactList({
  selectedContact,
  contacts,
  onSelect
}) {
  return (
    <section className="contact-list">
      <ul>
        {contacts.map(contact =>
          <li key={contact.id}>
            <button onClick={() => {
              onSelect(contact);
            }}>
              {contact.name}
            </button>
          </li>
        )}
      </ul>
    </section>
  );
}

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

export default function Chat({ contact }) {
  const [text, setText] = useState('');
  return (
    <section className="chat">
      <textarea
        value={text}
        placeholder={'Chat to ' + contact.name}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button>Send to {contact.email}</button>
    </section>
  );
}
  • 입력란에 정보를 입력한 다음, 다른 수신자를 선택하면, <Chat /> 컴포넌트가 동일한 위치에서 렌더링되기 떄문에 입력 state 가 유지된다
  • 많은 앱에서 이런 동작은 바람직할 수 있으나 채팅 앱에서는 그렇지 않다. 이런 문제를 해결하려면 key 를 추가해야 한다.
<Chat key={to.id} contact={to} />
  • 이렇게 하면 다른 수신자를 선택했을 때 Chat 컴포넌트가 그 아래 트리의 모든 state 를 처음부터 다시 생성한다.

제거된 컴포넌트에 대한 state 보존

  • 실제 채팅 앱에서 사용자가 이전 수신자를 다시 선택할 때 입력 state 를 복구하고 싶을 것이다.
  • 이런 방법을 위해서는 몇 가지 방법이 존대한다.
  1. 모든 채팅을 렌더링하되, Active 되지 않은 채팅방은 CSS 로 숨길 것
  2. 부모 컴포넌트에서 각 수신자에 대한 보류 중 메시지를 보관하기
  3. React state 이외의 다른 소스 활용
  4.  

Reference

 

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

 

Preserving and Resetting State – React

The library for web and native user interfaces

react.dev

 

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

 

state 보존 및 재설정 – React

The library for web and native user interfaces

react-ko.dev