서버 컴포넌트를 Authenticated하게 만드는 패턴

HOC 패턴으로 동일한 로직 제거하기

2025-03-15 |

5

다음과 같이 권한에 따라 진입이 불가능하면서 권한에 따라 특정 페이지로 리다이렉션 시켜주는 페이지가 있었습니다.

  • A: 모든 유저 접근 가능
  • B: 로그인 유저만 접근 가능
  • C: 가입 절차를 마친 유저만 접근 가능

A~C와 같은 3가지 케이스가 있다고 했을 때 다음과 같이 페이지를 나눌 수 있었어요.

  1. 로그인과 같은 페이지
  • 접근 가능: A, B, C
  1. 가입 절차 페이지
  • 접근 가능: B
  • A가 접근하면 로그인 페이지, C가 접근하면 메인 페이지로 이동
  1. 가입 절차를 마친 페이지로 리다이렉션 시키는 페이지
  • 접근 가능: C
  • A가 접근하면 로그인 페이지, B가 접근하면 가입 페이지로 이동

Next.js 서버 컴포넌트에서 다음과 같은 패턴으로 요구사항을 충족시킬 수 있습니다.

typescript

export default async function Page() { const user = await getUser(); if (!user) { redirect('/login'); } if (user.role === 'SIGN_UP') { redirect('/join'); } return ( <main> <h1>{user.name}</h1> </main> ); }

Next.js의 Route Groups를 사용해서 크게 3가지로 layout.tsx를 나누어 권한에 따라 리다이렉션 처리를 해줄 수 있었습니다.

문제 상황

문제 상황은 여기서 생깁니다.

조금 디테일하게 들어가면 위의 로직으로 충족이 안 되는 상황이 생깁니다. 예를 들면 아래와 같이 디테일하게 처리해야 되는 경우처럼요.

typescript

export default async function VIPPage() { const user = await getUser(); if (!user) { redirect('/login'); } if (user.role !== 'VIP' && dayjs(user.birthday).year() <= 1999) { redirect('/'); } return ( <main> <h1>{user.name}</h1> </main> ); }

이런 경우에는 Route Groups의 layout.tsx에서 로직으로 동일하게 처리될 수 없기 때문에, 개별적으로 처리해줘야 합니다.

이러한 케이스가 많지는 않았지만 이런 케이스가 많아질 수록 하나하나의 서버 컴포넌트에서 동일한 로직을 반복적으로 작성하는 것이 번거로워 질 것 같아 재사용할 수 있는 방법이 있을지 고민해보았습니다.

재사용 가능한 HOC 컴포넌트

이러한 케이스를 해결할 수 있는 HOC 컴포넌트를 작성해보았습니다.

typescript

export type AuthenticatedProps = { user: User; }; export function Authenticated<P extends AuthenticatedProps>( Component: React.ComponentType<P>, ) { return async function AuthenticationComponent( props: Omit<P, keyof AuthenticatedProps>, ) { let user: User | undefined; try { const __user = await api().users.me(); user = __user; } catch (error) { redirect('/login'); } return <Component {...(props as P)} user={user} />; }; }

제네릭 타입으로 user를 포함한 타입을 선언해줍니다. 그리고 async 컴포넌트를 리턴하는데 catch에서 로그인 페이지로 리다이렉션 시켜주는 정말 필수적인 동일한 로직만 포함하고 있습니다. 그리고 API를 통해 받은 user를 Component에 props로 전달해줍니다.

typescript

function VIPPage({ user }: AuthenticatedProps) { if (!user) { redirect('/login'); } if (user.role !== 'VIP' && dayjs(user.birthday).year() <= 1999) { redirect('/'); } return ( <main> <h1>{user.name}</h1> </main> ); } export default Authenticated(VIPPage);

이렇게 되면 user를 props로 직접적으로 내려받을 수 있게 되면서 매 컴포넌트마다 유저 API를 호출하는 동일한 로직을 반복적으로 작성할 필요가 없어졌습니다.