[nextJS] getLayout: 페이지간 상태를 공유하고 싶을 때
안녕하세요 오늘은 nextJS에서 제공하는 layout(v.13) getLayout()에 대해 다뤄보겠습니다.
회사 보일러플레이트에서는 현재 기존에 전역레벨에만 사용하던 Redux toolkit을 과감히 버리고 현재는 전역 / 지역마다 상태를 관리할 수 있는 React context API를 사용하고 있습니다.
실제로 사용하는 범위에서 상태와 핸들러를 관리하다보니 코드의 응집도가 높아서 매우 만족하며 사용하고 있습니다. context api의 렌더링 이슈는 use-context-selector가 나온 뒤로 적용시켜 리랜더링의 이슈도 (어느정도는)해결 되었죠.
공통된 로직과 패턴에는 provider를 각 페이지마다 감싸줘 공통된 로직과 패턴이라면 같이 사용할 수 있어 유용하게 사용하고 있는데요.
작업을 하다보니 페이지간 context를 공유해야 할 일이 생겼습니다.
예를 들어 캐시를 충전한다고 가정했을 때 충전할 리스트가 있고, 리스트를 선택했을 때 결제 페이지로 이동한다고 하면
선택한 캐시 아이템의 정보를 (id, amount..) 결제 페이지에서 사용해야 하겠죠.
// 충전 페이지
function CashRechargePage() {
return (
<>
<NextSeo title="캐시충전" />
<BasisLayout header={'캐시충전'} content={<CashRecharge />} />
</>
);
}
export default withPaymentProvider(CashRechargePage);
// 결제 페이지
function CashPaymentPage() {
return (
<>
<NextSeo title="결제하기" />
<BasisLayout header={'결제하기'} content={<CashPayment />} />
</>
);
}
export default withPaymentProvider(CashPaymentPage);
이럴 때 해당 정보를 전달하거나 저장하는 방법은 2가지가 있는데요.
1. router query로 전달
2. 전역 상태에 저장
3. 하나의 페이지에 state로 관리 (이럴걸 그랬지..)
router query로 전달하는 방법은 편하지만 유저가 id와 amount정보를 잃게 될 수도 있으니 조작될 수 있는 가능성이 있죠.
useRouter의 as 속성으로 해당 정보를 숨겨서 갈 수도 있겠지만, query는 string이 기 때문에 여러 데이터를 넘겼을 때 타입을 체크하고 바꿔줘야 하는 불편함이 있었습니다.
전역상태에 저장하는 방법 또한 구현은 가능하나 코드의 응집도가 떨어져 제가 선호하는 방식은 아니었습니다.
그럼, 페이지간에 context 즉 provider를 공유할 수 있는 방안은 없을까요?
getLayout() / layout.ts
nextJS의 layout을 이용하면 가능합니다.
Per-Page Layouts
Page Layouts If you need multiple layouts, you can add a property getLayout to your page, allowing you to return a React component for the layout. This allows you to define the layout on a per-page basis. Since we're returning a function, we can have complex nested layouts if desired.
When navigating between pages, we want to persist page state (input values, scroll position, etc.) for a Single-Page Application (SPA) experience.
This layout pattern enables state persistence because the React component tree is maintained between page transitions. With the component tree, React can understand which elements have changed to preserve state.
간단히 말하면 페이지마다의 레이아웃을 공유할 때 사용할 수 있으며 페이지간의 state를 지속하고 싶을 경우 사용할 수 있다고 합니다.
정확히 제 요구사항에 적합한 기능이라는 생각이 들었습니다.
레이아웃 패턴은 페이지 전환 사이에 React 컴포넌트 트리가 유지되기 때문에 상태 지속성을 가능하게 합니다. 컴포넌트 트리를 통해 React는 상태를 보존하기 위해 어떤 요소가 변경되었는지 이해할 수 있다고 합니다.
이러한 프로세스를 reconciliation라고 합니다. React가 어떤 요소가 변경되었는지 이해하고 싶다면 해당 문서를 살펴보세요.
적용 방법
이런 식으로 Page에 getLayout을 오브젝트로 등록하고
// pages/index.tsx
import type { ReactElement } from 'react'
import Layout from '../components/layout'
import NestedLayout from '../components/nested-layout'
import type { NextPageWithLayout } from './_app'
const Page: NextPageWithLayout = () => {
return <p>hello world</p>
}
Page.getLayout = function getLayout(page: ReactElement) {
return (
<Layout>
<NestedLayout>{page}</NestedLayout>
</Layout>
)
}
export default Page
최상단 페이지 app.tsx 에서 렌더링하는 컴포넌트의 getLayout를 체크해 가지고 옵니다.
// page/_app.tsx
import type { ReactElement, ReactNode } from 'react'
import type { NextPage } from 'next'
import type { AppProps } from 'next/app'
export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page)
return getLayout(<Component {...pageProps} />)
}
실무 적용
저뿐만 아니라 팀원 분들도 getLayout을 사용해야 하는데 문서의 예제처럼 각각 관리되는 것보다는 하나의 파일에서 관리되는 것이 좋을 것이라고 생각했습니다.
따라서 현재는 NextPageLayout.tsx이라는 파일 안에서 관리하고 있습니다.
import { ReactElement } from 'react';
src/components/@Layout/NextPageLayout.tsx
import { PaymentProvider } from '@/containers/CashPayment/context/usePaymentContext';
import { ProfileProvider } from '@/containers/My/Profile/context/useProfileContext';
import withVerifiedGuard from '@/hocs/profile/withVerifiedGuard';
import withAuthGuard from '@/hocs/withAuthGuard';
import withUnAuthGuard from '@/hocs/withUnAuthGuard';
const WithAuth = withAuthGuard(({ children }) => <>{children}</>);
const WithUnAuth = withUnAuthGuard(({ children }) => <>{children}</>);
const WithVerified = withVerifiedGuard(({ children }) => <>{children}</>);
export const getPaymentLayout = (page: ReactElement) => {
return (
<WithAuth>
<PaymentProvider>{page}</PaymentProvider>;
</WithAuth>
);
};
export const getProfileLayout = (page: ReactElement) => {
return (
<WithAuth>
<ProfileProvider>
<WithVerified>{page}</WithVerified>
</ProfileProvider>
</WithAuth>
);
};
export const getUserLayout = (page: ReactElement) => {
return <WithUnAuth>{page}</WithUnAuth>;
};
Layout에서 적용시킬 hook과 provider를 등록시키고 페이지 단에서 는 필요한 레이아웃만을 가져와 적용시켜 로직이 명확히 분리되었습니다.
import { NextSeo } from 'next-seo';
import BasisLayout from '@/components/@Layout/BasisLayout';
import { getPaymentLayout } from '@/components/@Layout/NextPageLayout';
import CashPayment from '@/containers/CashPayment';
function CashPaymentPage() {
return (
<>
<NextSeo title="결제하기" />
<BasisLayout header={'결제하기'} content={<CashPayment />} />
</>
);
}
CashPaymentPage.getLayout = getPaymentLayout;
export default CashPaymentPage;
정리
이번 포스팅에서는 page 간 state를 공유할 때 유용한 NextJS의 layout에 대해 다뤄보았습니다.
꼭 콘텍스트가 아니더라고 layout을 사용하면 state가 공유되니 유용하게 사용하실 수 있을 것이라고 생각합니다.