Context API 지옥에서 벗어나 라우터 레벨 캡슐화 달성하기
객실 선택 ➝ 투숙객 정보 입력 ➝ 결제 ➝ 예약 확정. 호텔 예약 시스템의 프론트엔드는 이처럼 순차적이고 복잡한 상태를 물고 넘어가는 거대한 파이프라인입니다. 기존 프로젝트에서는 이 수많은 상태를 상위 컴포넌트에서 관리하며 하위로 내려주는 전형적인 Props Drilling 패턴을 사용하고 있었고, 이는 곧 한계에 부딪혔습니다.
무거운 Redux를 도입하기엔 보일러플레이트가 부담스러웠고, React의 Context API를 활용하되 '어떻게 하면 현명하게 쪼개고 감출 수 있을까?' 에 대한 아키텍처적 고민을 시작했습니다.
The Problem: Provider Hell과 열려있는 상태
Context API를 쓴다고 했을 때 가장 먼저 마주하는 안티 패턴은 App.tsx 최상단에 모든 Provider를 몰아넣는 것입니다.
// 안티 패턴: Provider Hell
<UserProvider>
<ConfigProvider>
<ReservationProvider>
<PaymentProvider>
<App />
</PaymentProvider>
</ReservationProvider>
</ConfigProvider>
</UserProvider>
이런 구조는 두 가지 치명적인 문제를 낳습니다.
- 불필요한 리렌더링: 예약 정보 하나가 바뀌었는데, 상관없는 결제 컴포넌트나 글로벌 컴포넌트까지 리렌더링 영향을 받습니다.
- 캡슐화 파괴: 결제 도메인 밖의 엉뚱한 페이지에서도
useContext(ReservationContext)를 호출해 예약 상태를 마음대로 변형(Side Effect)할 수 있는 위험이 존재합니다.
The Solution: 도메인 격리와 Custom Hook 은닉화
1. 라우터(Router) 레벨의 Provider 격리
저는 도메인 주도 설계(DDD) 관점을 프론트엔드에 차용했습니다. 최상단에는 앱 구동에 필수적인 User와 Config만 남기고, 예약과 결제 관련된 Provider는 해당 기능이 동작하는 개별 라우터 레벨로 위치를 끌어내렸습니다.
결과적으로 예약 라우터에 진입해야만 예약 Context가 마운트되며, 도메인을 벗어나면 상태가 깔끔하게 언마운트되어 메모리 관리와 리렌더링 최적화를 동시에 이뤄냈습니다.
**2. Custom Hook을 통한 접근 통제 **
Context를 날것 그대로 노출하지 않고, 해당 로직을 처리하는 Custom Hook(useReservation)을 구현하여 상태 접근을 철저히 통제했습니다.
// useReservation.ts
export const useReservation = () => {
const context = useContext(ReservationContext);
if (!context) {
throw new Error('useReservation은 ReservationProvider 내부에서만 사용할 수 있습니다.');
}
return context;
};
이러한 에러 바운더리 처리를 통해, 해당 라우터 밖에서는 상태를 변형할 수 없도록 강제 했습니다. 비정상적인 접근이나 라우터 우회를 100% 차단하여 예약 프로세스의 순차성을 엄격히 보장할 수 있었습니다.
The Result & Retrospective
React 생태계에서 '상태 관리'는 영원한 숙제입니다. 이 프로젝트를 통해 단순히 어떤 라이브러리를 쓰느냐보다, 상태의 '경계'를 어떻게 나누고 '접근'을 어떻게 통제할 것인가 가 훨씬 더 중요한 아키텍처적 과제임을 체감했습니다.
기능별로 Provider를 쪼개고 Hook으로 캡슐화한 결과, 코드의 예측 가능성이 극대화되었고 동료들이 코드를 파악하기도 훨씬 쉬워졌습니다. 향후에는 서버 상태와 클라이언트 상태를 더 명확히 분리하기 위해 React Query의 도입을 적극적으로 검토해 볼 계획입니다.