icon

안동민 개발노트

11장 : API 라우트

미들웨어 사용


이전 절에서는 Next.js API 라우트 기본 동작과 GET/POST/PUT/DELETE 메서드 처리 방식을 살펴봤습니다.

이제는 API 라우트를 확장하고, 요청 처리를 중앙에서 관리하며 보안을 강화하는 핵심 도구인 미들웨어(Middleware)를 다루겠습니다.

App Router에서는 API 라우트 미들웨어를 별도 파일(src/middleware.ts)로 정의합니다. 이 미들웨어는 요청을 핸들러에 넘기기 전에 가로채 공통 작업을 수행할 수 있게 해줍니다.

개념적으로는 Express.js나 Koa.js의 미들웨어와 유사합니다.


API 라우트에서 미들웨어의 역할

미들웨어는 API 라우트가 실제 비즈니스 로직을 실행하기 전에 다양한 선행 작업을 수행할 수 있도록 합니다. 주요 역할은 다음과 같습니다.

  • 인증 및 권한 부여: 로그인된 사용자만 특정 API에 접근하도록 하거나, 특정 역할을 가진 사용자만 접근하도록 제어합니다. (가장 일반적인 사용 사례)
  • 로깅: 모든 API 요청에 대한 로그를 기록하여 디버깅, 모니터링, 감사 추적에 활용합니다.
  • 요청 유효성 검사: 요청 헤더, 쿼리 파라미터, 본문 등의 유효성을 사전에 검증하여 잘못된 요청을 차단합니다.
  • CORS (Cross-Origin Resource Sharing) 처리: 다른 도메인에서의 API 요청을 허용하거나 거부하는 정책을 설정합니다.
  • 데이터 파싱 및 변환: 요청 본문을 특정 형식으로 파싱하거나, 데이터를 가공하는 등의 전처리 작업을 수행합니다.
  • 속도 제한 (Rate Limiting): 특정 IP 주소나 사용자로부터 오는 요청 수를 제한하여 서비스 남용을 방지합니다.
  • 헤더 조작: 응답 헤더를 추가, 수정, 삭제하여 캐싱 정책이나 보안 관련 설정을 적용합니다.

Next.js App Router에서의 미들웨어 설정

Next.js App Router의 미들웨어는 프로젝트의 루트에 있는 src/middleware.ts (또는 .js) 파일에 정의됩니다. 이 파일은 서버 사이드에서만 실행되며, 클라이언트 번들에 포함되지 않습니다.

기본 구조
src/middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // 1. 요청 처리 로직
  console.log('미들웨어: 요청이 들어왔습니다.', request.url);

  // 2. 응답 반환 또는 다음 요청 처리
  // NextResponse.next(): 요청을 다음 미들웨어 또는 해당 라우트 핸들러로 전달
  return NextResponse.next();

  // 특정 조건 시 리다이렉트
  // return NextResponse.redirect(new URL('/login', request.url));

  // 특정 조건 시 응답 반환 (라우트 핸들러로 전달되지 않음)
  // return new NextResponse('권한이 없습니다.', { status: 401 });
}

// 미들웨어가 적용될 라우트 경로를 정의합니다.
// matcher는 정규 표현식으로 여러 경로를 지정할 수 있습니다.
export const config = {
  matcher: [
    '/api/:path*', // /api/로 시작하는 모든 경로
    '/dashboard/:path*', // /dashboard로 시작하는 모든 경로
  ],
};
middleware 함수
  • request: NextRequest: 들어오는 HTTP 요청에 대한 정보를 담고 있는 객체입니다.
  • 반환 값
    • NextResponse.next(): 현재 요청을 다음 미들웨어 체인으로 전달하거나, 더 이상 미들웨어가 없으면 해당 라우트 핸들러(API 라우트 또는 페이지)로 전달합니다.
    • NextResponse.redirect(url): 요청을 다른 URL로 리다이렉트합니다.
    • NextResponse.rewrite(url): 클라이언트의 URL은 변경하지 않고 내부적으로 다른 경로의 콘텐츠를 렌더링합니다. (주로 인증/권한 없는 경우 커스텀 에러 페이지 렌더링 시 활용)
    • new NextResponse(body, init): 요청을 중단하고 직접 응답을 반환합니다 (예: 401 Unauthorized, 404 Not Found 등).
config 객체
  • matcher: 미들웨어가 실행될 경로를 지정합니다. 배열 안에 문자열 또는 정규 표현식을 사용하여 여러 경로 패턴을 정의할 수 있습니다.
    • /api/:path*: /api/로 시작하는 모든 경로를 포함합니다.
    • /users/:id: /users/123과 같이 특정 동적 경로를 포함합니다.
    • /((?!_next|static|public).*): 특정 경로(_next, static, public 등)를 제외한 모든 경로를 포함합니다.

미들웨어를 활용한 API 라우트 보호

가장 일반적인 미들웨어 사용 사례는 API 라우트에 대한 인증 및 권한 부여입니다. NextAuth.js의 withAuth 헬퍼 함수를 사용하면 이를 매우 쉽게 구현할 수 있습니다.

실습: NextAuth.js와 미들웨어를 사용하여 /api/admin 라우트 보호

NextAuth.js 설정 파일 (src/app/api/auth/[...nextauth]/route.ts)에 authOptions export: 미들웨어에서 authOptions를 사용하여 세션 정보를 가져와야 하므로, 이 파일을 다음과 같이 수정하여 authOptionsexport합니다.

src/app/api/auth/[...nextauth]/route.ts
// ... (기존 코드) ...

const authOptions = {
  // ... (기존 설정: providers, session, callbacks 등) ...
};

const handler = NextAuth(authOptions);

// GET 및 POST 핸들러와 함께 authOptions도 export
export { handler as GET, handler as POST, authOptions };
src/middleware.ts 파일 수정 (NextAuth.js withAuth 사용)
src/middleware.ts
// src/middleware.ts
import { withAuth } from 'next-auth/middleware';
import { NextResponse } from 'next/server';

// withAuth는 NextAuth.js 세션을 요청 객체에 추가하고,
// 인증되지 않은 사용자를 signIn 페이지로 리다이렉트하는 기능을 제공합니다.
export default withAuth(
  // 이 함수는 인증된 요청에 대해서만 실행됩니다.
  // 토큰(`req.nextauth.token`)에 사용자의 역할 정보가 포함되어 있습니다.
  async function middleware(req) {
    const token = req.nextauth.token; // 토큰에 접근
    const pathname = req.nextUrl.pathname;

    console.log('미들웨어 (인증 후):', pathname, '토큰:', token?.email, '역할:', token?.role);

    // 예시: /api/admin 경로에 대해 'admin' 역할만 허용
    if (pathname.startsWith('/api/admin')) {
      if (token?.role !== 'admin') {
        console.log('미들웨어: admin 권한 없음 -> 403 Forbidden');
        // 'Forbidden' 응답 (클라이언트에게 권한 없음을 명시)
        return new NextResponse(JSON.stringify({ message: '접근 권한이 없습니다.' }), {
          status: 403,
          headers: { 'Content-Type': 'application/json' },
        });
      }
    }

    // 예시: /api/editor 경로에 대해 'admin' 또는 'editor' 역할만 허용
    if (pathname.startsWith('/api/editor')) {
      if (token?.role !== 'admin' && token?.role !== 'editor') {
        console.log('미들웨어: editor 권한 없음 -> 403 Forbidden');
        return new NextResponse(JSON.stringify({ message: '접근 권한이 없습니다.' }), {
          status: 403,
          headers: { 'Content-Type': 'application/json' },
        });
      }
    }

    // 그 외의 경우, 요청을 계속 처리합니다.
    return NextResponse.next();
  },
  {
    // 인증되지 않은 사용자를 위한 설정
    pages: {
      signIn: '/api/auth/signin', // NextAuth.js가 제공하는 로그인 페이지로 리다이렉트
    },
    // `authorized` 콜백은 요청을 계속 진행할지 여부만 결정합니다.
    // 여기서는 토큰이 존재하면(즉, 인증되면) 요청을 진행하도록 설정합니다.
    // 실제 역할 검사는 위의 `middleware` 함수에서 수행됩니다.
    callbacks: {
      authorized: ({ token }) => !!token, // 토큰이 있으면 true (인증됨)
    },
  }
);

// 미들웨어가 적용될 경로를 정의합니다.
export const config = {
  matcher: [
    '/api/admin/:path*',  // '/api/admin'으로 시작하는 모든 API 경로
    '/api/editor/:path*', // '/api/editor'로 시작하는 모든 API 경로
    // 여기에 보호할 다른 API 라우트 경로를 추가합니다.
    // 미들웨어로 페이지 라우트도 보호할 수 있습니다 (예: '/dashboard/:path*')
  ],
};

보호된 API 라우트 생성 (예: src/app/api/admin/data/route.ts): 이 API 라우트는 미들웨어에 의해 보호되므로, 내부에서는 추가적인 인증/권한 검사 없이 바로 비즈니스 로직을 실행할 수 있습니다.

src/app/api/admin/data/route.ts
import { NextRequest, NextResponse } from 'next/server';

// 이 API는 미들웨어에 의해 'admin' 역할만 접근 가능하도록 보호됩니다.
export async function GET(request: NextRequest) {
  // 미들웨어를 통과했다면, 요청자는 'admin' 권한을 가진 것으로 간주합니다.
  const adminData = {
    message: '환영합니다, 관리자님! 중요한 관리자 데이터입니다.',
    usersCount: 100,
    pendingApprovals: 5,
  };

  return NextResponse.json(adminData, { status: 200 });
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  console.log('관리자 데이터 생성 요청:', body);
  return NextResponse.json({ message: '관리자 데이터 생성 성공', data: body }, { status: 201 });
}
보호된 API 라우트 생성 (예: src/app/api/editor/data/route.ts)
src/app/api/editor/data/route.ts
import { NextRequest, NextResponse } from 'next/server';

// 이 API는 미들웨어에 의해 'admin' 또는 'editor' 역할만 접근 가능하도록 보호됩니다.
export async function GET(request: NextRequest) {
  const editorData = {
    message: '환영합니다, 편집자님! 편집 가능한 데이터입니다.',
    articlesCount: 50,
    drafts: 10,
  };

  return NextResponse.json(editorData, { status: 200 });
}
실습 확인

NextAuth.js 설정이 완료되어 있고, src/app/api/auth/[...nextauth]/route.ts에서 authOptions를 export했는지 먼저 확인합니다.

또한 callbacks에서 사용자 역할(예: admin@example.comadmin, editor@example.comeditor, 나머지는 user)을 세션에 추가하도록 설정했는지 함께 점검합니다.

src/middleware.ts 파일을 위 예시와 같이 수정합니다.

src/app/api/admin/data/route.tssrc/app/api/editor/data/route.ts 파일을 생성합니다.

개발 서버(npm run dev)를 실행합니다.

테스트 시나리오
  • 로그아웃 상태
    • http://localhost:3000/api/admin/data 또는 http://localhost:3000/api/editor/data 에 접근하면, NextAuth.js 로그인 페이지로 리다이렉트되는지 확인합니다.
  • 일반 사용자 (user 역할)로 로그인 (예: GitHub 로그인)
    • http://localhost:3000/api/admin/data 에 접근하면 403 Forbidden 응답 (접근 권한이 없습니다.)을 받는지 확인합니다.
    • http://localhost:3000/api/editor/data 에 접근하면 403 Forbidden 응답을 받는지 확인합니다.
  • 편집자 (editor 역할)로 로그인 (예: editor@example.com으로 설정한 계정)
    • http://localhost:3000/api/editor/data 에 접근하여 데이터를 성공적으로 받는지 확인합니다.
    • http://localhost:3000/api/admin/data 에 접근하면 403 Forbidden 응답을 받는지 확인합니다.
  • 관리자 (admin 역할)로 로그인 (예: admin@example.com으로 설정한 계정)
    • http://localhost:3000/api/admin/data 에 접근하여 데이터를 성공적으로 받는지 확인합니다.
    • http://localhost:3000/api/editor/data 에 접근하여 데이터를 성공적으로 받는지 확인합니다.

미들웨어 사용 시 고려사항 및 팁

  • 성능: 미들웨어는 모든 요청에 대해 실행되므로, 미들웨어 내에서 시간이 많이 소요되는 작업(예: 복잡한 데이터베이스 쿼리, 외부 API 호출)은 최소화해야 합니다. 꼭 필요한 경우에만 실행되도록 matcher를 잘 구성하는 것이 중요합니다.
  • 오류 처리: 미들웨어에서 오류가 발생하면 사용자에게 적절한 HTTP 상태 코드와 메시지를 반환해야 합니다.
  • 토큰 정보 접근: req.nextauth.token은 NextAuth.js의 JWT 세션 전략을 사용할 때만 유효합니다. 데이터베이스 세션 전략을 사용하는 경우, 미들웨어에서 직접 세션 정보를 가져와야 하거나, authorized 콜백을 통해 더 복잡한 로직을 구현해야 할 수 있습니다.
  • 미들웨어 체이닝: Next.js App Router의 미들웨어는 단일 함수로 정의되므로, 여러 개의 미들웨어 함수를 순서대로 실행하고 싶다면, 해당 함수들을 하나의 middleware.ts 파일 내에서 조합해야 합니다. (예: compose 함수를 만들어 미들웨어들을 엮는 방식).
  • 라우트 핸들러 내에서의 추가 검증: 미들웨어에서 대부분의 인증/권한 검사를 수행하더라도, 중요한 API 라우트에서는 혹시 모를 상황에 대비하여 라우트 핸들러 내부에서 getServerSession을 통해 한 번 더 검증하는 것이 안전합니다.
  • NextResponse.rewrite vs NextResponse.redirect
    • rewrite: 클라이언트의 URL은 그대로 유지한 채 내부적으로 다른 경로의 콘텐츠를 렌더링합니다. 주로 특정 조건에서 커스텀 오류 페이지를 보여주고 싶을 때 유용합니다.
    • redirect: 클라이언트의 URL을 실제로 변경하여 다른 페이지로 이동시킵니다. 로그인되지 않았을 때 로그인 페이지로 보내는 등의 경우에 사용됩니다.

Next.js API 라우트와 미들웨어의 조합은 애플리케이션의 백엔드 로직을 안전하고 효율적으로 관리할 수 있는 강력한 도구를 제공합니다. 이를 통해 복잡한 인증 및 권한 요구사항을 유연하게 처리하고, 코드의 응집성을 높일 수 있습니다.

목차