본문 바로가기

나야, 리액트 스터디

[week 5] state를 보존하고 초기화하기

안녕하세요 ? YB 김가현입니다

일주일이 너무 빠르네요 ……. 전 솝커톤도 안(못)했는데 왜이렇게 바쁜지 모르겠습니다 …..

 

5주차에는 state를 잘 구조화하는 방법, props, 리액트가 state를 제어하는 방법과 관련된 내용에 대해 알 수 있었는데요 ~! 비교적 낯설었던 내용인 State를 보존하고 초기화하기 부분을 위주로 공부한 내용을 정리해보았습니다

 


React는 JSX 구조를 기반으로 렌더 트리를 만든다.

그렇다면, 컴포넌트에 state를 줄 때 state는 렌더 트리의 어디에 위치할까 ?

State는 렌더 트리의 위치에 연결된다.

컴포넌트 내부에 state가 존재하는 줄 알았다. 완전 착각이었음 …

React는 UI 트리에서 해당 컴포넌트가 어디에 위치하는지에 따라 React가 관리하는 state를 그 컴포넌트와 연결짓는다.

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

 

위 코드에서 counter 변수는 return 문에서 두 번 사용되고 있다. 이 때 React는 <Counter /> JSX를 두 번 렌더링하는데, 각 <Counter />는 App 내에서 독립적인 위치에 렌더링 된다. state 역시 각각 독립적으로 관리한다.

이 상황에서는 아래와 같은 형태의 트리가 구성될 것이다.

각각 자신만의 독립된 score과 hover state를 가질 것이기 때문에, 왼쪽 카운터를 클릭하더라도 해당 컴포넌트의 count만 +1씩 올라갈 뿐 오른쪽 카운터 컴포넌트에는 영향을 끼치지 않는다.

 

 

React는 트리의 동일한 컴포넌트를 동일한 위치에 렌더링하는 동안 상태를 유지한다.

만약 위 코드에서 두 번째 Counter 컴포넌트의 렌더링을 중지한 뒤, 같은 위치에 새롭게 Counter 컴포넌트를 렌더링하면 어떻게 될까 ?

React는 컴포넌트가 렌더링되는 동안에만 컴포넌트의 state를 유지하고, 컴포넌트가 제거되거나 그 위치에 다른 컴포넌트가 렌더링되면 state를 삭제해버린다.

React는 컴포넌트가 DOM에서 제거되면 해당 state를 더 이상 추적하지 않기 때문이다 ! 따라서 새롭게 생긴 Counter는 새로운 state를 가지게 된다.

 

 

동일한 위치의 동일한 컴포넌트는 state를 보존합니다

export default function App() {
  const [isFancy, setIsFancy] = useState(false);
  return (
    <div>
      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}

 

Counter의 isFancy가 true 이든, false이든 state는 유지된다. isFancy에 상관없이, Counter는 App 컴포넌트가 반환하는 div의 첫 번째 자식 컴포넌트이기 때문이다. 

같은 위치에 같은 컴포넌트가 props만 다르게 렌더링 된다면, React는 같은 컴포넌트라고 판단하기 때문이다.

위의 경우도 isFancy 값에 상관 없이 렌더 트리에서 Counter의 자리는 동일하다.

 

React에서 중요한 것은 렌더 트리에서의 위치이다 !

React는 우리가 어떤 코드를 작성했는지 알지 못하고, 트리만 보고 판단하기 때문에 위의 경우는 같은 컴포넌트라고 판단한다.

 

 

당연하겠지만 동일한 위치의 동일한 컴포넌트는 state를 초기화한다.

같은 위치에서 특정 컴포넌트가 아예 다른 컴포넌트로 교체된다면, 당연히 state는 삭제된다.

이 때, 해당 컴포넌트와 그 컴포넌트가 포함하고 있는 자식 컴포넌트들(서브트리)의 상태가 모두 초기화된다.

이후 다시 같은 위치에 원래의 컴포넌트를 다시 렌더링하더라도 서브트리는 모두 새로운 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>
    </>
  );
}

 

위 코드를 실행시켜 테스트해보면, 버튼을 누를 때마다 입력 state가 사라진다.

MyComponent를 렌더링할 때마다 다른 MyTextField 함수가 만들어지면서, 모든 서브트리를 리셋하기 때문에 버튼을 클릭할 때마다 입력 상태가 사라진다.

 

 

동일한 위치에서 상태를 리셋하기

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

Next player! 버튼을 눌러도 player의 이름만 바뀔 뿐 점수는 초기화되지 않는다.

Counter 컴포넌트가 새로운 person 속성을 받지만 Counter가 새로 렌더링되지 않기 때문에 점수가 유지되는 것이다.

 

 

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

두 Counter가 독립적이기를 원한다면 둘을 다른 위치에 렌더링하는 방식을 택할 수 있다.

{isPlayerA &&
  <Counter person="Taylor" />
}
{!isPlayerA &&
  <Counter person="Sarah" />
}

  • isPlayerA가 true일 때는 Counter person="Taylor"가 렌더링되고,
  • isPlayerA가 false일 때는 Counter person="Sarah"가 렌더링된다.

위의 삼항 연산자와 다른 점은, 조건문에 의해 Counter 컴포넌트가 서로 교체된다는 것이다. 여기서는 컴포넌트가 조건에 따라 렌더링되고, 이전에 렌더링된 컴포넌트는 제거된 후 새로 렌더링 된다.

 

즉, isPlayerA 값이 변경될 때마다 기존의 Counter 컴포넌트가 제거되고 새로운 Counter 컴포넌트가 렌더링 되기 때문에 새로운 Counter 컴포넌트는 초기화된 state를 가지고 있는 것이다 ! (삼항 연산자에서는 하나의 컴포넌트만 항상 렌더링된다)

 

 

2. key를 이용해 state를 초기화하기

배열을 다룰때 key를 사용했던 것 처럼, 컴포넌트를 구별할 때도 key를 사용할 수 있다.

따라서 아래와 같이 key 값을 주어 ‘Taylor의 카운터입니다 !’ 혹은 ‘Sarah의 카운터입니다 !’ 라고 명시해줄 수 있다.

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

 

이렇게 key를 지정하면, key를 기반으로 컴포넌트를 판단하기 때문에 같은 컴포넌트를 렌더링해도 key값이 다르다면 React는 이를 다른 컴포넌트로 대체된 것이라 판단하여 state를 초기화한다.

다만 주의할 점은, 해당 key는 오직 해당 컴포넌트 내에서만 유효한 것이지, 전역적으로 유효한 것은 아니다.

 

 

 

궁금한 점 😶

렌더 트리에서 상태도 관리하면 더 편리할 것 같다는 생각이 들어요 ! 그런데 리액트가 컴포넌트 트리 내에서 상태를 위치에 따라 관리하는 방식을 선택한 이유가 궁금합니다 .. 해당 방식만이 가지는 이점이 있을까요 ?