(번역) State 유지 / 리셋하기 - NEW 리액트 공식문서

hongregii·2023년 3월 9일
1

State는 컴포넌트에 고립돼 있다. 리액트는 UI 트리에서 컴포넌트의 위치를 기반으로 어떤 state가 어떤 컴포넌트에 소속돼 있는 녀석인지를 계속 추적한다. 개발자는 리렌더링 사이에서 언제 state를 보존할지, 리셋할지 컨트롤할 수 있다.

이 문서에서는

  • 리액트가 컴포넌트 구조를 어떻게 "보는지"
  • 리액트가 state를 보존할지 / 리셋할지를 언제 선택하는지
  • 강제로 state 리셋시키는 법
  • keys와 types가 state의 보존 여부에 미치는 영향

을 배워보겠다.

UI 트리

브라우저는 UI를 모델링하기 위해 많은 트리 구조를 사용한다. DOM은 HTML을 대표하고, CSSOM은 CSS를 대표한다. 심지어 Accessibility tree 도 있다!

리액트 역시 UI를 관리하고 모델링하기 위한 트리를 사용한다. 리액트는 JSX로부터 UI 트리를 만듬. 그 다음 Recat DOM은 그 UI 트리에 맞게끔 브라우저 DOM 요소 를 업데이트한다. (React Native는 이 트리를 각 mobile platform에 맞게 업데이트함 ㄷㄷ.)

리액트 공식문서에서 virtual dom 이라는 표현은 사용되지 않는다. 이 문서의 리액트 DOM이 보통 우리가 알고 있는 virtual dom에 대한 이야기인듯.

State는 tree의 위치에 묶여있다!

컴포넌트에게 state를 부여하면, state가 컴포넌트 안에서 "살고" 있다고 생각할 수 있다. 아니다! state는 리액트 안에 매어있다. 그리고 리액트가 각 state를 컴포넌트에 연결해주는 것임. UI 트리의 어느 부분에 있는지 (위치)를 기반으로!

Counter 컴포넌트에는 score, hover 두 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>
  );
}

왼쪽, 오른쪽 컴포넌트는 서로 다른 녀석들이다. 트리에서 다른 위치에 렌더링됐기 때문. 리액트를 사용할 때 이런 위치는 보통 몰라도 되지만, 작동을 이해하는데 도움이 된다.

리액트에서 화면의 각 컴포넌트는 완전히 고립된 state를 가지고 있다. 오른쪽 컴포넌트를 눌러서 score state를 바꾼다고 왼쪽 score가 바뀌지 않음.

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

체크박스를 만들어서 누르면 두번째 컴포넌트를 없애봤다. ( {showB && <Counter /> )
원래 score3 이라는 값이 있었어도, 체크박스를 누르면 state가 사라진다. 다시 누르면 0으로 새로 생김.

리액트는 state를 UI 트리에서 자기 자리에 그려져 있을 때만 보존한다. 컴포넌트가 사라지거나 다른 컴포넌트가 그 자리에 그려지면, 리액트는 그 state를 버림.

같은 자리의 같은 컴포넌트 → 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가 바뀌어서 삼항연산자가 작동한다.
삼항연산자에 의해 그 자리에 다른 Counter 컴포넌트가 렌더링되는데, 어찌됐건 UI 트리에서 같은 자리에 같은 함수가 호출되기 때문에, 리액트는 state를 보존함.

마치 같은 "주소 : root의 N번째 자식의 M번째 자식..."를 가진 것과 같다. 리액트가 JSX 값이나 컴포넌트 함수 그 자체를 읽고 판단하는 것이 아님!

같은 자리에 다른 컴포넌트 → state 리셋

삼항연산자 자리에 같은 컴포넌트가 아니라, false 일 경우 <p> 태그를 렌더링한다고 치면, state는 버려진다.

물론 해당 컴포넌트가 자식이 있는 경우, 그 subtree의 state가 모두 버려진다.

이래서 함수형 컴포넌트를 다른 컴포넌트 안에서 선언하면 안되는 것임.

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>
    </>
  );
}
  • MyComponent가 부모, MyTextField는 자식 컴포넌트.
  • 부모의 button을 누르면 setState가 작동하여 렌더링이 다시 일어남.
  • 자식도 다시 렌더링 되는데, 리액트는 같은 자리에 다른 MyTextField 컴포넌트가 렌더링 된다고 생각함.
  • 그래서 부모 컴포넌트가 리렌더링 될 때마다 자식 state도 리셋됨.

같은 위치에서 state 리셋하기

Default : 같은 위치 같은 컴포넌트면 state 보존. 보통은 이렇게 하기를 원할 것임.

그런데 리셋하고 싶으면 어떡함? 다음 예시를 보자.
플레이어가 두명인데, 턴 돌아가면서도 자신의 score를 기억한다.

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>
  );
}
  • 버튼을 누르면 플레이어 (person props)가 바뀌는데, 컴포넌트 위치는 그대로다.
  • 플레이어가 바뀌면 바뀐 플레이어의 score state를 렌더링해야 함.
  • 그러나 같은 위치 같은 컴포넌트라서 score 유지됨.
  • Taylor가 점수 올리든, Sarah가 점수를 올리든 같은 score state가 하나씩 올라감.

그러니까 score state가 두개여야 한다는 말.
state를 리셋하는 방법은 두가지가 있다 :

  1. 다른 위치에 컴포넌트를 그리거나
  2. 각 컴포넌트를 unique하게 만들자. 어떻게 ? key 부여.

옵션 1. 다른 위치에 그리자.

화면 상 다른 위치에 그리자는 말이 아님.

<div>
      {isPlayerA &&
        <Counter person="Taylor" />
      }
      {!isPlayerA &&
        <Counter person="Sarah" />
      }
      <button onClick={() => {
        setIsPlayerA(!isPlayerA);
      }}>
        Next player!
      </button>
    </div>
  );
}

조건부 렌더링을 활용하면, react DOM 내에서의 위치를 바꿀 수 있는 것이다.

이러면 각 컴포넌트의 state가 DOM에서 내려갈 때마다 삭제된다.
버튼을 누를 때마다 score state는 초기값 0으로 리셋될 것.

화면상 같은 위치에 렌더링되는 컴포넌트 개수가 적으면 좋은 방법이다. 이 경우 두개니까 이렇게 해도 괜찮음.

옵션 2. key로 state 리셋하기

state를 리셋하는데 더 범용성 있는 방법이 있다.
key는 리스트 렌더링할 때 각 element를 구분하기 위해 사용한 적이 있을 것이다. 그러나 key는 리스트에만 쓰는 것이 아니라 리액트에서 구분이 필요한 모든 것에 사용함! Default로 리액트는 부모의 N번째 자식으로 컴포넌트를 구별한다. 그러나 key를 사용하면 이 컴포넌트가 "그냥 첫번째 자식", "두번째 자식" 이 아니라, Taylor의 컴포넌트라는 것을 명시하게 된다!

이러면 트리 내 어디에 그려지든 리액트가 이 컴포넌트를 구별하여 인지할 수 있다.

예시 코드는 이렇다

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

key를 지정해 주면 리액트한테 key 그 자체를 위치의 일부로 사용하라고 알려주는 셈이다. 그래서 리액트가 같은 위치에 같은 컴포넌트가 렌더링 돼도, 서로 다른 컴포넌트라는 것을 알 수 있다. 이제 컴포넌트가 내려가면 state는 즉시 삭제되고, 그려질 때마다 새로 선언됨.

단! key가 전역에서 unique한 것은 아님!
부모 내의 위치만 특정해주는 것임! = 같은 부모 내에서만 unique 유효함

key로 form 리셋하기

key로 state를 리셋하는 것이 Form에서 특히 좋다!
는 생략

컴포넌트가 사라져도 state를 보존하려면?

  • 모든 컴포넌트를 렌더링은 하되, CSS로 숨겨놓기
    ex. display : none
    이렇게 해도 react tree에서 사라지지는 않는다!
  • State 끌어올리기 : 공통 부모에서 state를 선언하면, 자식이 없어져도 prop이 사라지는 것 뿐, state는 부모 컴포넌트에 잘 살아있을 것이다. 가장 많이 쓰는 방법!
  • state 외에 다른 source 쓰기 :
    ex. localStorage. 사용자가 브라우저를 껐다 켜도 살아있을 수 있다...
profile
잡식성 누렁이 개발자

0개의 댓글