안녕하세요. 인재 채용 플랫폼 잡다의 웹 프론트엔드 개발을 담당하고 있는 배준형입니다.
최근 잡다 매칭 서비스와 관련된 백오피스 개발을 시작했는데요. 로그인 여부에 따른 페이지 접근 제어 기능 구현이 필요했습니다. 로그인 여부를 확인하고, 로그인하지 않은 사용자는 로그인 페이지로 리다이렉트 처리하는 작업이 전체 페이지에 걸쳐 반복적으로 발생할 수 있는 상황이었습니다.
이를 해결하기 위해 Next.JS Middleware 기능을 활용했는데요. 중복 코드를 줄이고 효율적인 로그인 여부 확인 및 리다이렉트 처리를 구현할 수 있었습니다. 관련하여 알게 된 내용을 공유하고자 합니다.
Next.js 미들웨어는 서버 측에서 요청을 처리하기 전에 코드를 실행할 수 있는 기능입니다. 이를 통해 다음과 같은 다양한 작업을 수행할 수 있습니다.
Next.js 미들웨어는 Next.js 12.2 버전부터 안정화되었는데요. 요청 처리 과정에서 다음과 같은 순서대로 실행됩니다.
next.config.js
에서 설정된 headers
next.config.js
에서 설정된 redirects
middleware
: 페이지 리라이트, 리다이렉트 등을 수행합니다.next.config.js
에서 설정된 beforeFiles
(페이지 리라이트)public/
, _next/static/
, pages/
, app/
등next.config.js
에서 설정된 afterFiles
(페이지 리라이트)/blog/[slug]
next.config.js
에서 설정된 fallback
(페이지 리라이트)next.config.js
에서 설정된 항목들과 미들웨어는 모두 위 순서대로 실행되는데, 미들웨어를 설정하고 항상 실행되는 것이 싫다면 특정 경로에서만 실행하도록 설정할 수도 있습니다. 이를 위해서는 config matcher
를 사용하여 원하는 경로를 지정하면 됩니다.
앞서 살펴본 미들웨어는 어떤 방식으로 활용할 수 있을까요? 로그인 여부 확인, 리다이렉트 처리, 라우팅 제어, 로깅 등 다양한 용도로 활용할 수 있으며, 중복 코드를 줄이고 개발 효율성을 높일 수 있습니다.
쿠키 조회
next/server
패키지에서 NextRequest
와 NextResponse
타입을 불러와 요청과 응답 객체를 생성합니다. 다음으로, request.cookies
객체를 사용하여 쿠키에 접근하고 특정 쿠키 값을 확인하거나 모든 쿠키를 가져올 수 있습니다.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
let cookie = request.cookies.get('nextjs')
console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
const allCookies = request.cookies.getAll()
console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
request.cookies.has('nextjs') // => true
request.cookies.delete('nextjs')
request.cookies.has('nextjs') // => false
// Setting cookies on the response using the `ResponseCookies` API
const response = NextResponse.next()
response.cookies.set('vercel', 'fast')
response.cookies.set({
name: 'vercel',
value: 'fast',
path: '/',
})
cookie = response.cookies.get('vercel')
console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
// The outgoing response will have a `Set-Cookie:vercel=fast;path=/` header.
return response
}
이 코드는 NextJS 미들웨어 공식 홈페이지에서 확인할 수 있는 코드인데요. Request, Response 모두에서 쿠키를 관리할 수 있습니다.
Routing 제어
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
return NextResponse.redirect('/new-url');
}
쿠키에 접근하는 것 외에도 Routing 제어도 가능합니다. Cookies를 확인해서 인증을 거치고, 특정 권한을 갖고 있지 않은 유저를 리다이렉션 처리할 수 있겠죠.
Logging
import { NextResponse, type NextRequest } from "next/server";
export default function middleware(request: NextRequest) {
const { nextUrl, method, headers } = request;
const { pathname } = nextUrl;
console.log(`[${method}] ${pathname}`);
console.log(headers);
return NextResponse.next();
}
각 요청에 대해 요청 정보를 추출하여 콘솔에 정보를 출력할 수도 있습니다. 이를 통해 디버깅, 에러 추적 등에 활용할 수 있을 것 같아요.
미들웨어는 Next.js에서 강력한 기능을 제공하지만, 모든 상황에 적합한 것은 아닙니다. 로그인 여부 확인과 리다이렉트 처리를 미들웨어 없이 구현하는 방법도 존재하며, 미들웨어를 사용하지 않는 것이 상황에 따라 더 적합할 수 있습니다. 미들웨어를 적용한 것과 적용하지 않은 것의 차이를 알아보기 위해 미들웨어를 사용하지 않고 로그인 여부를 확인하는 방법에 대해 먼저 알아보겠습니다.
우선, 백오피스 로그인 흐름은 다음과 같은데요.
accessToken
을 쿠키에 저장accessToken
존재 여부로 로그인 여부 판단accessToken
만료 시 쿠키에서 제거accessToken
은 API 응답 에러 처리이를 각 Page에서 확인해야 합니다. 미들웨어 없이 로그인 여부를 확인하는 방법을 먼저 알아보면 아래와 같은 방법들이 있을 것 같아요.
useEffect
로 리다이렉트페이지 컴포넌트에서 직접 로그인 여부 확인 방법 중 하나로 useEffect
Hook을 사용하여 컴포넌트 마운트 시 쿠키에 accessToken
존재 여부를 확인하고 리다이렉트 처리하는 방법을 살펴보겠습니다.
예시 코드
import { useEffect } from "react";
import { useCookies } from "react-cookie";
import { useRouter } from "next/router";
const ProtectedPage = () => {
const [cookies] = useCookies();
const router = useRouter();
useEffect(() => {
const accessToken = cookies.get("accessToken");
if (!accessToken) {
router.push("/login");
}
}, []);
return (
<div>
<h1>보호된 페이지</h1>
<p>이 페이지는 로그인된 사용자만 접근할 수 있습니다.</p>
</div>
);
};
export default ProtectedPage;
장점
단점
getStaticProps
또는 getServerSideProps
페이지 컴포넌트에서 직접 로그인 여부를 확인하는 방법 외에, getStaticProps
또는 getServerSideProps
함수를 사용하여 로그인 여부를 확인하고 리다이렉트 처리하는 방법도 존재합니다.
예시 코드
const ProtectedPage = () => {
// ...
}
export async function getStaticProps(context: GetStaticPropsContext) {
const { cookies } = context.req;
const accessToken = cookies.get(ACCESS_TOKEN_KEY);
if (!accessToken) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
// ...
return {
props: {
// ...
},
};
}
장점
단점
getStaticProps
는 빌드 타임에 실행되므로 페이지 로딩 속도에 영향을 줄 수 있습니다.getServerSideProps
는 매 요청마다 실행되므로 서버 부하가 증가할 수 있습니다.HOC (Higher-Order Component)는 컴포넌트를 반환하는 컴포넌트로 공통 기능을 HOC에서 처리하여 중복 코드를 줄일 수 있습니다.
예시 코드
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useCookies } from "react-cookie";
const withAuth =
<P,>(WrappedComponent: React.ComponentType<P>) =>
(props: React.PropsWithChildren<P>) => {
const [cookies] = useCookies();
const router = useRouter();
useEffect(() => {
const accessToken = cookies.get("accessToken");
if (!accessToken) {
router.push("/login");
}
}, []);
return <WrappedComponent {...props} />;
};
export default withAuth;
// 사용하는 곳에서 HOC로 컴포넌트를 감싸 사용합니다.
const ProtectedPage() {
return (
<div>
<AuthMyPage />
</div>
);
}
export default withAuth(ProtectedPage);
장점:
단점:
withAuth
외에도 여러 HOC가 만들어진다면 코드가 복잡해집니다.export default withAuth(withStore(withSomeLogic(Page))));
위 코드들은 직관적이고 간결합니다. 위 경우 말고도 여러가지 방법이 있을 것 같아요.
그런데, 언급한 방법 모두 로그인이 되어 있어야 하는 페이지가 많아질수록 중복 코드가 많아지게 됩니다. 리다이렉트 하는 코드만 분리할 수 있다고 하더라도 page 파일마다 작성해 줘야 하기에 완전히 중복을 제거할 수도 없고요.
이 경우 미들웨어를 활용하면 중복 코드를 최대한 줄이고, 한 곳에서 리다이렉트 처리를 컨트롤할 수 있습니다. 미들웨어는 middleware.ts
(or .js
) 파일을 pages
, app
, src
등의 디렉토리에 만들어 사용하면 되는데요.
예시 코드
// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
const AUTH_PAGES = ["/", "/login"];
export default function middleware(request: NextRequest) {
const { nextUrl, cookies } = request;
const { origin, pathname } = nextUrl;
const accessToken = cookies.get(ACCESS_TOKEN_KEY);
// 로그인이 필요 없는 페이지
if (AUTH_PAGES.some((page) => pathname.startsWith(page))) {
// 로그인 되어 있는 경우 메인 페이지로 리다이렉트
if (accessToken) {
return NextResponse.redirect(MAIN_PAGE);
} else {
// 로그인이 필요 없는 페이지는 그냥 다음 요청으로 진행
return NextResponse.next();
}
}
// 로그인이 필요한 페이지
if (!accessToken) {
// 로그인 페이지로 리다이렉트
return NextResponse.redirect(LOGIN_PAGE);
}
// 로그인 되어 있는 경우 요청 페이지로 진행
return NextResponse.next();
}
AUTH_PAGES
배열에 로그인이 필요 없는 페이지 경로를 설정합니다.accessToken
값을 쿠키에서 가져옵니다./
, /login
페이지에선 로그인 창을 보여주고, 그 외의 페이지에선 로그인이 되어 있는 상태여야 합니다. 그 과정을 미들웨어로 처리한 것인데, 각 페이지에서 로그인 여부를 확인하고 리다이렉트 시키는 과정을 미들웨어 하나로 해결했습니다. 이제 page 작업에 로그인 여부를 확인할 필요 없이 필요한 동작만 처리하면 되겠네요.
Next js Middleware를 이용해 Cookie, Routing 제어, Logging 등 다양하게 활용할 수 있는데요. 저는 /
, /login
두 페이지 외에 전체 페이지에 로그인 여부를 확인하고 리다이렉트 하는 기능에 활용했습니다. 결과적으로 중복되는 작업을 완전히 줄였고, 이후 작업에선 로그인 여부를 확인하지 않아도 돼서 조금이나마 편해졌습니다. 예시 코드의 경우 accessToken 값을 확인하고 없다면 리다이렉트 하는 코드만 존재하는데요. 이를 잘 활용하여 Refresh Token을 재발급 하는데도 사용할 수 있습니다.
제 경우엔 로그인 페이지
와 로그인 페이지 외 모든 페이지
2가지로 나뉘기에 미들웨어 사용이 적절했던 것 같아요. 그런데 미들웨어가 항상 좋은 것은 아닙니다. 모든 페이지에 로그인 여부 확인 로직이 필요한 경우가 아니라면 불필요한 오버헤드가 발생할 수 있습니다.
그런 경우엔 미들웨어 대신 useEffect
나 getServerSideProps
같은 기능을 활용하여 필요한 곳에서만 처리를 하는 것이 더 좋은 방법이 되겠죠. 따라서 프로젝트 규모나 코드 관리 방식, 개발자 경험 등을 고려해서 적절히 선택하면 좋을 것 같습니다.
와우 너무 좋은 자료인 것 같습니다 :)