Blog

Root Layout의 Provider 중첩을 Compose 패턴으로 정리하기


5 min read

서론#

Next.js 프로젝트를 만들다 보면
전역에서 필요한 Provider가 점점 늘어난다.

처음에는 몇 개 되지 않기 때문에
root layout 안에 직접 중첩해도 크게 불편하지 않다.

하지만 기능이 추가되면서 Provider가 늘어나면
layout.tsx는 점점 읽기 어려워진다.

<Providers>
  <PWAProvider>
    <SnackBarProvider>
      <TermsProvider>
        <ToastProvider>
          <MSWProvider />
          <AuthBootstrap />
          <WebPushProvider>
            <NotificationSSEProvider>
              <main className="w-full flex-1">{children}</main>
              <Footer />
            </NotificationSSEProvider>
          </WebPushProvider>
        </ToastProvider>
      </TermsProvider>
    </SnackBarProvider>
  </PWAProvider>
</Providers>

이 구조는 동작에는 문제가 없지만,
Provider가 많아질수록 layout의 책임이 흐려진다.

이 글은
root layout에 직접 중첩되어 있던 Provider들을
Compose 패턴으로 정리한 과정을 기록한다.

문제였던 구조#

기존 root layout은
여러 Provider가 JSX 안에서 직접 중첩되어 있었다.

Provider는 전역 상태, 알림, 약관, PWA, Web Push, SSE처럼
앱 전체에 필요한 기능을 감싸는 역할을 한다.

문제는 Provider의 개수가 늘어나면서
layout.tsx가 다음 두 가지 책임을 동시에 가지게 된다는 점이었다.

  • 페이지의 기본 구조를 정의하는 책임
  • Provider 중첩 순서를 관리하는 책임

root layout은
앱의 가장 바깥 구조를 보여주는 파일이다.

그런데 Provider 중첩이 깊어지면
정작 중요한 mainFooter 같은 요소가
Provider 구조 안에 묻히게 된다.

Compose 패턴으로 분리하기#

개선 후에는 Provider 조합을
AppProviders 컴포넌트로 분리했다.

<AppProviders>
  <MSWProvider />
  <AuthBootstrap />
  <main className="w-full flex-1">{children}</main>
  <Footer />
</AppProviders>

이제 layout.tsx에서는
앱 전체를 감싸는 Provider가 있다는 사실만 드러난다.

구체적으로 어떤 Provider들이 어떤 순서로 중첩되는지는
AppProviders 내부에서 관리한다.

AppProviders 구현#

Provider 목록은 배열로 관리했다.

type ProviderComponent = ComponentType<{ children: ReactNode }>;

const providers: ProviderComponent[] = [
  QueryProviders,
  PWAProvider,
  SnackBarProvider,
  ToastProvider,
  TermsProvider,
  WebPushProvider,
  NotificationSSEProvider,
];

그리고 reduceRight를 사용해
배열의 순서대로 바깥에서 안쪽으로 Provider를 중첩했다.

const AppProviders = ({ children }: { children: ReactNode }) =>
  providers.reduceRight<ReactNode>(
    (acc, Provider) => <Provider>{acc}</Provider>,
    children
  );

reduceRight를 사용하는 이유는
JSX 중첩 구조와 배열 순서를 맞추기 위해서다.

예를 들어 배열이 다음과 같다면,

const providers = [AProvider, BProvider, CProvider];

결과는 다음 구조가 된다.

<AProvider>
  <BProvider>
    <CProvider>
      {children}
    </CProvider>
  </BProvider>
</AProvider>

즉,
배열의 앞에 있는 Provider일수록
더 바깥쪽 Provider가 된다.

개선된 점#

이번 정리에서 느낀 개선점은
크게 세 가지였다.

1. 유지보수성#

Provider 관리가 단순해졌다.

새 Provider를 추가하거나 제거해야 할 때
layout.tsx의 JSX 중첩 구조를 다시 수정할 필요가 없다.

providers 배열만 수정하면 된다.

const providers: ProviderComponent[] = [
  QueryProviders,
  PWAProvider,
  SnackBarProvider,
  ToastProvider,
  TermsProvider,
  WebPushProvider,
  NotificationSSEProvider,
];

Provider의 순서 역시
배열의 순서로 명확하게 관리할 수 있다.

2. 가독성 향상#

root layout이 훨씬 읽기 쉬워졌다.

기존에는 Provider 중첩을 따라가야
실제 페이지 구조를 확인할 수 있었다.

개선 후에는
layout.tsx에서 다음 요소들이 바로 드러난다.

  • 전역 Provider
  • MSW 초기화
  • AuthBootstrap
  • main 영역
  • Footer

Provider 중첩 구조가 사라지면서
layout.tsx는 앱의 바깥 구조를 보여주는 역할에 더 가까워졌다.

3. 유연성#

Provider를 배열로 관리하면
구조를 바꾸는 비용도 줄어든다.

특정 Provider의 위치를 바꿔야 할 때
깊게 중첩된 JSX를 옮기는 대신
배열 안의 순서만 조정하면 된다.

또한 환경이나 조건에 따라
Provider 목록을 다르게 구성해야 하는 경우에도
배열 기반 구조가 더 다루기 쉽다.

주의할 점#

Compose 패턴으로 정리했다고 해서
Provider 순서의 중요성이 사라지는 것은 아니다.

Provider 중에는
다른 Provider의 context에 의존하는 경우가 있을 수 있다.

예를 들어 어떤 Provider가
QueryClient나 인증 상태를 내부에서 사용한다면,
그 Provider는 필요한 context보다 안쪽에 있어야 한다.

그래서 Provider를 배열로 정리할 때도
순서는 단순한 보기 좋음의 문제가 아니다.

의존 관계를 기준으로
바깥에서 안쪽 순서를 결정해야 한다.

참고한 글과 실제 적용 PR#

이번 정리는
React Provider Composition을 다룬 글을 참고하면서 진행했다.

해당 글에서는
여러 Provider가 피라미드처럼 중첩되는 문제를
배열 기반 composition으로 풀어내는 방식을 소개한다.

내 프로젝트에서는 이 아이디어를 그대로 복사하기보다는,
현재 root layout 구조에 맞게 AppProviders 컴포넌트로 분리했다.

실제 적용은 아래 PR에서 진행했다.

이 PR에서는
root layout에 직접 중첩되어 있던 Provider들을
AppProviders 내부의 providers 배열로 옮겼다.

결과적으로 layout.tsx는
페이지 구조를 보여주는 역할에 더 집중하게 되었고,
Provider 추가와 제거는 별도의 배열에서 관리할 수 있게 되었다.

정리#

Provider를 root layout에 직접 중첩하는 방식은
처음에는 단순하고 직관적이다.

하지만 Provider가 늘어나면
layout.tsx의 가독성이 떨어지고,
파일의 책임도 흐려진다.

이번 개선에서는
전역 Provider 조합을 AppProviders로 분리하고,
reduceRight를 이용해 Compose 패턴으로 정리했다.

그 결과 root layout은
앱의 전체 구조에 집중할 수 있게 되었고,
Provider 관리는 배열 하나로 명확하게 분리되었다.

이 리팩터링은 기능을 바꾸는 작업은 아니지만,
앱의 전역 구조를 더 읽기 쉽게 만드는 정리 작업에 가깝다.

4