[Next.js] middleware를 사용한 인가처리

jiny·2025년 11월 22일

기술 면접

목록 보기
75/78

🗣️ Next.js에서 middleware를 사용하여 인가 처리를 하는 방법에 대해서 설명해주세요.

  • 의도: 지원자가 Next.js에서 middleware를 사용하여 인가 처리를 구현할 수 있는지 평가
    • middleware의 개념과 역할을 설명한다.
    • middleware를 사용하여 인가 처리를 구현하는 방법을 설명한다.
    • middleware를 사용하는 예를 떠올려 본다.
  • 주어진 답안 (모범 답안)

    Next.js에서 middleware요청(Request)과 응답(Response) 사이에 실행되는 코드로, 요청을 가로채어 특정 작업을 수행할 수 있습니다.
    이를 활용하면 인증(Authentication) 및 권한 부여(Authorization), 로깅(Logging), 리다이렉트 등의 기능을 효율적으로 구현할 수 있습니다.

    특히, 인가 처리에 활용하면 특정 페이지에 대한 접근 권한을 제어할 수 있습니다.
    예를 들어, 로그인된 사용자만 접근할 수 있도록 설정하거나, 특정 역할(Role)을 가진 사용자에게만 특정 페이지를 허용하는 방식으로 사용할 수 있습니다.

    Next.js에서는 프로젝트의 middleware 파일을 통해 이러한 로직을 구현할 수 있습니다.
    이 미들웨어는 요청이 들어올 때 실행되며, 사용자의 인증 상태를 확인하여 허가되지 않은 경우 로그인 페이지로 리다이렉트하거나, 접근을 제한하는 역할을 합니다.
    또한, 특정 경로에 대해서만 실행되도록 설정할 수도 있어, 보호가 필요한 페이지에만 적용할 수 있습니다.

    결론적으로, Next.js의 middleware를 사용하면 페이지마다 별도로 인증 로직을 구현할 필요 없이 일관된 방식으로 인가 처리를 적용할 수 있으며, 보안성을 강화하는 데 도움이 됩니다.


📝 개념 정리

🌟 Middleware란?

  • 개념: 요청(Request)이 라우트로 전달되기 전에 실행되는 서버 사이드 코드
  • 이 단계에서 할 수 있는 대표적인 작업
    • 특정 URL로 redirect
    • 다른 경로로 rewrite (URL은 그대로, 내부적으로 다른 경로로 처리)
    • 헤더, 쿠키 수정
    • 바로 응답(Response)을 만들어 반환해서 라우트까지 가지 않게 막기
  • App Router에서의 위치
    • 프로젝트 루트(app 또는 pages와 같은 레벨, 혹은 src 아래 루트)에 middleware.js 또는 middleware.ts 파일을 만들면 된다.
    • Next.js 15에서는 장기적으로 proxy 파일 컨벤션으로 옮겨가는 중이며, 기존 middleware는 deprecated 예정이다.

🌟 동작 시점과 실행 흐름

  • 동작 시점
    • 라우트가 매칭되기 전에, 캐시된 내용이 서빙되기 전에 실행된다.
    • 즉, 정적 페이지/데이터 캐시가 있더라도 먼저 Middleware가 요청을 검사하고, 그 후에
      • 허용 → 캐시 or 라우트로 전달
      • 차단/리다이렉트 → 여기서 응답 확정
  • 실행 흐름
    1. 클라이언트가 GET /dashboard 요청
    2. Next.js가 이 요청을 받으면 먼저 middleware를 호출
    3. middleware(request) 안에서
      • 세션 쿠키를 확인해 로그인 여부 확인
      • 특정 국가/도메인에서 온 요청인지 체크
      • A/B 테스트용 분기 결정 등
    4. 그 결과에 따라
      • 그대로 진행: NextResponse.next() 반환
      • 다른 URL로 redirect: NextResponse.redirect()
      • 다른 내부 경로로 rewrite: NextResponse.rewrite()
      • 직접 응답: new NextResponse("Blocked", { status: 403 })

🌟 기본 사용법

  • 최상단 예시

    // middleware.ts
    import { NextResponse, NextRequest } from 'next/server';
    
    export function middleware(request: NextRequest) {
      const isLoggedIn = Boolean(request.cookies.get('auth_token')?.value);
                   
      // 로그인 안 된 사용자가 /dashboard 쪽으로 접근할 경우 로그인 페이지로 리다이렉트
      if (!isLoggedIn && request.nextUrl.pathname.startsWith('/dashboard')) {
        const loginUrl = new URL('/login', request.url);
        return NextResponse.redirect(loginUrl);
      }
      
      return NextResponse.next();
    }
    • NextRequest: URL, 헤더, 쿠키 등 요청 정보에 접근할 수 있는 객체
    • NextResponse: next(), redirect(), rewrite(), json(), 쿠키/헤더 설정 등을 제공
  • matcher로 특정 경로에만 적용

    export const config = {
      matcher: ['/dashboard/:path*', '/settings/:path*'],
    };
    • config.matcher를 사용하면, 해당 패턴에 맞는 경로에만 Middleware가 실행된다.
    • 자주 쓰는 패턴
      • '/((?!_next|static|.*\\..*).*)'처럼 정적 파일이나 _next 등을 제외하고 전부 적용
      • ['/dashboard/:path*', '/profile/:path*']처럼 보호가 필요한 경로만 적용

🌟 NextRequest, NextResponse

NextRequest, NextResponse는 Proxy(전 Middleware) 함수 안에서 쓰는 요청·응답 객체이다.

1. 공통 개념 정리

  • NextRequest
    • 들어온 요청을 읽는 객체
    • URL, 쿼리스트링, 헤더, 쿠키, 메서드 같은 정보에 접근할 때 사용한다.
  • NextResponse
    • 돌려줄 응답을 만드는 객체
    • next(), redirect(), rewrite() 같은 헬퍼와, 헤더/쿠키 설정 기능을 제공한다.
  • 둘 다 next/server에서 가져온다.
    import { NextRequest, NextResponse } from "next/server";

2. NextRequest: 요청 읽기 전용 객체

  • 기본 프로퍼티
    export function middleware(req: NextRequest) {
      const url = req.nextUrl; // NextURL 객체
      const pathname = req.pathname; // "/dashboard"
      const searchParams = url.searchParams; // URLSearchParams
      
      const method = req.method; // "GET", "POST" 등
      const headers = req.headers; // Headers 객체
      const cookie = req.cookies.get("auth_token")?.value; // 쿠키 읽기
    }
    • nextUrl
      • 브라우저의 URL과 비슷하지만, Next.js 전용 속성(basePath, locale 등)을 가진 객체
      • pathname, origin, searchParams 등을 편하게 다룰 수 있다.
    • method
      • HTTP 메서드 (GET, POST 등)
    • headers
      • 표준 Headers 객체
      • get, set 같은 메서드는 있지만, Proxy에서는 주로 get만 사용한다.
    • cookies
      • req.cookies.get("name")로 읽을 수 있는 쿠키 컬렉션
  • URL 조작
    nextUrl을 복사(clone)해서 수정하는 패턴이 아주 자주 나온다.
    export function middleware(req: NextRequest) {
      const url = req.nextUrl.clone();
      
      if (url.pathname === "/") {
        url.pathname = "/home";
        return NextResponse.redirect(url); // 조작한 url로 redirect
      }
      
      return NextResponse.next();
    }
    • clone()을 쓰는 이유: 원본 nextUrl을 그대로 두고, 새로운 URL로 redirect/rewrite할 때 실수를 줄이기 위해서
  • 쿠키 읽기

    const token = req.cookies.get("auth_token")?.value;
    
    if (!token) {
      // 로그인 안 된 상태
    }
    • cookies.get(name){ name: string; value: string } | undefined
    • Proxy에서 쿠키는 읽기 전용이다. (수정은 NextResponse 쪽에서 함)
  • 본문(body) 관련
    • Proxy(전 Middleware)는 요청 본문을 읽는 용도로 설계된 건 아니다.
    • Next.js 공식 문서 기준으로, Proxy에서는 body를 읽는 건 제한/비권장되는 방향이고, 본문을 읽어서 로직을 짜야 한다면 Route Handler나 API Route에서 처리하는 게 정석이다.

3. NextResponse: 응답/조작용 객체

  • 기본 패턴 4가지
    1. 그냥 통과시키기: NextResponse.next()
      export function middleware(req: NextRequest) {
        // 아무 변경 없이 라우트/캐시로 넘김
        return NextResponse.next();
      }
    2. 다른 URL로 리다이렉트: NextResponse.redirect()
      export function middleware(req: NextRequest) {
        if (!req.cookies.get("auth_token")) {
          const loginUrl = new URL("/login", req.url);
          loginUrl.searchParams.set("from", req.nextUrl.pathname);
          return NextResponse.redirect(loginUrl);
        }
        return NextResponse.next();
      }
    3. 내부 라우트로 rewrite: NextResponse.rewrite()
      export function middleware(req: NextRequest) {
        const url = req.nextUrl.clone();
        
        if (url.pathname.startsWith("/blog")) {
          url.pathname = url.pathname.replace("/blog", "/news");
          return NextResponse.rewrite(url);
        }
        
        return NextResponse.next();
      }
    4. 직접 응답 만들기: new NextReponse(), NextResponse.json()
      export function middleware() {
        return new NextResponse("Forbidden", { status: 403 });
        // 또는
        // return NextResponse.json({ message: "Forbidden" }, { status: 403 });
      }
      • 이때는 라우트(handler/page)까지 가지 않고 Middleware에서 바로 응답이 끝난다.
  • 헤더/쿠키 수정
    export function middleware(req: NextRequest) {
      const res = NextResponse.next();
      
      // 헤더 추가
      res.headers.set("x-request-id", crypto.randomUUID());
      
      // 쿠키 설정
      res.cookies.set("experiment_group", "A", {
        path: "/",
        maxAge: 60 * 60 * 24, // 1일
        httpOnly: true,
      });
      
      return res;
    } 
    • res.headers
      • 표준 Headers 객체 (set, delete 등 가능)
    • res.cookies
      • set(name, value, options)으로 응답 쿠키 설정
      • delete(name)로 제거도 가능

🌟 자주 쓰는 기능

1. Redirect

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname === "/") {
    return NextResponse.redirect(new URL('/home', request.url));
  }
  return NextResponse.next();
}
  • //home 리다이렉트
  • 구 도메인/구 URL → 신 URL로 영구 이동
  • 언어/지역별 서브 경로로 분기 (/en, /ko 등)

2. Rewrite

export function middleware(request: NextRequest) {
  const url = request.nextUrl.clone();
  
  if (url.pathname.startsWith('/blog')) {
    url.pathname = url.pathname.replace('/blog', '/news');
    return NextResponse.rewrite(url);
  }
  
  return NextResponse.next();
}
  • URL 바는 그대로인데, 내부적으로 다른 라우트로 연결할 때 사용한다.
  • SEO 상 /blog/...는 유지하면서 실제 처리는 /news/...에서 하는 식

3. 헤더/쿠키 조작

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // 응답 헤더 추가
  response.headers.set('x-custom-header', 'my-value');
  
  // 응답 쿠키 설정
  response.cookies.set('experiment_group', 'A', {
    path: '/',
    maxAge: 60 * 60 * 24,
  });
  
  return response;
}
  • 보안 헤더(Content-Security-Policy, X-Frame-Options 등)를 통합 관리하거나,
  • 간단한 실험군/실험대상 쿠키(A/B 테스트) 설정 등에 자주 사용한다.

4. 국가/도메인 기반 분기

  • request.headers.get('x-forwarded-host'), request.nextUrl.hostname 등을 이용해 호스트에 따라 분기
  • Vercel Geo, Edge Runtime에서 제공하는 위치 정보(리전, 국가 코드)로 지역별 페이지/금지 페이지를 구성할 수 있다.

🌟 어떤 용도에 쓰면 좋은가?

1. 인증/인가 (Auth)

  • "로그인이 안 됐으면 /login으로 보내기" 같은 로직에 최적화되어 있다.
  • 장점
    • 특정 경로로 들어오는 모든 요청에 대해 일관된 가드를 적용할 수 있다.
    • 페이지나 Route Handler별로 일일이 auth 체크 코드를 넣지 않아도 된다.
  • 주의할 점
    • 복잡한 세션 로직(데이터베이스 조회 등)을 Edge Middleware에서 직접 하는 건 추천하지 않는다.
      • Edge는 파일 시스템 접근이 불가능하고, 일부 Node API 제한 등의 제약이 존재한다.
    • 보통은
      • 토큰 유효성 정도만 Middleware에서 검증하고,
      • 실제 세션 조회DB 접근Route Handler/페이지에서 처리하는 패턴이 많다.

2. 국제화(i18n)/언어 라우팅

  • Accept-Language 헤더, 쿠키 등을 읽어서
    • //ko 또는 /en 같은 언어 서브 경로로 리다이렉트
    • 또는 request.nextUrl.locale을 사용한 리다이렉트/리라이트

3. A/B 테스트, 실험군 라우팅

  • 최초 방문 시: 실험군을 쿠키에 랜덤으로 A/B 저장
  • 이후 요청: 쿠키를 읽고 /pricing/pricing-a 또는 /pricing-b로 rewrite/redirect

이렇게 하면 클라이언트 코드 수정 없이도 서버 라우팅 레벨에서 실험 분기를 만들 수 있다.

4. 공통 로깅/감사 로그

  • 요청 IP, 경로, User-Agent, 쿠키 정보를 모아서 로깅 서비스로 보내는 등, 모든 요청에 대해 공통적으로 수행해야 하는 일중앙에서 처리할 수 있다.

🌟 middleware를 사용한 인가 처리

미들웨어로 인가(authorization)를 한다는 건 결국 "요청이 라우트에 도달하기 전에, 이 사용자가 이 경로에 들어와도 되는지 미리 걸러낸다"라는 뜻이다.

1. 로그인 이후 요청 흐름

  • 로그인 성공 시
    • 백엔드가 세션 ID 또는 JWT쿠키에 저장해서 응답한다.
    • 토큰 안에 sub(user id), role(권한), 만료 시간 등을 넣어둔다.
  • 이후 모든 요청
    • middleware가 가장 먼저 실행된다.
    • 쿠키에서 토큰을 읽고 검증한다.
    • 요청 경로(/admin, /dashboard 등)와 유저 role을 비교해서
      • 접근 가능하면 → NextResponse.next()로 통과
      • 로그인이 필요하면 → /login으로 redirect
      • 권한이 부족하면 → /403 또는 메인으로 redirect
  • 라우트 (페이지/Route Handler)
    • 실제 데이터 로직은 여기서 진행한다.
    • 보안을 위해 라우트에서도 한 번 더 권한을 확인하는 것이 권장된다.

2. 인가 미들웨어 설계 패턴

  • 보호할 경로와 권한 정의
    // 예시: lib/access-control.ts
    export const PUBLIC_PATHS = ["/", "/login", "/signup", "/about"];
         
    export const ROLE_RULES = [
      { pattern: /^\/admin(\/|$)/, allowed: ["ADMIN"] },
      { pattern: /^\/dashboard(\/|$)/, allowed: ["USER", "ADMIN"] },
    ];
    • PUBLIC_PATHS: 누구나 접근 가능
    • ROLE_RULES: 정규식을 사용해서 /admin/**은 ADMIN만, /dashboard/**는 USER 이상 접근 가능
  • 유저 정보를 어디에 보관할까?
    1. JWT 쿠키
      • 장점: 백엔드와 프론트엔드가 분리되어 있어도 검증이 가능하다. (stateless)
      • 단점: 토큰 크기/만료/재발급 관리가 필요하다.
    2. 세션 ID 쿠키
      • 쿠키에는 세션 키만 넣고, 실제 유저/권한 정보는 서버에 저장한다.
      • 미들웨어에서 세션 스토어에 접근해 읽어오고 권한을 판단한다.

3. JWT + 미들웨어로 인가 구현

  1. JWT 검증 유틸 (Edge 환경 기준)

    // lib/auth-token.ts
    import { JWTPayload, jwtVerify } from "jose";
    
    const secret = new TextEncoder().encode(process.env.JWT_SECRET!); // 문자열을 Uint8Array(바이너리)로 변환
    
    export type UserTokenPayload = JWTPayload & {
      sub: string; // user id
      role: "USER" | "ADMIN";
    };
    
    // 토큰을 검증해서 payload를 돌려주고, 실패하면 null을 리턴
    export async function verifyAuthToken(
      token: string
    ): Promise<UserTokenPayload | null> {
      try {
        const { payload } = await jwtVerify(token, secret);
        return payload as UserTokenPayload;
      } catch {
        return null;
      }
    }
    • jose는 Edge 런타임에서도 잘 쓰이는 JWT 라이브러리이다.
      • JWTPayload
        • JWT 페이로드 타입 정의용 TypeScript 타입
        • JWT 안에 들어가는 iss, sub, aud, exp, iat 같은 표준 필드들을 포함하고 있는 타입
      • jwtVerify
        • jwtVerify(token, secret) 형식으로 사용
        • 토큰의 서명(Signature)을 검증하고, payload를 파싱해주는 함수
        • 토큰이 위조되었거나 만료되었으면 에러를 던짐
  1. 경로에 따른 role 매칭 유틸

    // lib/access-control.ts
    export const PUBLIC_PATHS = ["/", "/login", "/signup", "/forgot-password"];
    
    export const ROLE_RULES = [
      { pattern: /^\/admin(\/|$)/, allowed: ["ADMIN"] },
      { pattern: /^\/dashboard(\/|$)/, allowed: ["USER", "ADMIN"] },
    ];
    
    export function isPublicPath(pathname: string) {
      return PUBLIC_PATHS.some(
        (p) => pathname === p || pathname.startsWith(`${p}/`)
      );
    }
    
    export function getRequiredRoles(pathname: string): string[] | null {
      const rule = ROLE_RUELS.find((r) => r.pattern.test(pathname));
      return rule?.allowed ?? null;
    }
  1. middleware.ts에서 인가 처리

    // middleware.ts
    import type { NextRequest } from "next/server";
    import { NextResponse } from "next/server";
    import { verifyAuthToken } from "@/lib/auth-token";
    import { isPublicPath, getRequiredRoles } from "@/lib/access-control";
    
    export async function middleware(req: NextRequest) {
      const { pathname } = req.nextUrl;
      
      // 1) 공개 경로는 바로 통과
      if (isPublicPath(pathname)) {
        return NextResponse.next();
      }
      
      // 2) auth 토큰 조회
      const token = req.cookies.get("auth_token")?.value;
      
      // 토큰 자체가 없으면 비로그인 → 로그인 페이지로 리다이렉트
      if (!token) {
        const loginUrl = new URL("/login", req.url);
        loginUrl.searchParams.set("from", pathname); // 돌아올 위치
        return NextResponse.redirect(loginUrl);
      }
      
      // 3) 토큰 검증
      const user = await verifyAuthToken(token);
      
      // 토큰이 만료되었거나 위조되었으면 다시 로그인 유도
      if (!user) {
        const loginUrl = new URL("/login", req.url);
        return NextResponse.redirect(loginUrl);
      }
      
      // 4) 요청 경로에 필요한 role 확인
      const requiredRoles = getRequiredRoles(pathname);
      
      if (requiredRoles && !requiredRoles.includes(String(user.role))) {
        // 권한 없음 → 403 페이지 등으로
        const forbiddenUrl = new URL("/403", req.url);
        return NextResponse.redirect(forbiddenUrl);
      }
      
      // 5) 모든 체크 통과 → 라우트로 진행
      return NextResponse.next();
    }
    
    // 6) 어떤 경로에 미들웨어를 적용할지 설정
    export const config = {
      matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
    };

🌟 Middleware vs. Route Handler/API Route

구분MiddlewareRoute Handler(route.ts) / API Route
실행 시점라우트 매칭 이전특정 라우트로 매칭된 후
위치루트의 middleware.tsapp/**/route.ts, pages/api/**
주 목적필터링, 리다이렉트, 가드 ,헤더·쿠키 수정실제 비즈니스 로직, 데이터 CRUD
응답 생성가능하지만, 보통 간단 응답에 사용주로 여기서 실제 JSON/HTML 반환
적용 범위matcher로 지정한 여러 경로에 공통 적용각 라우트별로 개별 적용

복잡한 API는 Route Handler에게 맡기고, 그 전에 "이 요청이 들어와도 되는지?"를 판단하는 건 Middleware에서 처리하는 패턴이 가장 깔끔하다.


🌟 Next.js Middleware vs. axios 인터셉터

  1. 공통점/유사점
    • 둘 다 요청-응답 사이에 끼어드는 훅 역할을 한다.
    • 대표적으로 다음과 같은 작업을 할 때 사용한다.
      • 공통 로그 찍기
      • 공통 에러 처리
      • 인증/인가 체크 (토큰 확인, 401 처리 등)
    • 코드도 느낌이 비슷하다.
      • Next.js middleware
        export function middleware(req: NextRequest) {
          // 여기서 검사
          return NextResponse.next();
        }
      • axios interceptor
        axiosInstance.interceptors.request.use((config) => {
          // 여기서 검사
          return config;
        });
  1. 결정적 차이: 동작하는 층
    • Next.js Middleware(Proxy)
      • 네트워크 레벨, 서버 쪽에서 동작한다.
      • 브라우저 → (프록시/미들웨어) → Next.js 라우트/페이지
      • 사용자가 URL을 치거나 링크를 클릭해서 들어오는 페이지/정적 파일/Route Handler 요청 전체를 다룰 수 있다.
      • 할 수 있는 일
        • 요청을 어디로 보낼지 결정 (redirect, rewrite)
        • 응답 생성 (403, json 등)
        • 응답 헤더/쿠키 추가
      • 특징
        • 페이지 이동 자체를 막거나 보내버릴 수 있다.
        • 이미지, HTML, API 등 모든 리소스 요청 단위에서 동작한다. (matcher에 따라)
    • axios 인터셉터
      • 클라이언트 코드(또는 Node 백엔드 코드)에서 axios로 보내는 HTTP 요청만 가로챈다.
        • 컴포넌트 안에서 axios.get("/api/...") 할 때
        • 서버 코드에서 axios.post("https://...") 할 때
      • 할 수 있는 일
        • 요청 직전에 헤더 추가 (Authorization, trace-id 등)
        • 응답 직후에 공통 에러 처리, 토큰 갱신
      • 특징
        • axios로 보낸 요청만 대상으로 한다.
        • 페이지 이동, 이미지 로딩, <a> 클릭 같은 건 전혀 관여하지 못한다.
        • 네트워크 상의 진짜 요청 흐름을 바꾸지는 못하고, 내가 보내는 axios 요청의 설정/결과만 손대는 느낌이다.
  1. 인증/인가 예시로 비교해 보기
    • Next.js 미들웨어(프록시)에서 인가
      export async function middleware(req: NextRequest) {
        const token = req.cookies.get("auth_token")?.value;
        const { pathname } = req.nextUrl;
        
        // 로그인 필요 페이지
        if (!token && pathname.startsWith("/dashboard")) {
          return NextResponse.redirect(new URL("/login", req.url));
        }
        
        return NextResponse.next();
      }
      • 사용자가 /dashboard URL에 들어오는 모든 요청(페이지, 데이터, SSR 등) 앞에서 쿠키를 보고 아예 /login으로 보내버릴 수 있다.
      • 브라우저 주소창이 /login으로 바뀐다. ➡️ 완전한 라우팅 제어
    • axios 인터셉터에서 인증 헤더
      axiosInstance.interceptors.request.use((config) => {
        const token = getTokenFromStorage();
        if (token) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      });
      • 클라이언트 코드가 axios를 사용할 때만 동작한다.
      • 페이지 URL 접근 자체를 막을 수는 없다.
        • /dashboard 페이지에 그냥 들어오는 것은 막지 못하고,
        • 그 페이지에서 API 호출이 실패하는 것 정도만 제어할 수 있다.
  1. 요약 비교

    항목Next.js Middleware(Proxy)axios 인터셉터
    동작 위치서버/프록시 레이어 (요청-응답 경계)클라이언트(또는 Node) 코드 안
    대상브라우저가 보내는 모든 HTTP 요청 (matcher 범위 내)axios로 보낸 요청만
    주 용도라우팅 제어, 인가 가드, 리다이렉트, 헤더/쿠키 조작공통 헤더 삽입, 공통 에러 처리, 로깅
    URL 변경 가능?가능 (redirect/rewrite)불가능 (요청 config만 수정)
    페이지 접근 차단URL 단위로 직접 막을 수 있음직접 막진 못하고, API 실패로 간접 제어
    이미지/정적 파일도?matcher에 따라 포함될 수 있음아예 대상 아님
  1. 어떻게 같이 쓰면 좋을까?
    • 미들웨어/프록시
      • 이 URL은 로그인 안 한 사람은 절대 못 들어오게 제어하기
      • 특정 국가/도메인에서는 이 페이지를 다른 곳으로 보내기
        ➡️ URL 레벨 보호 + UX 레벨 흐름 제어
    • axios 인터셉터
      • 로그인 되어 있으면 모든 API 요청에 토큰 헤더 자동 첨부
      • 401 나오면 공통으로 로그아웃 처리 또는 토큰 재발급 시도
        ➡️ API 호출마다 반복되는 인증/에러 처리

🌟 성능/제약사항 및 주의점

  • 성능 관점
    • 모든 요청이 Middleware를 거치면 그만큼 추가 오버헤드가 있다.
    • 특히 matcher를 너무 광범위하게 잡으면 이미지, 정적 파일 요청들에 대해서도 Middleware 함수가 실행되어 불필요한 비용이 생긴다.
    • 따라서 꼭 필요한 경로만 matcher에 명시하고, 정적 자원 경로(/_next, /static, 이미지 경로 등)는 명시적으로 제외한다.
  • 실행 환경 제약
    • 기본적으로 Edge Runtime에서 동작하도록 설계되어 있어 Node.js 전용 API 사용이 제한된다.
      • 예: fs 모듈 직접 사용, 특정 Node 서버 라이브러리 사용 등
    • 환경에 따라 세부 제약이 달라질 수 있고, 일부는 앞으로도 계속 변경될 수 있기 때문에, Node 전용 라이브러리 의존 로직은 Middleware에 넣지 않는 것이 안전하다.
  • 캐시와의 관계
    • Middleware는 캐시 이전에 실행되므로 조건에 따라 다른 페이지로 보내는 퍼스널라이제이션과 캐시를 병행하기 좋다.
    • 다만, 쿠키/헤더 기반 조건부 캐시를 다룰 때는 캐시 키 설계(쿠키를 캐시 키에 포함할지 등)를 잘 고려해야 한다.

🌟 proxy

  • 현재는 다음과 같이 파일명만 바꾸고, 안에 있는 로직은 그대로 둬도 동작한다.
    • middleware.tsproxy.ts
    • export function middlewareexport function proxy
  • 다만 Next.js 팀이 이걸 단순 리네임이 아니라 개념을 바꾸는 신호로 쓰고 있어서, 앞으로는 전역 인가 로직을 다 여기 몰아넣는 용도로 쓰지 말라는 메시지가 같이 깔려 있다.

1. 코드/동작 관점: 지금은 거의 1:1 치환

  • 기존 코드: middleware.ts

    import { NextResponse } from "next/server";
    
    export function middleware() {
      return NextResponse.next();
    }
  • 변경된 코드: proxy.ts

    import { NextResponse } from "next/server";
    
    export function proxy() {
      return NextResponse.next();
    }
  • 바뀌는 건 딱 세 가지이다.
    1. 파일 이름: middleware.tsproxy.ts
    2. export 이름: middlewareproxy
    3. (next.config.js에 실험 옵션 쓰고 있었다면) middleware* 관련 옵션 이름 → proxy*로 변경
  • 그 외는 모두 그대로이다.
    • NextResponse, NextRequest 쓰는 방법
    • export const config = { matcher: ... } 구조
    • NextResponse.next / redirect / rewrite / json

➡️ 그래서 내가 지금 쓰던 인증/로그/리다이렉트 로직파일/함수 이름만 맞게 바꾸면 그대로 동작한다고 생각하면 된다.

2. 진짜 차이: 런타임 + 앞으로의 사용 의도

  • 런타임 제약 (Node 고정)
    • Proxy는 기본으로 Node.js runtime에서만 실행된다.
    • Proxy 파일 안에서는 export const config = { runtime: 'edge' } 같이 runtime 옵션을 쓸 수 없다.
    • 예전 middleware에서는 Edge runtime이 기본이고, 설정으로 Node를 선택하는 패턴도 있었는데, Proxy는 아예 runtime 선택을 막고 Node로 고정해 버렸다.
  • 역할에 대한 공식 입장 변화 (공식 문서 Migration 섹션)
    • "middleware"라는 이름 때문에 사람들이 Express 미들웨어처럼 모든 인증/비즈니스 로직을 한 곳에 몰아넣는 오해를 한다.
    • Middleware는 매우 강력해서 남용되기 쉬운데, 실제로는 마지막 수단(last resort) 정도로만 써야 한다.
    • 그래서 이름을 "proxy"로 바꾸면서 "이건 앱 앞단에 있는 네트워크 경계/프록시 레이어다"라는 의미를 강조하겠다.
  • 즉, Next.js 팀이 원하는 새 마인드는
    • Proxy = NGINX / CDN Edge 같은 '요청 앞단 라우팅·리다이렉트' 레이어
    • 인증/인가 = 각 라우트/데이터 레이어(API, Route Handler, Server Action 등)에서 다시 한 번 체크
    라는 식의 방어 심층(Defense in depth) 구조이다.

3. 기능은 같은데, 쓰는 방식은 이렇게 생각하면 편하다.

  1. 지금 프로젝트 마이그레이션
    • middleware.tsproxy.ts
    • export function middlewareexport function proxy
    • config.matcher 그대로
    • runtime 설정 있었다면 제거
  1. 기능 관점에서 차이
    • 리다이렉트, 리라이팅, 쿠키·헤더 수정, 응답 직접 반환 → 전부 그대로 가능
    • 실행 순서(redirects → proxy → filesystem routes ...)도 기존 middleware와 사실상 동일한 위치에 있다.
  1. 의도/베스트 프랙티스 관점에서 차이
    • 비추천 방식: 앱 전체 인가를 proxy 한 파일에서만 함
    • 추천 방식 (이중 체크 구조)
      • Proxy: 쿠키 보고 로그인/비로그인 라우팅, 간단한 필터링, 공통 리다이렉트 처리
      • 각 API/Route Handler: 실제 권한 체크(권한 없으면 401/403 응답)

0개의 댓글