프로젝트를 진행하다가 로그인 여부에 따라 페이지 접근을 제어하는 기능을 구현해야 했습니다. 처음에는 각 페이지 컴포넌트마다 로그인 체크 로직을 넣어야 하나 했는데, 보호해야 할 페이지가 여러 개이고 한 곳에서 관리하는 게 효율적이라는 생각이 들었습니다. 찾아보니 이런 공통 로직은 미들웨어로 한 곳에서 처리하는 게 일반적이었고, Next.js 16에서는 proxy.ts 파일이 그 역할을 한다는 것을 알게 되었습니다. 이 글은 그 과정에서 공부한 내용을 정리한 글입니다.
제가 만든 서비스는 로그인한 사용자만 LLM 레시피 추출 기능을 사용하고, 마이페이지에서 레시피를 관리할 수 있도록 구현하고 싶었습니다.
로그인 여부를 체크해야 하는 페이지는 아래와 같습니다 — 사실상 서비스의 모든 페이지입니다.
각 페이지 컴포넌트마다 로그인 체크 로직을 작성한다면 동일한 코드가 여러 곳에 흩어지고, 나중에 수정할 때도 모든 파일을 일일이 찾아가야 하는 문제가 생깁니다. 한 곳에서 관리할 수 있는 방법이 필요하다고 생각했습니다.
한 곳에서 관리할 수 있는 방법을 찾다가 미들웨어라는 개념이 떠올랐습니다. 미들웨어란 요청이 실제로 처리되기 전에 실행되는 중간 레이어입니다. 모든 페이지 요청이 미들웨어를 거쳐서 들어오기 때문에, 이 영역에서 로그인 여부를 체크하는 로직을 관리하면 좋겠다는 생각을 했습니다.
그래서 Next.js에서는 미들웨어를 어떻게 작성하는지 공식 문서를 찾아봤습니다.
Next.js 16부터 기존의 middleware.ts는 proxy.ts로 이름이 바뀌었습니다. 기능은 동일하고, 이름이 역할을 더 잘 반영하도록 변경된 것입니다. proxy.ts는 요청이 완료되기 전에 코드를 실행할 수 있게 해주는 파일로, 요청을 가로채서 리다이렉트, 리라이트, 헤더 수정 등을 처리할 수 있습니다.

공식 문서에서는 아래와 같은 상황에서 사용을 권장합니다.
proxy.ts는 모든 요청마다 실행되기 때문에 여기서 무거운 작업을 하면 전체 페이지 로딩 성능에 영향을 줍니다. 공식 문서에서 명시적으로 금지하는 경우는 아래와 같습니다.
복잡한 로직은 API Routes나 Server Component에서 처리하는 것이 적절합니다.
제가 구현하려는 것은 쿠키에 저장된 토큰의 존재 여부만 확인하는 가벼운 체크입니다. DB 조회나 외부 API 호출 없이 빠르게 판단할 수 있기 때문에 적절한 케이스라고 판단해 proxy.ts에 로그인 체크 로직을 구현하기로 결정했습니다.
아래는 실제 프로젝트에 적용한 proxy.ts 코드입니다.
실제 코드 보러가기
import { withAuth } from 'next-auth/middleware';
import { NextResponse } from 'next/server';
export default withAuth(
function middleware(req) {
const { token } = req.nextauth;
const { pathname } = req.nextUrl;
// 인증은 되었으나 추가 정보 입력(isComplete)이 안 된 경우 /signup으로 리다이렉트
if (token && !token.isComplete && pathname !== '/signup') {
return NextResponse.redirect(new URL('/signup', req.url));
}
return NextResponse.next();
},
{
callbacks: {
authorized: ({ token }) => !!token,
},
pages: {
signIn: '/login',
},
},
);
// 보호할 경로 목록 — /login, /api/*, /docs, /_next 는 제외
export const config = {
matcher: ['/((?!login|api|docs|_next/static|_next/image|favicon.ico).*)'],
};
config.matcher로 proxy.ts가 실행될 경로를 지정하고, next-auth의 withAuth를 사용해 토큰 존재 여부를 확인합니다. 토큰이 없으면 자동으로 /login으로 리다이렉트되고, 토큰은 있지만 추가 정보 입력(isComplete)이 완료되지 않은 경우에는 /signup으로 보냅니다.
각 페이지 컴포넌트마다 로그인 체크 로직을 반복해서 작성하는 대신, 미리 더 나은 방법을 찾아보고 싶었습니다. proxy.ts를 도입한 덕분에 처음부터 로그인 체크 로직을 한 곳에서 관리할 수 있게 됐고, 보호할 경로를 추가하거나 변경할 때도 proxy.ts만 수정하면 됩니다.