[리액트 공식문서 읽기] MANAGING STATE - Sharing State Between Components

JaeHong Jeong·2023년 9월 26일
post-thumbnail

Overview

때로는 두 컴포넌트의 상태가 항상 함께 변경되기를 원할 수도 있다. 이를 수행하려면 둘 다에서 상태를 제거하고 가장 가까운 공통 상위 항목으로 옮긴 다음 props를 통해 상태를 전달한다. 이는 상태 올리기(lifting state up)라 알려져 있으며, 리액트 코드를 작성하는 가장 일반적인 작업 중 하나다.

Lifting state up by example

이 예에서 상위 Accordion 컴포넌트는 두 개의 별도 Panel 을 렌더링한다.

  • Accordion
    • Panel
    • Panel

Panel 컴포넌트에는 콘텐츠 표시 여부를 결정하는 부울 isActive 상태가 있다.

두 패널 모두에 대해 표시 버튼을 누른다.

import { useState } from 'react';

function Panel({ title, children }) {
  const [isActive, setIsActive] = useState(false);
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="About">
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel title="Etymology">
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

한 패널의 버튼을 눌러도 다른 패널에는 영향을 주지 않는다. 두 패널은 독립적이다.

하지만 이제 주어진 시간에 하나의 패널만 확장되도록 변경하고 싶다고 가정하자. 해당 디자인에서는 두 번째 패널을 확장하면 첫 번째 패널이 축소된다. 어떻게 할까?

이 두 패널을 조정하려면 세 단계를 거쳐 상위 컴포넌트로 “상태를 올려야”한다.

  1. 자식 컴포넌트에서 상태를 삭제한다.
  2. 공통 상위 항목에서 하드코딩된 데이터를 전달한다.
  3. 공통 상위 항목에 상태를 추가하고 이벤트 핸들러와 함께 전달한다.

이렇게 하면 Accordion 컴포넌트가 두 Panel 을 모두 조정하고 한 번에 하나씩만 확장할 수 있다.

Step 1: Remove state from the child components

PanelisActive 에 대한 제어권을 상위 컴포넌트에 부여한다. 이는 상위 컴포넌트가 isActivePanel 에 prop으로 대신 전달한다는 의미다. 먼저 Panel 컴포넌트에서 다음 줄을 제거해라.

const [isActive, setIsActive] = useState(false);

And instead, add isActive to the Panel’s list of props:

대신 Panel 의 prop 목록에 isActive 를 추가해라.

function Panel({ title, children, isActive }) {

이제 Panel 의 상위 컴포넌트는 isActive 를 prop으로 전달하여 제어할 수 있다. 반대로 이제 Panel 컴포넌트는 isActive 값을 제어할 수 없다. 이제 상위 컴포넌트에 달려있다.

Step 2: Pass hardcoded data from the common parent

상태를 해제하려면 조정하려는 두 하위 컴포넌트 모두에서 가장 가까운 공통 상위 컴포넌트를 찾아야 한다.

  • Accordion (가장 가까운 공통 부모)
    • Panel
    • Panel

이 예에서는 Accordion 컴포넌트다. 두 패널 위에 있고 prop을 제어할 수 있으므로 현재 패널이 활성화된 “진실의 소스”가 된다. Accordion 컴포넌트가 하드코딩된 isActive 을 두 패널 모두에게 전달하도록 만든다.

import { useState } from 'react';

export default function Accordion() {
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel title="About" isActive={true}>
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel title="Etymology" isActive={true}>
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

function Panel({ title, children, isActive }) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={() => setIsActive(true)}>
          Show
        </button>
      )}
    </section>
  );
}

Accordion 컴포넌트에서 하드코딩된 isActive 값을 편집해 보고 화면에서 결과를 확인해라.

Step 3: Add state to the common parent

상태를 올리면 상태로 저장하는 내용의 성격이 바뀌는 경우가 많다.

이 경우 한 번에 하나의 패널만 활성화되어야 한다. 이는 Accordion 공통 상위 컴포넌트가 어느 패널이 활성 패널인지 추적해야 함을 의미한다. boolean 값 대신 상태 변수에 대한 활성 Panel 의 인덱스로 숫자를 사용할 수 있다.

const [activeIndex, setActiveIndex] = useState(0);

When the activeIndex is 0, the first panel is active, and when it’s 1, it’s the second one.

activeIndex0 이면 첫 번째 패널이 활성화되고, 1 이면 두 번째 패널이 활성화된다.

패널 중 하나에서 “Show” 버튼을 클릭하면 Accordion 의 활성 인덱스를 변경해야 한다. PanelAccordion 내부에 정의되어 있으므로 activeIndex 상태를 직접 설정할 수 없다. Accordion 컴포넌트는 이벤트 핸들러를 prop으로 전달하여 Panel 컴포넌트가 상태를 변경할 수 있도록 명시적으로 허용해야 한다.

<>
  <Panel
    isActive={activeIndex === 0}
    onShow={() => setActiveIndex(0)}
  >
    ...
  </Panel>
  <Panel
    isActive={activeIndex === 1}
    onShow={() => setActiveIndex(1)}
  >
    ...
  </Panel>
</>

Panel 내부의 <button>onShow prop을 클릭 이벤트 핸들러로 사용한다.

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="About"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        With a population of about 2 million, Almaty is Kazakhstan's largest city. From 1929 to 1997, it was its capital city.
      </Panel>
      <Panel
        title="Etymology"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        The name comes from <span lang="kk-KZ">алма</span>, the Kazakh word for "apple" and is often translated as "full of apples". In fact, the region surrounding Almaty is thought to be the ancestral home of the apple, and the wild <i lang="la">Malus sieversii</i> is considered a likely candidate for the ancestor of the modern domestic apple.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Show
        </button>
      )}
    </section>
  );
}

이것으로 상태 끌어 올리기 완료다. 상태를 공통 상위 컴포넌트로 이동하면 두 패널을 조정할 수 있었다. 두 개의 “is shown” 플래그 대신 활성 인덱스를 사용하면 주어진 시간에 하나의 패널만 활성화된다. 그리고 이벤트 핸들러를 자식에게 전달하면 자식이 부모의 상태를 변경할 수 있다.

💡 DEEP DIVE

Controlled and uncontrolled components

일부 로컬 상태가 “제어되지 않음”인 컴포넌트를 호출하는 것이 일반적이다. 예를 들어, isActive 상태 변수가 있는 원래 Panel 컴포넌트는 해당 상위 패널이 활성상태인지 여부에 영향을 줄 수 없기 때문에 제어되지 않는다.

대조적으로, 컴포넌트의 중요한 정보가 자체 로컬 상태가 아닌 props에 의해 구동되는 경우 컴포넌트가 “제어된다”고 말할 수 있다. 이를 통해 상위 컴포넌트가 해당 동작을 완전히 지정할 수 있다. isActive prop이 있는 최종 Panel 컴포넌트는 Accordion 컴포넌트에 의해 제어된다.

제어되지 않는 컴포넌트는 구성이 덜 필요하므로 상위 컴포넌트 내에서 사용하기가 더 쉽다. 하지만 함께 조정하려는 경우 유연성이 떨어진다. 제어되는 컴포넌트는 최대한 유연하지만 props를 사용하여 완전히 구성하려면 상위 컴포넌트가 필요하다.

실제로 “제어됨”과 “제어되지 않음”은 엄격한 기술 용어가 아니다. 각 컴포넌트에는 일반적으로 로컬 상태와 소품이 혼합되어 있다. 그러나 이는 컴포넌트의 설계 방식과 컴포넌트가 제공하는 기능에 대해 설명하는 유용한 방법이다.

컴포넌트를 작성할 때 그 안의 어떤 정보를 제어해야 하는지(props를 통해), 어떤 정보를 제어하지 않아야 하는지(state를 통해) 고려해라. 하지만 언제든지 마음을 바꾸고 나중에 리팩토링할 수 있다.

A single source of truth for each state

리액트 애플리케이션에서는 많은 컴포넌트가 자체 상태를 갖는다. 일부 상태는 입력과 같이 리프 컴포넌트(트리 하단에 있는 컴포넌트) 가까이에 “살아”있을 수 있다. 다른 상태는 앱 상단에 더 가깝게 “실시간”일 수 있다. 예를 들어, 클라이언트 측 라우팅 라이브러리도 일반적으로 현재 경로를 리액트 상태에 저장하고 이를 props로 전달하여 구현된다.

각각의 고유한 상태에 대해 이를 “소유”하는 컴포넌트를 선택한다. 이 원칙은 “single source of truth”를 갖는 것으로도 알려져 있다. 이는 모든 상태가 한 곳에 있다는 것을 의미하지는 않는다. 그러나 각 상태에는 해당 정보를 보유하는 특정 컴폰넌트가 있다. 컴포넌트 간에 공유 상태를 복제하는 대신 이를 공통 공유 상위로 올리고 필요한 하위 항목에 전달한다.

작업을 진행하면 앱이 변경된다. 상태의 각 부분이 어디에 있는지 파악하는 동안 상태를 아래로 이동하거나 위로 이동하는 것이 일반적이다. 이것은 모두 과정의 일부다.

몇 가지 추가 컴포넌트를 사용하여 이것이 실제로 어떤 느낌인지 확인하려면 Thinking in React를 읽어봐라.

Recap

  • 두 컴포넌트를 조정하려면 해당 컴포넌트의 상태를 공통 상위 컴포넌트로 이동해라.
  • 그런 다음 공통 부모의 prop을 통해 정보를 전달한다.
  • 마지막으로 자식이 부모의 상태를 변경할 수 있도록 이벤트 핸들러를 전달한다.
  • 컴포넌트를 “제어됨”(props에 의해 구동됨)또는 “제어되지 않음”(상태에 의해 구동됨)으로 간주하는 것이 유용하다.
profile
반갑습니다.

0개의 댓글