Sharing State Between Components — 전체 번역
원문: react.dev/learn/sharing-state-between-components
정확한 표현이 필요하면 항상 원문을 함께 보세요. 번역은 자연스럽게 풀어 옮긴 의역이며, 코드 안의 영문 식별자·UI 문구(
Show,Almaty, Kazakhstan등)는 원문 그대로 둡니다.원문은 CC BY 4.0 라이선스로 react.dev에서 발행되었으며, 본 번역은 그 라이선스에 따라 출처를 표시하고 재배포합니다.
들어가며
가끔, 두 컴포넌트의 state가 항상 함께 바뀌어야 할 때가 있습니다. 그럴 때는 두 컴포넌트에서 state를 모두 제거하고, 가장 가까운 공통 부모 컴포넌트로 옮긴 다음, props로 다시 두 컴포넌트에 내려주세요. 이를 상태 끌어올리기(lifting state up) 라고 부르며, React 코드를 짜면서 가장 자주 하게 될 일 중 하나입니다.
이 문서에서 배우게 될 것
- 컴포넌트들 사이에서 state를 끌어올려 공유하는 방법
- 제어 컴포넌트(controlled) 와 비제어 컴포넌트(uncontrolled) 가 무엇인가
예제로 보는 끌어올리기
다음 예제에서, 부모 Accordion 컴포넌트는 두 개의 Panel을 분리해 렌더링합니다.
AccordionPanelPanel
각 Panel 컴포넌트는 콘텐츠가 보일지를 결정하는 boolean 타입의 isActive state를 가집니다.
두 패널 모두에서 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>
</>
);
}h3, p { margin: 5px 0px; }
.panel {
padding: 10px;
border: 1px solid #aaa;
}한 패널의 버튼을 눌러도 다른 패널은 영향을 받지 않는 것에 주목하세요 — 두 패널은 독립적입니다.
📊 처음 트리 —
Accordion아래 두Panel이 있고, 둘 다isActive: false로 접혀 있습니다.클릭 후 — 한
Panel의 버튼을 누르면, 그 패널의isActive만 갱신됩니다. 다른 패널은 그대로입니다.
그런데 이제, 항상 한 패널만 펼쳐지도록 바꾸고 싶다고 합시다. 이 디자인에서는 두 번째 패널을 펼치면 첫 번째 패널이 접혀야 합니다. 어떻게 할 수 있을까요?
두 패널을 조율(coordinate) 하려면, 그들의 state를 부모 컴포넌트로 “끌어올려야” 합니다. 세 단계로 진행합니다.
- 자식 컴포넌트에서 state를 제거합니다 (Remove).
- 공통 부모로부터 하드코딩된 데이터를 전달합니다 (Pass).
- 공통 부모에 state를 추가하고, 이벤트 핸들러와 함께 내려보냅니다 (Add).
이렇게 하면 Accordion 컴포넌트가 두 Panel을 조율해 한 번에 하나만 펼치도록 만들 수 있습니다.
Step 1: 자식 컴포넌트에서 state 제거하기
Panel의 isActive 제어권을 부모 컴포넌트에 넘길 것입니다. 이는 부모 컴포넌트가 isActive를 props로 Panel에 전달한다는 뜻입니다. 먼저 Panel 컴포넌트에서 이 줄을 제거하세요.
const [isActive, setIsActive] = useState(false);대신, isActive를 Panel의 props 목록에 추가합니다.
function Panel({ title, children, isActive }) {이제 Panel의 부모 컴포넌트가 props로 내려보내 isActive를 제어할 수 있습니다. 반대로, Panel 컴포넌트는 이제 isActive 값에 대한 제어권이 없습니다 — 부모 컴포넌트의 몫이 되었습니다.
Step 2: 공통 부모에서 하드코딩된 데이터 전달하기
state를 끌어올리기 위해서는, 조율하고 싶은 두 자식 컴포넌트의 가장 가까운 공통 부모 컴포넌트를 찾아야 합니다.
Accordion(가장 가까운 공통 부모)PanelPanel
이 예제에서는 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를 끌어올리는 일은 state로 무엇을 저장할지 그 본질을 바꾸는 경우가 많습니다.
이 경우, 한 번에 하나의 패널만 활성이 되어야 합니다. 즉, 공통 부모인 Accordion 컴포넌트가 어떤 패널이 활성인지를 추적해야 합니다. boolean 값 대신, 활성 패널의 인덱스로 숫자를 state 변수에 사용할 수 있습니다.
const [activeIndex, setActiveIndex] = useState(0);activeIndex가 0이면 첫 패널이 활성이고, 1이면 두 번째 패널이 활성입니다.
어느 Panel에서든 “Show” 버튼을 클릭하면 Accordion의 active index가 바뀌어야 합니다. Panel은 activeIndex state를 직접 설정할 수 없습니다 — Accordion 안에 정의된 state이기 때문입니다. Accordion 컴포넌트는 이벤트 핸들러를 props로 내려보내 , Panel 컴포넌트가 자신의 state를 바꿀 수 있도록 명시적으로 허용해야 합니다.
<>
<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를 공통 부모로 옮긴 덕분에, 두 패널을 조율할 수 있게 되었습니다. 두 개의 “is shown” 플래그 대신 active index를 사용한 덕분에 한 번에 하나의 패널만 활성이 되도록 보장했습니다. 그리고 자식에게 이벤트 핸들러를 내려보낸 덕분에, 자식이 부모의 state를 바꿀 수 있게 되었습니다.
📊 처음 상태 —
Accordion의activeIndex가0이므로, 첫Panel은isActive = true를 받습니다.변경 후 —
Accordion의activeIndex가1로 바뀌면, 이번엔 두 번째Panel이isActive = true를 받습니다.
더 깊게 — 제어 컴포넌트와 비제어 컴포넌트
자기 자신의 로컬 state를 가진 컴포넌트를 비제어(uncontrolled) 라고 부르는 것은 흔합니다. 예를 들어 처음의 Panel 컴포넌트는 isActive state 변수를 가지고 있어 비제어입니다 — 부모가 패널이 활성인지 여부에 영향을 줄 수 없으니까요.
반대로, 컴포넌트의 중요한 정보가 자체 로컬 state가 아니라 props로 주도된다면, 그 컴포넌트는 제어(controlled) 라고 말할 수 있습니다. 이렇게 하면 부모 컴포넌트가 그 자식의 행동을 완전히 결정할 수 있습니다. isActive prop을 가진 마지막 Panel 컴포넌트는 Accordion이 제어합니다.
비제어 컴포넌트는 부모 안에서 사용하기 더 쉽습니다 — 설정이 적게 필요하기 때문입니다. 그러나 함께 조율하고 싶을 때는 덜 유연합니다. 제어 컴포넌트는 최대로 유연하지만, 부모 컴포넌트가 props로 그 행동을 완전히 설정해야 합니다.
실무에서는, “제어” 와 “비제어” 가 엄격한 기술 용어는 아닙니다 — 각 컴포넌트는 보통 로컬 state와 props가 섞여 있습니다. 하지만 컴포넌트가 어떻게 설계되었고 어떤 능력을 제공하는지에 대해 이야기할 때 유용한 표현입니다.
컴포넌트를 작성할 때, 어떤 정보가 props로 제어되어야 하고, 어떤 정보가 state로 비제어되어야 할지를 고민해보세요. 그러나 언제든지 마음을 바꿔서 리팩토링할 수 있습니다.
각 state에는 하나의 source of truth
React 앱에서, 많은 컴포넌트가 자기 자신의 state를 가집니다. 어떤 state는 잎 컴포넌트(leaf, 트리 맨 아래) 에 가까이 있을 수 있습니다 — 입력 같은 곳입니다. 다른 state는 앱의 위쪽에 더 가까이 있을 수도 있습니다. 예를 들어, 클라이언트 사이드 라우팅 라이브러리도 보통 현재 라우트를 React state로 저장하고 props로 내려보내는 식으로 구현됩니다!
각각의 state 조각마다, 그것을 “소유” 하는 컴포넌트를 골라야 합니다. 이 원칙은 “single source of truth” 라고도 알려져 있습니다. 모든 state가 한 곳에 있다는 뜻이 아니라, 각 state 조각마다 그 정보를 보유하는 특정 컴포넌트가 있다는 뜻입니다. 컴포넌트들 사이에서 공유하는 state를 복제하는 대신, 그들의 공통 부모로 끌어올리고, 그것이 필요한 자식들에게 내려보내세요.
여러분의 앱은 작업하면서 바뀝니다. 각 state 조각이 어디에 있어야 할지 아직 알아가는 동안, state를 아래로 옮겼다가 다시 위로 옮기는 일이 흔합니다. 이 모든 과정이 작업의 일부입니다!
이 감각을 더 많은 컴포넌트로 실습해보고 싶다면 Thinking in React 를 읽어보세요.
요약
- 두 컴포넌트를 조율하고 싶다면, 그들의 state를 공통 부모로 옮기세요.
- 그다음, 정보를 공통 부모에서 props로 내려보내세요.
- 마지막으로, 이벤트 핸들러를 내려보내 자식이 부모의 state를 바꿀 수 있게 하세요.
- 컴포넌트를 “제어”(props로 driven) 또는 “비제어”(state로 driven)로 생각해보는 것이 유용합니다.
연습 문제
문제 1 — 동기화된 입력
두 입력은 독립적입니다. 둘이 동기화되도록 만드세요 — 한 입력을 편집하면 다른 입력도 같은 텍스트로 갱신되어야 하고, 그 반대도 마찬가지입니다.
💡 힌트: 두 입력의 state를 부모 컴포넌트로 끌어올려야 합니다.
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>
);
}풀이 보기
text state 변수와 handleChange 핸들러를 부모 컴포넌트로 옮기세요. 그다음 둘을 props로 두 Input 컴포넌트에 모두 내려보내세요. 이렇게 하면 두 입력이 동기화됩니다.
import { useState } from 'react';
export default function SyncedInputs() {
const [text, setText] = useState('');
function handleChange(e) {
setText(e.target.value);
}
return (
<>
<Input
label="First input"
value={text}
onChange={handleChange}
/>
<Input
label="Second input"
value={text}
onChange={handleChange}
/>
</>
);
}
function Input({ label, value, onChange }) {
return (
<label>
{label}
{' '}
<input
value={value}
onChange={onChange}
/>
</label>
);
}문제 2 — 리스트 필터링
다음 예제에서, SearchBar는 텍스트 입력을 제어하는 자체의 query state를 가집니다. 그 부모 FilterableList 컴포넌트는 아이템들의 List를 표시하지만, 검색 query를 고려하지 않습니다.
filterItems(foods, query) 함수를 사용해 검색 query에 따라 리스트를 필터링하세요. 변경을 테스트하려면, 입력에 “s”를 입력했을 때 리스트가 “Sushi”, “Shish kebab”, “Dim sum”으로 좁혀지는지 확인하세요.
filterItems는 이미 구현되어 있고 import되어 있으니 직접 작성할 필요 없습니다!
💡 힌트:
querystate와handleChange핸들러를SearchBar에서 제거하고,FilterableList로 옮기고 싶을 것입니다. 그다음query와onChangeprops로SearchBar에 내려보내세요.
import { useState } from 'react';
import { foods, filterItems } from './data.js';
export default function FilterableList() {
return (
<>
<SearchBar />
<hr />
<List items={foods} />
</>
);
}
function SearchBar() {
const [query, setQuery] = useState('');
function handleChange(e) {
setQuery(e.target.value);
}
return (
<label>
Search:{' '}
<input
value={query}
onChange={handleChange}
/>
</label>
);
}
function List({ items }) {
return (
<table>
<tbody>
{items.map(food => (
<tr key={food.id}>
<td>{food.name}</td>
<td>{food.description}</td>
</tr>
))}
</tbody>
</table>
);
}풀이 보기
query state를 FilterableList 컴포넌트로 끌어올리세요. filterItems(foods, query)를 호출해 필터링된 리스트를 얻고, List에 내려보내세요. 이제 query 입력을 바꾸면 리스트에 반영됩니다.
import { useState } from 'react';
import { foods, filterItems } from './data.js';
export default function FilterableList() {
const [query, setQuery] = useState('');
const results = filterItems(foods, query);
function handleChange(e) {
setQuery(e.target.value);
}
return (
<>
<SearchBar
query={query}
onChange={handleChange}
/>
<hr />
<List items={results} />
</>
);
}
function SearchBar({ query, onChange }) {
return (
<label>
Search:{' '}
<input
value={query}
onChange={onChange}
/>
</label>
);
}
function List({ items }) {
return (
<table>
<tbody>
{items.map(food => (
<tr key={food.id}>
<td>{food.name}</td>
<td>{food.description}</td>
</tr>
))}
</tbody>
</table>
);
}🇬🇧 원문 페이지: Sharing State Between Components — react.dev
본 번역은 CC BY 4.0 하에 react.dev 커뮤니티가 작성한 원문을 한국어로 옮긴 것입니다. 정확한 표현이 필요하면 항상 원문을 참고하세요.





