컴포넌트 간 State 공유하기

때때로 두 컴포넌트의 state가 항상 함께 변경되기를 원할 수 있습니다. 그렇게 하려면 각 컴포넌트에서 state를 제거하고 가장 가까운 공통 부모 컴포넌트로 옮긴 후 props로 전달해야 합니다. 이 방법을 State 끌어올리기라고 하며 React 코드를 작성할 때 가장 흔히 하는 일 중 하나입니다.

학습 내용

  • State 끌어올리기를 통해 컴포넌트 간 state를 공유하는 방법
  • 제어 컴포넌트와 비제어 컴포넌트

State 끌어올리기 예시

예시에서는 부모 컴포넌트인 Accordion이 두 개의 Panel을 렌더링합니다.

  • Accordion
    • Panel
    • Panel

Panel 컴포넌트는 콘텐츠 표시 여부를 결정하는 Boolean형 isActive 상태를 가집니다.

각 패널의 Show 버튼을 눌러봅시다.

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

Error

Extra 185 of 186 byte(s) found at buffer[1]

한 패널의 버튼을 눌러도 다른 패널에는 영향을 미치지 않고 독립적입니다.

Accordion이라는 이름의 하나의 부모와 Panel이라는 이름의 두 자식으로 구성된 세 컴포넌트 트리를 나타내는 다이어그램입니다. 두 Panel 컴포넌트는 값이 false인 isActive를 가집니다.
Accordion이라는 이름의 하나의 부모와 Panel이라는 이름의 두 자식으로 구성된 세 컴포넌트 트리를 나타내는 다이어그램입니다. 두 Panel 컴포넌트는 값이 false인 isActive를 가집니다.

처음에는 각 PanelisActive state가 false이기 때문에 두 컴포넌트 모두 닫힌 상태로 보입니다.

이전과 동일한 그림에서 자식 중 첫 번째 Panel 컴포넌트의 강조 표시된 isActive가 값이 true로 변경된 클릭을 나타냅니다. 두 번째 Panel 컴포넌트는 여전히 false 값을 가집니다.
이전과 동일한 그림에서 자식 중 첫 번째 Panel 컴포넌트의 강조 표시된 isActive가 값이 true로 변경된 클릭을 나타냅니다. 두 번째 Panel 컴포넌트는 여전히 false 값을 가집니다.

Panel의 버튼 중 어느 것을 클릭하더라도 클릭한 해당 PanelisActive state만 변경됩니다.

그러나 이제 한 번에 하나의 패널만 열리도록 변경하려고 합니다. 두 번째 패널을 열기 위해선 첫 번째 패널을 닫아야 합니다. 어떻게 해야 할까요?

두 패널을 조정하려면 다음 세 단계를 통해 부모 컴포넌트로 패널의 “State 끌어올리기”가 필요합니다.

  1. 자식 컴포넌트의 state를 제거합니다.
  2. 하드 코딩된 값을 공통 부모로부터 전달합니다.
  3. 공통 부모에 state를 추가하고 이벤트 핸들러와 함께 전달합니다.

이 방법으로 Accordion 컴포넌트가 두 Panel을 조정하고 한 번에 하나만 열리도록 할 수 있습니다.

Step 1: 자식 컴포넌트에서 state 제거하기

PanelisActive에 대한 제어권을 부모 컴포넌트에 줄 수 있습니다. 즉 부모 컴포넌트는 isActivePanel에 prop으로 전달합니다. 다음 줄을 Panel 컴포넌트에서 제거하는 것으로 시작해봅시다.

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

대신 Panel의 prop 목록에 isActive를 추가합니다.

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

이제 Panel의 부모 컴포넌트는 props 내리꽂기를 통해 isActive를 제어할 수 있습니다. 반대로 Panel 컴포넌트는 isActive를 제어할 수 없습니다. 이제 부모 컴포넌트에 달려 있습니다.

Step 2: 하드 코딩된 데이터를 부모 컴포넌트로 전달하기

state를 올리려면, 조정하려는 자식 컴포넌트의 가장 가까운 공통 부모 컴포넌트에 두어야 합니다.

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

예시에서는 Accordion 컴포넌트입니다. 이 컴포넌트는 두 패널의 상위에 있고 props를 제어할 수 있기 때문에 현재 어느 패널이 활성화되었는지에 대한 “진리의 원천(source of truth)“이 됩니다. Accordion 컴포넌트가 하드 코딩된 값을 가지는 isActive를 (예를 들면 true) 두 패널에 전달하도록 만듭니다.

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: 공통 부모에 state 추가하기

상태 끌어올리기는 종종 state로 저장하고 있는 것의 특성을 바꿉니다.

이 케이스에서는, 한 번에 하나의 패널만 활성화되어야 합니다. 이를 위해 공통 부모 컴포넌트인 Accordian어떤 패널이 활성화된 패널인지 추적하고 있어야 합니다. state 변수에 boolean 값을 사용하는 대신, 활성화되어있는 Panel의 인덱스 숫자를 사용할 수 있습니다.

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

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

Panel에서 “Show” 버튼을 클릭하면 Accordion의 활성화된 인덱스를 변경해야 합니다. activeIndex state는 Accordion 내에서 정의되었기 때문에 Panel은 값을 직접 설정할 수 없습니다. Accordion 컴포넌트는 Panel 컴포넌트가 state를 변경할 수 있음을 이벤트 핸들러를 prop으로 전달하기를 통해 명시적으로 허용해야 합니다.

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

이렇게 상태 끌어올리기가 완성됩니다! state를 공통 부모 컴포넌트로 옮기는 것은 두 패널을 조정할 수 있게 합니다. 두 개의 “보임” 플래그 대신 활성화된 인덱스를 사용하는 것은 한 번에 하나의 패널만 활성화됨을 보장합니다. 그리고 자식 컴포넌트로 이벤트 핸들러를 전달하는 것은 자식 컴포넌트에서 부모의 상태를 변경할 수 있게 합니다.

Accordion이라는 이름의 하나의 부모와 Panel이라는 이름의 두 자식으로 구성된 세 컴포넌트 트리를 나타내는 다이어그램입니다. Accordion은 값이 0인 activeIndex를 가지며, 첫 번째 패널의 isActive에 true를, 두 번째 패널의 isActive에 false를 반환합니다.
Accordion이라는 이름의 하나의 부모와 Panel이라는 이름의 두 자식으로 구성된 세 컴포넌트 트리를 나타내는 다이어그램입니다. Accordion은 값이 0인 activeIndex를 가지며, 첫 번째 패널의 isActive에 true를, 두 번째 패널의 isActive에 false를 반환합니다.

처음에 AccordionactiveIndex0이므로 첫 번째 PanelisActive = true를 받습니다.

이전과 동일한 그림에서 부모 컴포넌트 Accordion의 강조 표시된 activeIndex가 값이 1로 변경된 클릭을 나타냅니다. 두 하위 Panel 컴포넌트에 대한 전달 흐름도 강조 표시되어 isActive 값이 반대로 변경됨을 나타냅니다: 첫 번째 패널은 false, 두 번째 패널은 true
이전과 동일한 그림에서 부모 컴포넌트 Accordion의 강조 표시된 activeIndex가 값이 1로 변경된 클릭을 나타냅니다. 두 하위 Panel 컴포넌트에 대한 전달 흐름도 강조 표시되어 isActive 값이 반대로 변경됨을 나타냅니다: 첫 번째 패널은 false, 두 번째 패널은 true

AccordionactiveIndex state가 1로 변경되면 두 번째 PanelisActive = true를 받게 됩니다.

Deep Dive

제어와 비제어 컴포넌트

“제어되지 않은” 몇몇 지역 state를 갖는 컴포넌트를 사용하는 것은 흔한 일입니다. 예를 들어 isActive state를 갖는 원래의 Panel 컴포넌트는 해당 컴포넌트의 부모에서 패널의 활성화 여부에 영향을 줄 수 없기 때문에 제어되지 않습니다.

반대로 컴포넌트의 중요한 정보가 자체 지역 state 대신 props에 의해 만들어지는 경우 컴포넌트가 “제어된다”고 합니다. 이를 통해 부모 컴포넌트가 동작을 완전히 지정할 수 있습니다. isActive prop을 갖는 최종 Panel 컴포넌트는 Accordion 컴포넌트에 의해 제어됩니다.

비제어 컴포넌트는 설정할 것이 적어 부모 컴포넌트에서 사용하기 더 쉽습니다. 하지만 여러 컴포넌트를 함께 조정하려고 할 때 비제어 컴포넌트는 덜 유연합니다. 제어 컴포넌트는 최대한으로 유연하지만, 부모 컴포넌트에서 props로 충분히 설정해주어야 합니다.

실제로 “제어”와 “비제어”는 엄격한 기술 용어가 아니며 일반적으로 컴포넌트는 지역 state와 props를 혼합해서 사용합니다. 그러나 이런 구분은 컴포넌트의 설계와 제공하는 기능에 관해 설명하는데 유용한 방법입니다.

컴포넌트를 작성할 때 어떤 정보가 (props를 통해) 제어되어야 하고 어떤 정보가 (state를 통해) 제어되지 않아야 하는지 고려하세요. 그렇지만 언제든 마음이 바뀔 수 있고 나중에 리팩토링 할 수 있습니다.

각 state의 단일 진리의 원천

React 애플리케이션에서 많은 컴포넌트는 자체 state를 가집니다. 일부 상태는 입력처럼 리프 컴포넌트(트리 맨 아래에 있는 컴포넌트)와 가깝게 “생존”합니다. 다른 상태는 앱의 상단에 더 가깝게 “생존”할 수 있습니다. 예를 들면 클라이언트 측 라우팅 라이브러리도 현재 경로를 React state로 저장하고 props로 전달하도록 구현되어 있습니다!

각각의 고유한 state에 대해 어떤 컴포넌트가 “소유”할지 고를 수 있습니다. 이 원칙은 또한 “단일 진리의 원천” 을 갖는 것으로 알려져 있습니다. 이는 모든 state가 한 곳에 존재한다는 의미가 아니라 그 정보를 가지고 있는 특정 컴포넌트가 있다는 것을 말합니다. 컴포넌트 간의 공유된 state를 중복하는 대신 그들의 공통 부모로 끌어올리고 필요한 자식에게 전달하세요.

작업이 진행되면서 애플리케이션은 계속 변합니다. 각 state가 어디에 “생존”해야 할지 고민하는 동안 state를 아래로 이동하거나 다시 올리는 것은 흔히 있는 일입니다. 이건 모두 과정의 일부입니다!

컴포넌트를 몇 개 더 사용하여 어떤 느낌인지 알아보려면 React로 사고하기를 읽어보세요.

요약

  • 두 컴포넌트를 조정하고 싶을 때 state를 그들의 공통 부모로 이동합니다.
  • 그리고 공통 부모로부터 props를 통해 정보를 전달합니다.
  • 마지막으로 이벤트 핸들러를 전달해 자식에서 부모의 state를 변경할 수 있도록 합니다.
  • 컴포넌트를 (props로부터) “제어”할지 (state로부터) “비제어” 할지 고려하면 유용합니다.

챌린지 1 of 2:
동기화된 입력

아래 두 입력은 독립적입니다. 두 입력의 동기화 상태를 유지하세요. 한 입력을 수정하면 다른 입력도 같은 문구로 변경되어야 하며 반대 경우도 동일합니다.

import { useState } from 'react';

export default function SyncedInputs() {
  return (
    <>
      <Input label="First input" />
      <Input label="Second input" />
    </>
  );
}

function Input({ label }) {
  const [text, setText] = useState('');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <label>
      {label}
      {' '}
      <input
        value={text}
        onChange={handleChange}
      />
    </label>
  );
}