컴포넌트 간 State 공유하기
때때로 두 컴포넌트의 state가 항상 함께 변경되기를 원할 수 있습니다. 그렇게 하려면 각 컴포넌트에서 state를 제거하고 가장 가까운 공통 부모 컴포넌트로 옮긴 후 props로 전달해야 합니다. 이 방법을 State 끌어올리기라고 하며 React 코드를 작성할 때 가장 흔히 하는 일 중 하나입니다.
학습 내용
- State 끌어올리기를 통해 컴포넌트 간 state를 공유하는 방법
- 제어 컴포넌트와 비제어 컴포넌트
State 끌어올리기 예시
예시에서는 부모 컴포넌트인 Accordion
이 두 개의 Panel
을 렌더링합니다.
Accordion
Panel
Panel
각 Panel
컴포넌트는 콘텐츠 표시 여부를 결정하는 Boolean형 isActive
상태를 가집니다.
각 패널의 Show 버튼을 눌러봅시다.
Error
Extra 185 of 186 byte(s) found at buffer[1]
한 패널의 버튼을 눌러도 다른 패널에는 영향을 미치지 않고 독립적입니다.
그러나 이제 한 번에 하나의 패널만 열리도록 변경하려고 합니다. 두 번째 패널을 열기 위해선 첫 번째 패널을 닫아야 합니다. 어떻게 해야 할까요?
두 패널을 조정하려면 다음 세 단계를 통해 부모 컴포넌트로 패널의 “State 끌어올리기”가 필요합니다.
- 자식 컴포넌트의 state를 제거합니다.
- 하드 코딩된 값을 공통 부모로부터 전달합니다.
- 공통 부모에 state를 추가하고 이벤트 핸들러와 함께 전달합니다.
이 방법으로 Accordion
컴포넌트가 두 Panel
을 조정하고 한 번에 하나만 열리도록 할 수 있습니다.
Step 1: 자식 컴포넌트에서 state 제거하기
Panel
의 isActive
에 대한 제어권을 부모 컴포넌트에 줄 수 있습니다. 즉 부모 컴포넌트는 isActive
를 Panel
에 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);
activeIndex
가 0
이면 첫 번째 패널이 활성화된 것이고, 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를 공통 부모 컴포넌트로 옮기는 것은 두 패널을 조정할 수 있게 합니다. 두 개의 “보임” 플래그 대신 활성화된 인덱스를 사용하는 것은 한 번에 하나의 패널만 활성화됨을 보장합니다. 그리고 자식 컴포넌트로 이벤트 핸들러를 전달하는 것은 자식 컴포넌트에서 부모의 상태를 변경할 수 있게 합니다.
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> ); }