Context 리렌더링 문제와 해결법

Context는 편리하지만 잘못 쓰면 성능 문제의 원인이 됩니다. Context 값이 바뀌면 그 Context를 구독하는 모든 컴포넌트가 리렌더링되기 때문입니다.

문제: 하나의 값이 바뀌어도 전부 리렌더링

const AppContext = createContext();
 
function App() {
  const [user, setUser] = useState({ name: '홍길동' });
  const [theme, setTheme] = useState('light');
  const [count, setCount] = useState(0);
 
  return (
    <AppContext.Provider value={{ user, theme, count, setCount }}>
      <Header />   {/* user만 씀 */}
      <Sidebar />  {/* theme만 씀 */}
      <Counter />  {/* count만 씀 */}
    </AppContext.Provider>
  );
}

count가 1씩 증가할 때마다 HeaderSidebar도 리렌더링됩니다. count를 전혀 쓰지 않는데도요.

해결 1: Context 분리

관련 없는 값들을 별도 Context로 나눕니다.

const UserContext = createContext();
const ThemeContext = createContext();
const CountContext = createContext();
 
function App() {
  const [user, setUser] = useState({ name: '홍길동' });
  const [theme, setTheme] = useState('light');
  const [count, setCount] = useState(0);
 
  return (
    <UserContext.Provider value={{ user, setUser }}>
      <ThemeContext.Provider value={{ theme, setTheme }}>
        <CountContext.Provider value={{ count, setCount }}>
          <Header />   {/* UserContext만 구독 → count 변경에 리렌더링 안 됨 */}
          <Sidebar />  {/* ThemeContext만 구독 */}
          <Counter />  {/* CountContext만 구독 */}
        </CountContext.Provider>
      </ThemeContext.Provider>
    </UserContext.Provider>
  );
}

이제 count가 바뀌어도 CountContext를 구독하는 Counter만 리렌더링됩니다.

해결 2: value 메모이제이션

Context value로 객체를 넘길 때, 매 렌더링마다 새 객체가 생성됩니다. useMemo로 안정화합니다.

function UserProvider({ children }) {
  const [user, setUser] = useState(null);
 
  // 매 렌더링마다 새 객체 생성 → 모든 소비자 리렌더링
  // const value = { user, setUser }; // 나쁜 예
 
  // user가 바뀔 때만 새 객체 생성
  const value = useMemo(() => ({ user, setUser }), [user]);
 
  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

setUseruseState가 보장하는 안정적인 참조이므로 의존성에 넣을 필요 없습니다.

해결 3: 상태와 dispatch 분리

useReducer를 쓸 때 state와 dispatch를 별도 Context로 분리합니다. dispatch는 항상 같은 참조이므로, dispatch만 쓰는 컴포넌트는 state 변경에 리렌더링되지 않습니다.

const StateContext = createContext();
const DispatchContext = createContext();
 
function Provider({ children }) {
  const [state, dispatch] = useReducer(reducer, initialState);
 
  return (
    <DispatchContext.Provider value={dispatch}>  {/* dispatch는 항상 같은 참조 */}
      <StateContext.Provider value={state}>
        {children}
      </StateContext.Provider>
    </DispatchContext.Provider>
  );
}
 
// state가 바뀌어도 dispatch만 쓰는 컴포넌트는 리렌더링 안 됨
function ActionButton() {
  const dispatch = useContext(DispatchContext);
  return <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>;
}
 
// state가 바뀌면 리렌더링
function Counter() {
  const { count } = useContext(StateContext);
  return <div>{count}</div>;
}

해결 4: 소비자 컴포넌트에 memo 적용

Context를 사용하는 컴포넌트를 memo로 감싸면 Context 값 자체가 바뀔 때만 리렌더링됩니다. 부모의 다른 이유로 인한 리렌더링은 막을 수 있습니다.

const ThemeToggle = memo(function ThemeToggle() {
  const { theme, setTheme } = useContext(ThemeContext);
  return (
    <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
      현재 테마: {theme}
    </button>
  );
});

하지만 Context 값 자체가 바뀌면 memo와 무관하게 리렌더링됩니다.

해결 5: Context 선택적 구독 (고급)

같은 Context에서 특정 값만 구독하고 싶다면, 커스텀 훅에서 useMemo로 필요한 것만 반환합니다.

// Context 전체
const UserContext = createContext();
 
// 이름만 쓰는 컴포넌트용 훅
function useUserName() {
  const { user } = useContext(UserContext);
  return user?.name;
}
 
// 권한만 쓰는 컴포넌트용 훅
function useUserRole() {
  const { user } = useContext(UserContext);
  return user?.role;
}

단, 이 방법만으로는 리렌더링을 막을 수 없습니다. Context 값이 바뀌면 훅이 있는 컴포넌트는 여전히 리렌더링됩니다. 완전한 선택적 구독이 필요하다면 Zustand 같은 외부 상태 관리 라이브러리를 사용하는 것이 현실적입니다.

전략 선택 가이드

Context 리렌더링 문제가 있을 때

1. Context에 관련 없는 값들이 섞여 있나?
   → YES: Context 분리

2. Context value가 매번 새 객체로 생성되나?
   → YES: useMemo로 value 안정화

3. state 읽기와 쓰기를 구분할 수 있나?
   → YES: state/dispatch Context 분리

4. Context 자체를 분리하기 어렵고 값이 자주 바뀌나?
   → Zustand 같은 전용 상태 관리 라이브러리 고려

정리

해결 방법효과복잡도
Context 분리관련 없는 구독자 리렌더링 제거낮음
value 메모이제이션불필요한 참조 변경 방지낮음
state/dispatch 분리dispatch만 쓰는 컴포넌트 최적화중간
소비자에 memo부모 리렌더링 전파 차단낮음

Context를 처음 설계할 때부터 하나의 Context에 너무 많은 것을 넣지 않는 것이 가장 좋은 예방책입니다.