두 개의 컴포넌트의 상태를 보통 동시에 변경하고 싶은 경우가 있다. 이를 위해선, 양쪽에서 상태를 삭제해서 제일 가까운 공통 부모로 이동해, 거기서 상태를 props로 경유해 컴포넌트에 넘긴다. 이건 상태의 리프트 업(lifting state up)으로도 알려져 있고, React 코드를 적을 때 취급하는 가장 일반적인 작업 중 하나이다.
배울 수 있는 것
· 컴포넌트 간 상태 공유 방법
· 제어된(controlled) 컴포넌트와 비제어(uncontrolled) 컴포넌트는 무엇인지
상태의 리프트 업 예시
아래의 예시에선, 부모의 Accordion
컴포넌트가 두 개의 각 Panel
을 렌더링한다.
Accordion
Panel
Panel
각 Panel
컴포넌트에선, 내용을 표시중인지 아닌지를 결정하는 불린형의 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>
);
}
// 이렇게 열고 닫는 형태의 UI를 아코디언이라고 한다.
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>
</>
);
}
양쪽 패널의 버튼을 눌러도, 다른 양쪽의 패널에는 영향이 가지 않는다. 두 개의 패널은 독립되어있다.
- 처음엔, 각 Panel의 isActive 상태는 false이므로, 모든 것이 접혀있다.
- 그 중 하나의 Panel 버튼을 클릭하면, 그 Panel의 isActive의 상태가 갱신된다.
그러나 이번엔 그거을 변경하여, 한 번이 하나의 패널만이 전개되도록 하고싶다고 해보자. 이 설계에선, 두 번째 패널을 전재하면 첫 번째 패널이 접힌다. 어떻게해서 실현하면 좋을까?
이 두 개의 패널을 협조하여 동작시키기 위해선, 아래의 세 단계로, 부모 컴포넌트에 "상태를 리프트 업"할 필요가 있다.
- 자식 컴포넌트에서 상태를 삭제한다.
- 공통 부모 컴포넌트에서 하드 코딩된 데이터를 넘긴다.
- 공통 부모 컴포넌트에서 상태를 추가하고, 이벤트 핸들러와 동시에 밑으로 넘긴다.
이것으로, Accordion
컴포넌트가 양쪽의 Panel
의 조정하는 역할이 되고, 한 번에 한 쪽만을 전개할 수 있게 된다.
1단계: 자식 컴포넌트에서 상태를 삭제하기
Panel
의 isActive
의 제어권을 부모 컴포넌트에 전달하는 형태가 된다. 즉, 부모 컴포넌트가 isActive
를 Panel
에 props
로서 넘겨준다는 것이다. 우선은 Panel
컴포넌트에서 아래 코드를 삭제하자.
const [isActive, setIsActive] = useState(false);
대신, isActive
를 Panel
의 props의 리스트에 추가한다.
function Panel({ title, children, isActive }) {
이걸로, Panel
의 부모 컴포넌트는 isActive
의 props로서 넘겨주는 것으로 제어가 가능하다. 반대로, Panel
컴포넌트는 isActive
의 값을 자기자신이 제어할 수 없게 됐다. 제어가 부모 컴포넌트로 옮겨간 것이다.
2단계: 공통 부모 컴포넌트에서 하드 코딩된 데이터를 넘기기
상태를 리프트 업하기 위해선, 협조하여 동작시키고 싶은 모든 자식 컴포넌트의, 제일 가까운 공통 부모 컴포넌트를 특정할 필요가 있다.
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}>
{/*패널에 isActive 값을 각각 넘겨준다.*/}
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
값을 편집해보고, 화면 상에 일어나는 결과를 확인해보자.
3단계: 공통 부모 컴포넌트에 상태를 추가하기
상태를 리프트 업하는 것으로, 상태로서 지닌 데이터의 의미가 변할 때가 있다.
이번 경우엔, 한 번에 하나의 패널만이 액티브 상태로 있어야 한다. 즉, 공통 부모 컴포넌트로 존재하는 Accordion은, 어떤 패널이 액티브한지 관리할 필요가 있다. 상태 변수로서는, boolean
값 대신에, 액티브한 Panel
의 인덱스를 표현하는 숫자 값을 사용할 수 있다.
const [activeIndex, setActiveIndex] = useState(0);
activeIndex
가 0
일 때는 첫 번째 패널이, 1
일 때는 두 번째 패널이 액티브 상태로 변한다.
이 중 하나의 Panel
의 "Show" 버튼이 클릭될 때도, Accordion
의 액티브 인덱스를 변경할 필요가 있다. activeIndex
라는 상태는 Accordion
내에 정의되는 것이기 때문에, Panel
에서부터 그것을 직접 하는 것은 불가능하다. Accordion
컴포넌트는, props로서 onShow
이벤트 핸들러를 밑으로 넘겨주는 것으로, Panel
컴포넌트가 아코디언 상태를 변경할 수 있도록 명시적으로 허가할 필요가 있다.
<>
<Panel
isActive={activeIndex === 0}
onShow={() => setActiveIndex(0)}
>
...
</Panel>
<Panel
isActive={activeIndex === 1}
onShow={() => setActiveIndex(1)}
>
...
</Panel>
</>
그리고 Panel
내의 <button>
은, 클릭 이벤트 핸들로로서 props로 존재하는 onShow
를 사용한다.
코드
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>
);
}
이걸로 상태의 리프트 업이 완성되었다. 상태를 공통 부모 컴포넌트에 이동한 것으로, 두 개의 패널을 협조적으로 동작하게 만들었다. "표시중" 플래그를 두 개 쓰는 대신 액티브 인덱스를 사용해서, 한 번에 액티브한 패널이 하나만 있는 것이 보증되었다. 또, 이벤트 핸들러의 자식에게 넘겨줘서, 자식이 부모의 상태를 변경할 수 있게 되었다.
제어된 컴포넌트와 비제어 컴포넌트
일반적으로, 로컬 상태를 지닌 컴포넌트를 "비제어(uncontrolled)"되어있다고 부른다. 예를 들면,isActive
라는 상태 변수를 지닌 원래Panel
컴포넌트는, 패널이 액티브인지 아닌지에 관해 부모가 영향을 끼칠 수 없기 때문에, 비제어 컴포넌트이다.
대조적으로, 중요한 정보가 로컬 상태가 아닌 props로 구동될 때, 컴포넌트는 "제어된(controlled)" 것으로 불리는 경우가 있다. 이것으로, 부모 컴포넌트가 그 행동을 완전히 지정할 수 있다.isActive
를 props로서 가진 최종적인Panel
컴포넌트는,Accordion
컴포넌트에 의해 제어된다.
비제어 컴포넌트는, 설정이 적게 필요하기 때문에 부모 컴포넌트 내에 삽입되어 사용되는 것이 간단하게 된다. 그러나, 그것들을 협조하여 동작시키고 싶은 경우 유연성이 없다. 제어된 컴포넌트는 정말 유연하지만, 부모 컴포넌트가 props로 완전히 설정해줄 필요가 있다.
실제로는, "제어된", "비제어"는 기술용어로서 엄밀한 것은 아니다. 각 컴포넌트는 보통, 로컬 상태와 props 양 쪽을, 혼재해서 가지고 있다. 그러나, 컴포넌트가 어떻게 설계되는지, 어떤 기능을 가지고 있는지 얘기할 때엔, 이렇게 생각하는 편이 도움이 된다.
컴포넌트를 작성할 때엔, 이 안의 어떤 정보를(props로) 제어하고, 어떤 정보를(상태를 사용해서) 제어하지 않을지 검토해보자. 그러나 후에 생각을 바꾸고 리팩토링하는 것은 언제나 가능하다.
각 상태의 신뢰 가능한 단 하나의 정보원
React 어플리케이션에선, 많은 컴포넌트가 자기자신의 상태를 계속 유지한다. 일부 상태는, 입력 필드와 같은 말단 컴포넌트(트리의 최하위의 컴포넌트)에 가까운 곳에 존재한다. 일부 상태는 어플리케이션의 상층부에 가까운 곳에 존재할 것이다. 예를 들면, 클라이언트 사이드 라우팅 라이브러리도, React의 상태의 현재 루트를 지니고, props를 사이에 두고 밑으로 넘겨주는 것으로 실제 구현하는 것이 일반적이다.
각각의 상태에 대해, 그것을 '소유'하는 컴포넌트를 선택해보자. 이 원리는, "신뢰 가능한 단 하나의 정보원(single source of truth)"로서 알려져있다. 이것은, 모든 상태가 한 군데에 모여있다는 의미는 아니다. 각각의 상태에 대해, 그 정보를 유지하는 특정 컴포넌트가 존재해야 한다는 의미이다. 컴포넌트 사이에 공유되는 상태는 복제되는 대신, 공통 부모 컴포넌트에 리프트 업되어, 그것을 필요로하는 자식에게 넘겨주는 방식으로 구현하자.
당신의 어플리케이션은 작업을 진행하던 중에 변환해간다. 또 각각의 상태가 어디에 존재해야하는지 모를 경우엔, 상태를 아래에 이동시켜서, 위로 돌려주는 일은 종종 있다. 이것은 개발 프로세스의 일환이다.
좀 더 많은 컴포넌트가 등장하는 예시로 경험적으로 감각을 이해하고 싶은 경우엔, React만의 법칙을 읽어보자.
정리
- 두 개의 컴포넌트를 협조적으로 동작시키려는 경우엔, 상태의 공통 부모 컴포넌트에 이동한다.
- 다음으로, 그 공통 부모 컴포넌트에서 props를 경유하여 정보를 밑으로 넘긴다.
- 마지막으로, 자식이 부모의 상태를 변경할 수 있도록, 이벤트 핸들러를 밑으로 넘긴다.
- 컴포넌트를 "제어된"(props에 의해 구동되는) 것인지 "비제어"(상태에 의해 구동되는) 인지의 관점에서 생각하는 것이 유용하다.