들어가기에 앞서
우리 큐브(CVVE)의 모바일 고객 애플리케이션은 PHP와 제이쿼리를 사용하여 만들어져 있습니다. 오래된 기술로 볼 수 있지만 충분히 잘 만들어져 동작하며 지금까지 좋은 고객 경험을 제공해 왔습니다.
올해 큐브(CVVE)는 새로운 기능이 많이 추가되었습니다. 그 중 고객 애플리케이션에는 선불카드와 전자영수증 기능이 추가되었습니다. 이에따라 더 나은 고객 경험과 함께 개발과 유지보수의 용이성을 위해 Next.js + Typescript 를 사용하여 새로운 고객 애플리케이션을 만들게 되었습니다.
익숙했던 React.js 와는 또 다른 Next.js 의 페이지 접근 권한 인증 개발을 함께 공유합니다. Next.js 애플리케이션을 개발하며 페이지 접근 권한 인증개발에 고민이 있으신 분들에게 도움이 되었으면 좋겠습니다.
HOC (폐기)
처음 고려한 방법은 HOC(High Order Component, 고차 컴포넌트)를 사용한 방법이었습니다. 페이지 접근 권한 인증을 필요로 하는 컴포넌트에 인증 처리를 하는 컴포넌트를 먼저 거치도록 하는 방법이었습니다. 결과적으로는 Next.js 의 특성을 고려하여 폐기되었지만 그 과정을 공유하고자 남깁니다.
참고
- High Order Component - react doc
- Create a HOC (higher order component) for authentication in Next.js - stackoverflow
- Implement Protected Routes in Next.js docs
- How to create HOC for auth in Next.js? - stackoverflow
최종 작업물
import { NextPage } from "next";
import { useRouter } from "next/router";
export const withAuth = (WrappedComponent: NextPage) => {
return (props: React.ComponentProps<any>) => {
// checks whether we are on client / browser or server.
if (typeof window !== "undefined") {
const Router = useRouter();
const accessToken = sessionStorage.getItem("accessToken");
if (!accessToken) {
Router.replace("/login");
}
}
<WrappedComponent {...props} />;
};
};
에러 해결
Text content does not match server-rendered HTML
에러 원인은 아래 아래 두 React tree가 일치하지 않기 때문에 일어났다.
- pre-rendered(SSR/SSG)
- rendered during the first render in the Browser
인증을 위한 고차 컴포넌트를 아래와 같이 수정 토큰을 가지고 있지 않으면 ‘/login’ 페이지로 이동시키기 때문에 기존 컴포넌트를 return 시켜도 큰 문제는 없지만 ‘/login’ 페이지 이동까지 컴포넌트가 rendering 되면서 볼 수 있는 것이 문제
import { NextPage } from "next";
import { useRouter } from "next/router";
export const withAuth = (WrappedComponent: NextPage) => {
return (props: React.ComponentProps<any>) => {
// checks whether we are on client / browser or server.
const Router = useRouter();
const accessToken = sessionStorage.getItem("accessToken");
if (!accessToken) {
Router.replace("/login");
- return null;
}
- return <WrappedComponent {...props} />;
- return null;
+ <WrappedComponent {...props} />;
};
};
ReferenceError: sessionStorage is not defined
Next.js는 SSR이기 때문에 서버 렌더링 시에 세션스토리지를 사용할 수 없음
import { NextPage } from "next";
import { useRouter } from "next/router";
export const withAuth = (WrappedComponent: NextPage) => {
return (props: React.ComponentProps<any>) => {
// checks whether we are on client / browser or server.
+ if (typeof window !== "undefined") {
const Router = useRouter();
const accessToken = sessionStorage.getItem("accessToken");
if (!accessToken) {
Router.replace("/login");
}
+ }
<WrappedComponent {...props} />;
};
};
getServerSideProps (채택)
getServerSideProps를 사용하여 서버사이드 랜더링 시에 토큰여부를 체크하도록 하였습니다. 그리고 서버사이드에서 접근 불가능한 세션스토리지 대신에 쿠키를 사용합니다. 쿠키에서 토큰 존재 여부를 체크하는 코드는 별도의 유효한 토큰을 체크하는 함수로도 대체가 가능합니다.
참고
- How I can get localstorage data inside getServerSideProps - stackoverflow
- Authentication - Next.js docs
- cookies-next - npm
최종 작업물
서버사이드 렌더링 시에 토큰여부를 체크하여 없으면 로그인 페이지로 리다이렉트 토큰이 존재하면 해당 페이지에의 getServerSideProps의 결과를 반환
export const requireAuthentication = (gssp: any) => async (context: GetServerSidePropsContext) => {
const { req } = context;
const token = req.cookies.accessToken;
if (!token) {
// Redirect to login page
return {
redirect: {
destination: '/login',
permanent: false,
}
};
}
// Run `getServerSideProps` to get page-specific data
const gsspData = await gssp(context);
// Pass page-specific props along with user data from `requireAuthentication` to component
return {
props: {
...gsspData.props,
}
};
}
적용 페이지
TestPage 컴포넌트의 getServerSideProps에 requireAuthentication를 적용하였다. 해당 페이지에 접근하면 서버사이드에서 렌더링을 진행하며 토큰 존재여부를 체크한다.
서버사이드 렌더링 시에 토큰을 쿠키에서 뽑아내 Props로 전달, Hydration 시에 생기는 에러를 방지한다.
import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
interface Props {
cookies: Record<any, any>
}
const TestPage = ({ cookies }: Props) => {
return (...);
}
export const getServerSideProps: GetServerSideProps<Props> = requireAuthentication(async (context: GetServerSidePropsContext) => {
return {
props: {
cookies: context.req.cookies
}
};
})
export default TestPage;
Hydration 이란
- Hydration은 Next.js의 렌더링 과정으로, 모든 페이지를 pre-render 한다.
- Next.js의 pre-render 방식은 두 가지이다.
- SSG (Static-site Generation): 빌드 시에 페이지를 생성
- SSR (Server-side Rendering): 클라이언트 요청 시에 페이지를 생성
- Hydration 과정에서 흔히 나타나는 에러는 Text content does not match server-rendered HTML 이다.
- 에러 원인은 아래 아래 두 React tree가 일치하지 않기 때문에 일어난다.
- pre-rendered(SSR/SSG)
- rendered during the first render in the Browser
글을 마치며
기존 React.js의 CSR에 익숙한 개발자라면 Next.js의 SSR/SSG 방식의 차이에서 오는 혼란을 겪을 수 있습니다. 그래도 더 나은 애플리케이션을 위해 공부하는데 노력을 아끼지 말아야겠습니다.