컴포넌트 간 상태 공유의 어려움
"상태 관리 입문"에서는 리액트 애플리케이션 개발에서 가장 중요하고도 도전적인 주제 중 하나인 상태 관리(State Management) 의 세계로 들어갈 준비를 하겠습니다.
그 첫 단계로, 왜 "상태 관리"라는 별도의 개념과 라이브러리가 필요한지, 그리고 리액트의 기본 기능만으로는 컴포넌트 간에 상태를 공유하는 것이 왜 어려운지에 대해 알아보겠습니다. 이 어려움을 이해하는 것이 효과적인 상태 관리 솔루션을 선택하고 활용하는 첫걸음입니다.
리액트의 상태와 데이터 흐름 복습
리액트의 핵심은 컴포넌트 기반 아키텍처와 단방향 데이터 흐름(Unidirectional Data Flow) 입니다.
- 컴포넌트의 상태 (
useState
): 각 컴포넌트는 독립적인 상태(state
)를 가질 수 있으며, 이 상태가 변경되면 해당 컴포넌트와 그 하위 컴포넌트들이 다시 렌더링됩니다. - 프롭스 (
props
): 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달하는 유일한 방법입니다. 데이터는 항상 위에서 아래로(부모에서 자식으로) 흐릅니다.
이러한 단방향 데이터 흐름은 애플리케이션의 동작을 예측 가능하게 만들고 디버깅을 쉽게 한다는 큰 장점을 가지고 있습니다. 하지만 특정 상황에서는 오히려 상태 공유를 복잡하게 만듭니다.
컴포넌트 간 상태 공유의 일반적인 시나리오
리액트 애플리케이션을 개발하다 보면 필연적으로 여러 컴포넌트가 동일한 상태를 참조하거나 변경해야 하는 상황에 직면합니다. 몇 가지 예시를 들어보겠습니다.
쇼핑 카트 (장바구니)
ProductList
컴포넌트 (상품 목록 표시)AddToCartButton
컴포넌트 (상품 추가 버튼)CartIcon
컴포넌트 (장바구니에 담긴 상품 개수 표시)CartModal
컴포넌트 (장바구니 상세 내용 표시) 이 모든 컴포넌트들은 장바구니 상태(상품 목록, 총 개수, 총 가격) 를 공유하고 변경해야 합니다.
사용자 인증 (로그인 상태)
LoginPage
컴포넌트 (로그인 폼)Header
컴포넌트 (로그인/로그아웃 버튼, 사용자 이름 표시)Dashboard
컴포넌트 (로그인 여부에 따라 내용 변경) 애플리케이션 전반에 걸쳐 사용자의 로그인 상태가 공유되어야 합니다.
다크 모드/라이트 모드 테마
ThemeToggle
컴포넌트 (테마 전환 버튼)Header
,Footer
,MainContent
등 다양한 컴포넌트 (테마에 따라 스타일 변경)
현재 테마 모드 상태는 여러 컴포넌트에 영향을 미칩니다.
상태 공유의 어려움: "Props Drilling"
리액트의 기본인 props
를 사용해서 위와 같은 상태 공유 시나리오를 해결하려고 하면 곧바로 Props Drilling이라는 문제에 부딪히게 됩니다.
Props Drilling이란?
상태(state)를 필요한 자식 컴포넌트에 도달시키기 위해, 중간에 위치한 여러 컴포넌트들을 거쳐 props
로 계속해서 전달해야 하는 현상을 의미합니다.
Props Drilling 예시: 테마 전환
간단한 테마 전환 예시를 통해 Props Drilling의 문제를 시각화해 보겠습니다.
import React, { useState } from 'react';
import Header from './Header';
import MainContent from './MainContent';
import Footer from './Footer';
function App() {
const [theme, setTheme] = useState('light'); // 최상위 App에서 테마 상태 관리
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
<h1>프롭스 드릴링 예시</h1>
<button onClick={toggleTheme}>테마 전환 ({theme})</button>
{/* Header, MainContent, Footer에 theme과 toggleTheme을 전달해야 함 */}
<Header theme={theme} toggleTheme={toggleTheme} />
<MainContent theme={theme} />
<Footer theme={theme} />
</div>
);
}
export default App;
import React from 'react';
function Header({ theme, toggleTheme }) { // theme, toggleTheme을 props로 받음
return (
<header style={{ backgroundColor: theme === 'light' ? '#eee' : '#333', color: theme === 'light' ? '#333' : '#eee', padding: '15px', borderRadius: '5px', marginBottom: '20px' }}>
<h2>헤더</h2>
<p>현재 테마: {theme}</p>
{/* Header 내부에서 토글 버튼을 사용하지 않아도 props를 계속 내려줘야 함 */}
{/* <button onClick={toggleTheme}>테마 전환 (여기에 있어도)</button> */}
</header>
);
}
export default Header;
import React from 'react';
import ContentSection from './ContentSection'; // 더 깊은 자식 컴포넌트
function MainContent({ theme }) { // theme을 props로 받음
return (
<div style={{ backgroundColor: theme === 'light' ? '#f8f8f8' : '#555', color: theme === 'light' ? '#333' : '#eee', padding: '20px', borderRadius: '5px', marginBottom: '20px' }}>
<h3>메인 콘텐츠</h3>
<p>이곳은 주요 내용이 표시되는 공간입니다.</p>
{/* ContentSection에 theme을 또 전달해야 함 */}
<ContentSection theme={theme} />
</div>
);
}
export default MainContent;
import React from 'react';
function ContentSection({ theme }) { // theme을 props로 받음
return (
<div style={{ border: `1px solid ${theme === 'light' ? '#ccc' : '#888'}`, padding: '15px', borderRadius: '5px', marginTop: '15px' }}>
<h4>콘텐츠 섹션</h4>
<p>테마에 따라 이 텍스트의 색깔도 변합니다.</p>
</div>
);
}
export default ContentSection;
import React from 'react';
function Footer({ theme }) { // theme을 props로 받음
return (
<footer style={{ backgroundColor: theme === 'light' ? '#eee' : '#333', color: theme === 'light' ? '#333' : '#eee', padding: '15px', borderRadius: '5px' }}>
<p>© 2024 테마 앱. 현재 테마: {theme}</p>
</footer>
);
}
export default Footer;
위 예시에서 theme
상태는 App
컴포넌트에서 선언되었지만, 실제 theme
값을 사용하는 ContentSection
컴포넌트까지 도달하기 위해 Header
, MainContent
를 거쳐 props
로 계속해서 전달되어야 합니다. 심지어 Header
컴포넌트는 theme
값을 직접 사용하지 않더라도 toggleTheme
함수를 하위 컴포넌트에 전달해야 한다면 계속해서 props
를 받아야 합니다.
프롭스 드릴링의 문제점
코드의 복잡성 증가: 중간 컴포넌트들은 자신에게는 필요 없는 props
를 단순히 전달하기 위해 정의해야 하므로, 코드가 불필요하게 길어지고 복잡해집니다.
유지보수성 저하: 상태를 사용하는 컴포넌트가 변경되거나, 중간에 새로운 컴포넌트가 추가되면, 이 props
를 전달하는 모든 상위/중간 컴포넌트의 코드도 함께 수정해야 합니다. 이는 작은 변경에도 큰 영향을 미칠 수 있습니다.
디버깅의 어려움: 특정 상태의 값이 어디서부터 오는지, 어떤 컴포넌트들을 거쳐 전달되는지 파악하기 어려워집니다.
성능 저하 가능성 (간접적): props
가 변경되면 해당 컴포넌트와 모든 자식 컴포넌트가 재렌더링됩니다. 비록 리액트가 효율적인 재렌더링을 하지만, 불필요한 props
전달은 잠재적으로 최적화를 어렵게 만들 수 있습니다.
상태 관리의 필요성
리액트의 단방향 데이터 흐름은 예측 가능하고 안정적인 구조를 제공하지만, 애플리케이션의 규모가 커지고 컴포넌트 트리가 깊어지며 여러 컴포넌트가 광범위하게 상태를 공유해야 할 때, 프롭스 드릴링과 같은 문제에 직면하게 됩니다.
이러한 문제들을 해결하고, 애플리케이션 전반에 걸쳐 상태를 효율적이고 전역적으로 관리하기 위해 상태 관리 라이브러리(State Management Library) 가 필요해집니다. 상태 관리 라이브러리는 공통된 상태를 컴포넌트 트리의 최상단이나 특정 위치에 저장하고, 필요한 컴포넌트에서만 이 상태에 직접 접근하고 변경할 수 있도록 하는 메커니즘을 제공합니다.
"컴포넌트 간 상태 공유의 어려움"은 여기까지입니다. 이 장에서는 리액트의 기본 상태 관리 방식과 단방향 데이터 흐름의 장점을 복습하고, 여러 컴포넌트 간에 상태를 공유할 때 발생하는 주요 문제점인 프롭스 드릴링의 개념과 그로 인한 어려움을 구체적인 예시를 통해 이해했습니다.
이제 왜 별도의 상태 관리 솔루션이 필요한지에 대한 공감대가 형성되었을 것입니다.