Context API가 있는데 왜 상태 관리 라이브러리가 필요할까

이태관·2025년 1월 13일
post-thumbnail

React를 공부하다 보면 전역 상태 관리를 다루는 다양한 라이브러리인 Redux, Zustand, Recoil 같은 상태관리 라이브러리를 접하게 됩니다.

하지만 이미 React에 비슷한 기능을 하는 Context API가 존재하는데, 왜 이런 추가 라이브러리가 필요할까요?

이번 글에서는 웹개발에서 자주 쓰는 구조인 Header, Content, Footer컴포넌트와 isDark라는 전역상태를 관리해야하는 상황을 가정해서 Context API의 한계와 상태 관리 라이브러리의 필요성, 그리고 Zustand를 활용한 상태 관리 예제를 통해 그 차이를 알아보겠습니다.


1. Context API의 역할

Context API는 React에서 컴포넌트 트리 전체에 데이터를 전파하기 위해 제공되는 내장 도구입니다.
주로 전역 상태를 관리하거나 깊게 중첩된 컴포넌트로 데이터를 전달할 때 유용합니다.
Context API의 기본 사용법은 아래와 같습니다

import { useState, useContext } from "react";
import { createContext } from "react";

const ThemeContext = createContext(null);

function App() {
  const [isDark, setIsDark] = useState(false);

  return (
    <ThemeContext.Provider value={{ isDark, setIsDark }}>
      <Header />
      <Content />
      <Footer />
    </ThemeContext.Provider>
  );
}

function Header() {
  const { isDark, setIsDark } = useContext(ThemeContext);
  const toggleTheme = () => {
    setIsDark(!isDark);
  };
  console.log("header render");
  return (
    <header className={"header"}>
      <h2>Header 컴포넌트</h2>
      <div>{`isDark : ${isDark}`}</div>
      <button className="button" onClick={toggleTheme}>
        DarkMode
      </button>
    </header>
  );
}

function Content() {
  console.log("content render");
  return (
    <div className="content">
      <h2>Content 컴포넌트</h2>
    </div>
  );
}

function Footer() {
  console.log("footer render");
  return (
    <footer className="footer">
      <h2>Footer 컴포넌트</h2>
    </footer>
  );
}

export default App;

위 코드는 전역 상태 isDark를 Context API로 관리하는 기본적인 예제입니다. Context API만으로도 전역 상태 관리는 가능합니다. 하지만 다음과 같은 한계가 존재합니다.


2. Context API의 단점과 한계

  1. 불필요한 렌더링 문제
  • 이 문제가 상태관리 라이브러리를 사용하는 가장 큰 이유라고 생각합니다.
  • Context API를 사용하면 Provider 내부의 모든 컴포넌트가 해당 상태를 구독하게 됩니다.
  • 여기서 구독이란 Provider에서 제공하는 값이 변경되면 하위 컴포넌트가 이를 감지하여 리렌더링 하는것을 의미합니다.
  • 아래 gif의 콘솔창을 보면 버튼을 눌러 isDark라는 전역상태를 바꾸면 contentfooter컴포넌트도 리렌더링되는걸 보실 수 있습니다.
  1. 복잡한 상태 관리 어려움
  • 상태가 복잡해지면 서로 다른 상태들 간에 의존성이 생길 수 있습니다.
  • 예를 들어, 사용자가 특정 옵션을 선택하면 다른 옵션이 자동으로 업데이트되어야 하는 상황이 있다고 가정하면, 이를 처리하는 로직이 여러 컴포넌트에 분산될 가능성이 높습니다. 이로 인해 상태 변경이 예상치 못한 부작용을 초래하거나 디버깅이 어려워질 수 있습니다.
  1. 성능 최적화 어려움
  • Context API는 성능 최적화를 위한 기능이 내장되어 있지 않아, 별도로React.memo나 상태를 분리하는 작업이 필요합니다.

3. 상태 관리 라이브러리의 장점

  1. 렌더링 최적화
  • Zustand, Recoil등은 구독하는 상태만 렌더링하도록 설계되어, 불필요한 렌더링을 방지합니다.
  1. 코드 간소화
  • 상태관리 로직이 한곳에 집중되므로 코드가 단순하고 유지보수가 용이합니다.

4. Zustand로 예시코드

이제 Zustand를 사용해서 위에서 설명드린 isDark라는 상태를 관리하는 코드를 리팩토링해보겠습니다.

import { create } from "zustand";

export const ZustandStore = create((set) => ({
  isDark: false,
  toggleTheme: () =>
    set((state) => ({
      isDark: !state.isDark,
    })),
}));

function App() {
  return <Page />;
}

function Page() {
  console.log("page render");
  return (
    <div className="page">
      <Header />
      <Content />
      <Footer />
    </div>
  );
}

function Header() {
  const isDark = ZustandStore((state) => state.isDark);
  const handleToggle = ZustandStore((state) => state.toggleTheme);

  console.log("header render");
  return (
    <header className={"header"}>
      <h2>Header 컴포넌트</h2>
      <div>{`isDark : ${isDark}`}</div>
      <button className="button" onClick={handleToggle}>
        DarkMode
      </button>
    </header>
  );
}

function Content() {
  console.log("content render");
  return (
    <div className="content">
      <h2>Content 컴포넌트</h2>
    </div>
  );
}

function Footer() {
  console.log("footer render");
  return (
    <footer className="footer">
      <h2>Footer 컴포넌트</h2>
    </footer>
  );
}

export default App;

Zustand를 이용한 리펙토링 결과 : 더이상 ContentFooter컴포넌트가 불필요하게 리렌더링 되지 않음


5. 결론

Context API는 소규모 프로젝트에서 Props Drilling을 피하기 위한 충분한 대안이지만, 프로젝트의 규모가 커질수록 불필요한 리렌더링복잡한 상태 관리문제가 발생합니다. 이를 해결하기 위해 Zustand와 같은 상태관리 라이브러리가 등장했으며, 렌더링 최적화와 효율적인 상태 관리가 가능해집니다.

0개의 댓글