icon
7장 : 상태 관리 입문

컴포넌트 간 상태 공유의 어려움


"상태 관리 입문"에서는 리액트 애플리케이션 개발에서 가장 중요하고도 도전적인 주제 중 하나인 상태 관리(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의 문제를 시각화해 보겠습니다.

App.js (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;
Header.js
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;
MainContent.js
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;
ContentSection.js
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;
Footer.js
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>&copy; 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) 가 필요해집니다. 상태 관리 라이브러리는 공통된 상태를 컴포넌트 트리의 최상단이나 특정 위치에 저장하고, 필요한 컴포넌트에서만 이 상태에 직접 접근하고 변경할 수 있도록 하는 메커니즘을 제공합니다.


"컴포넌트 간 상태 공유의 어려움"은 여기까지입니다. 이 장에서는 리액트의 기본 상태 관리 방식과 단방향 데이터 흐름의 장점을 복습하고, 여러 컴포넌트 간에 상태를 공유할 때 발생하는 주요 문제점인 프롭스 드릴링의 개념과 그로 인한 어려움을 구체적인 예시를 통해 이해했습니다.

이제 왜 별도의 상태 관리 솔루션이 필요한지에 대한 공감대가 형성되었을 것입니다.