개발/React

[중급반] Step 2-4. Context API 심화 및 분리 패턴

ophelisis 2025. 12. 15. 14:01
반응형

React의 내장 기능인 Context API는 Prop Drilling 없이 전역 데이터를 공유하는 강력한 도구입니다. 하지만 잘못 사용하면 성능 저하의 주범이 되기 쉽습니다. 중급 개발자는 Context API의 한계를 정확히 파악하고, 이를 해결하는 **'분리 패턴(Separation Pattern)'**을 사용하여 성능과 편의성을 모두 잡아야 합니다.

이 섹션에서는 Context API의 단점을 최소화하고 효율성을 극대화하는 방법을 다룹니다.


1. 🔍 Context API의 작동 원리 및 문제점

1-1. Context API의 구조와 역할

  • Provider: 데이터를 제공하는 컴포넌트로, value Props를 통해 상태나 함수를 하위 트리에 전달합니다.
  • Consumer: useContext Hook을 사용하여 Provider가 제공하는 value를 가져다 쓰는 컴포넌트입니다.

Context API는 Redux나 Recoil 같은 외부 라이브러리 없이 테마, 사용자 인증 정보 등 전역적으로 필요한 데이터를 전달하는 데 매우 유용합니다.

1-2. Context의 치명적인 문제: 불필요한 리렌더링

Context API의 가장 큰 성능 이슈는 바로 전파 방식에 있습니다.

문제점: Context Provider의 value 객체가 변경되면, 해당 Context를 구독하고 있는 모든 하위 Consumer 컴포넌트는 value 내의 특정 값이 변하지 않았더라도 무조건 리렌더링됩니다.

만약 Context value에 자주 변하는 데이터(예: 알림 개수)와 거의 변하지 않는 데이터(예: 사용자 프로필)가 함께 있다면, 알림 개수 하나 때문에 수많은 컴포넌트가 불필요하게 리렌더링되는 비효율이 발생합니다.

2. 🛡️ Context 분리 전략 (Context Separation Pattern)

불필요한 리렌더링 문제를 해결하는 가장 확실한 방법은 '관심사에 따라 Context를 쪼개는' 것입니다.

2-1. 자주 변하는 상태와 불변 상태의 분리

  • 분리: 자주 변하는 상태(예: 모달 상태, 알림 수)와 거의 변하지 않는 상태(예: 현재 언어, 사용자 정보)를 별도의 Context로 분리합니다.
  • 효과: UserContext의 데이터가 변하더라도 ThemeContext를 구독하는 컴포넌트에는 영향을 주지 않습니다.

2-2. 상태와 액션(함수)의 분리

Context를 **'상태 Context'**와 **'디스패치(액션) Context'**로 분리하는 방법입니다.

  • 상태 Context (StateContext): 읽기 전용 상태 값만 제공합니다. (자주 변함)
  • 디스패치 Context (DispatchContext): 상태를 변경하는 함수만 제공합니다. (함수는 useCallback으로 감싸면 한 번 생성된 후 변하지 않음)

이렇게 분리하면, 상태를 변경하는 함수를 가져다 쓰는 컴포넌트가 상태 값 자체를 가져다 쓰는 컴포넌트보다 훨씬 적게 리렌더링됩니다.

3. 🎣 커스텀 훅을 이용한 Context 사용 패턴

Context API를 사용할 때 코드를 깔끔하게 유지하고 구독 로직을 단순화하는 모범 사례입니다.

3-1. Context Hook으로 감싸기

Context Provider와 Consumer를 직접 노출하지 않고, Custom Hook으로 감싸서 사용합니다.

JavaScript
 
// Before (복잡함)
const user = useContext(UserContext); 

// After (간결함)
const user = useUser(); // useUser Hook 안에 Context 로직 캡슐화

// useUser Hook 구현 예시
const useUser = () => {
  const context = useContext(UserContext);
  // Context가 없을 때 에러 처리 로직 등을 추가할 수 있음
  if (!context) throw new Error('useUser must be used within a UserProvider');
  return context;
};

이 방식은 코드의 가독성을 높이고 Context 사용 시 필수적인 예외 처리 로직을 강제하는 효과가 있습니다.

3-2. useContext + useMemo를 이용한 최적화

Provider 컴포넌트의 value prop은 매 렌더링마다 새로운 객체 {}를 생성합니다. 이것이 Context를 구독하는 모든 하위 컴포넌트의 리렌더링을 유발합니다.

  • 해결책: value prop에 전달되는 객체를 반드시 **useMemo**로 감싸서, 객체 내부의 핵심 상태가 변하지 않는 한 참조(Reference)가 유지되도록 해야 합니다.

🛠️ 2-4 실습 미션 (Challenge)

  1. 리렌더링 문제 재현: 하나의 Context (AppStateContext)에 자주 변하는 상태 (count)와 자주 변하지 않는 상태 (userInfo)를 모두 담으세요. count를 변경했을 때, userInfo만 사용하는 컴포넌트까지 리렌더링되는 것을 DevTools Profiler로 확인합니다.
  2. Context 분리 적용: CountContext와 UserContext 두 개로 분리하고, 각 Context를 구독하는 컴포넌트들이 각자의 상태가 변경될 때만 리렌더링되는 것을 확인하여 성능 개선을 검증하세요.
반응형