Skip to content

Latest commit

 

History

History
468 lines (347 loc) · 13.6 KB

미들웨어.md

File metadata and controls

468 lines (347 loc) · 13.6 KB

미들웨어

  • 미들웨어는 요청이 완료되기전에 코드가 실행된다
  • 수신된 요청을 기반으로 응답 수정/생성, 리다이렉트 등이 가능함
  • 미들웨어는 캐싱된 내용과 경로가 일치하기 전에 실행된다

유즈케이스

  • 미들웨어를 사용하면 성능, 보안 및 UX 등이 크게 향상됨
  • 아래는 미들웨어가 특히 효과적인 시나리오들임

인증 및 인가

  • 유저가 특정 API 또는 페이지에 접근하기 전에 세션 쿠리를 활용해서 검증이 가능함

서버 사이트 리다이렉트

  • locale, role 등 조건부로 리다이렉트 처리가 가능함

경로 재설정

  • request를 기반으로 API 경로 또는 페이지에 대한 경로를 동적으로 바꿀수ㅜ있음
  • A/B 테스트, 기능 롤아웃, 레거시 경로를 지원할 수 있게됨

봇 탐지

  • 봇 요청을 탐지하고 해당 봇에 대해서 막을 수 있음

로깅과 분석

  • page 또는 API에 접근하기전에 request data에 대한 분석이 가능함

기능 플래그

  • 원활한 기능 롤아웃 또는 테스트를 위해서 기능을 동적으로 활성화/비활성화가 가능함

하지만 미들웨어가 항상 최적의 접근방법이 아니라는것을 인식하는건 중요함
아래는 미들웨어를 피해야하는 시나리오임

복잡한 데이터 로딩 및 조작

  • 미들웨어는 이런 데이터를 로딩하거나 조작하기 위해 설계된것이 아님
  • 대신 라우트 핸들러나 서버측 유틸리티 내에서 수행해야됨

무거운 계산 작업

  • 미들웨어는 가볍고 빠르게 작동해야함. 안그러면 페이지 로딩에 지연이 생길수도있음
  • 무거운 계산 작업이나 장시간 진행되는 프로세스는 전용 라우트 핸들러에서 작업해야함

광범위한 세션 관리

  • 미들웨어는 기본적인 세션 작업은 관리할 수 있음
  • 하지만 광범위한 세션 관리는 전용 인증서비스나 라우트 핸들러에서 관리해야함

데이터베이스 직접 접근

  • 미들웨어에서 DB에 직접 접근하는것은 권장되지 않음
  • 마찬가지로 전용 라우트 핸들러나 서버 측 유틸리티 내에서 수행되야함

컨벤션

  • 프로젝트 루트에 middware.ts를 정의하여 미들웨어 사용이 가능함
  • 미들웨어는 하나의 프로젝트에서 1개만 사용이 가능함
  • 미들웨어가 복잡한 경우 기능별로 모듈화하여 불러온 뒤 사용하는게 좋음
  • 단일 미들웨러를 통해서 충돌을 방지하고 여러 미들웨어 계층을 통해서 성능을 최적화함

예시

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// 비동기처리가 필요한 경우 async/await 키워드 사용도 가능함
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL("/home", request.url));
}

// /about/:path*에 대해서만 미들웨어가 실행된다
export const config = {
  matcher: "/about/:path*",
};

Path 설정

  • 기본적으로 모든 라우터에 대해서 미들웨어가 실행됨
  • 아래 설정을 통해서 특정 경로를 정확하게 타겟팅하거나 제외하기 위해서 matcher를 사용하는것이 중요함
  • 매칭 순서
    1. next.config.mjs에서 적용된 헤더
    2. next.config.mjs에서 적용된 리다이렉트
    3. 미들웨어가 실행됨. 추가적인 rewrites, redirects 등이 처리됨
    4. 파일 시스템 기반 라우트 체크전에 적용되는 rewrites 규칙
    5. 파일 시스템 기반 라우트가 체크됨
    6. 파일 시스템 기반 라우트가 체크 후 적용됨
    7. /blog/[slug] 같은 동적 라우트가 처리됨
    8. 마지막으로 적용되는 규칙으로 이전 단게에서 매칭되지 않는 요청이 처리됨

Matcher

  • 특정 경로에서만 미들웨어가 실행되게 할 수 있음
// src/middleware.ts
export const config = {
  matcher: "/about/:path*",
};
// src/middleware.ts
export const config = {
  matcher: ["/about/:path*", "/dashboard/:path*"],
};

matcher의 경우 정규표현식을 지원한다.

export const config = {
  matcher: [
    /*
     * 아래 요소중에 하나라도 포함되면 미들웨어가 실행된다
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    "/((?!api|_next/static|_next/image|favicon.ico).*)",
  ],
};

또한 missing 또는 has 배열로 데이터를 추가해서 바이패스가 가능하다
그리고 아래 들어가는 값들은 동적이 아닌 빌드타임에 정적으로 값을 가지고있어야 한다

export const config = {
  matcher: [
    /*
     * 아래 요소중에 하나라도 포함되면 미들웨어가 실행된다
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    {
      source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
      missing: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },

    {
      source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
      has: [
        { type: "header", key: "next-router-prefetch" },
        { type: "header", key: "purpose", value: "prefetch" },
      ],
    },

    {
      source: "/((?!api|_next/static|_next/image|favicon.ico).*)",
      has: [{ type: "header", key: "x-present" }],
      missing: [{ type: "header", key: "x-missing", value: "prefetch" }],
    },
  ],
};

Matcher 설정하기

  • 항상 /으로 시작해야 한다
  • 매개변수를 포함할 수 있음.
    • /about/:path -> /about/a, /about/b
    • 하지만 /about/a/c 는 불가능함
  • 와일드 마스크를 통한 표현도 가능함
    • /about/:path* -> /about/a/b/c 와 일치함
  • 괄호 내부에 정규표현식 사용도 가능함
    • /about/(.*) -> /about/:path` 와 동일함

조건 기반 제어

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // /about → /about-2 redirect
  if (request.nextUrl.pathname.startsWith("/about")) {
    return NextResponse.rewrite(new URL("/about-2", request.url));
  }

  // /dashboard → /dashboard/user redirect
  if (request.nextUrl.pathname.startsWith("/dashboard")) {
    return NextResponse.rewrite(new URL("/dashboard/user", request.url));
  }
}

NextResponse

  • redirect 를 활용한 다른 URL로 리다이렉트
  • rewrites 를 활용한 지정 URL의 응답을 재구성
  • API Route, getServerSideProps, rewrite 를 향한 request header 설정
  • 응답 쿠키, 헤더 설정

쿠키 사용하기

Request

  • get, getAll, set, delete 를 사용해서 쿠키 제어 가능
  • has 를 사용해서 쿠키 존재여부 확인 가능
  • delete 를 사용해서 모든 쿠키 삭제 가능

Response

  • get, getAll, set, delete 를 사용해서 쿠키 제어 가능
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // 요청에 대해 Cookie:nextjs=fast가 설정되어 있다고 가정
  // RequestCookies API를 통해서 쿠키를 가져올 수 있음
  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

  // 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: '/' }

  // 응답 헤더에는 Set-Cookie:vercel=fast;path=/ 쿠키가 설정되어있음
  return response;
}

헤더 설정하기

  • NextResponse 를 통해서 헤더 세팅이 가능하다
  • Next.js v13.0.0 이후로 해당 기능을 사용할 수 있다
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  // 기존 request.headers를 복사하고 새로운 x-hello-from-middleware1 헤더를 설정
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set("x-hello-from-middleware1", "hello");

  // NextResponse.rewrite를 통해서도 request header를 설정할 수 있음
  const response = NextResponse.next({
    request: {
      // New request headers
      headers: requestHeaders,
    },
  });

  // 새로운 response header 설정
  response.headers.set("x-hello-from-middleware2", "hello");
  return response;
}

CORS - Cross Origin Resource Shared

  • 미들웨어에서 CORS와 관련된 헤더도 설정이 가능함
import { NextRequest, NextResponse } from "next/server";

const allowedOrigins = ["https://acme.com", "https://my-app.org"];

const corsOptions = {
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

export function middleware(request: NextRequest) {
  // 기존 헤더에서 origin 가져오기
  const origin = request.headers.get("origin") ?? "";
  const isAllowedOrigin = allowedOrigins.includes(origin);

  // preflight 요청 제어
  const isPreflight = request.method === "OPTIONS";

  if (isPreflight) {
    const preflightHeaders = {
      ...(isAllowedOrigin && { "Access-Control-Allow-Origin": origin }),
      ...corsOptions,
    };
    return NextResponse.json({}, { headers: preflightHeaders });
  }

  // request 제어
  const response = NextResponse.next();

  if (isAllowedOrigin) {
    response.headers.set("Access-Control-Allow-Origin", origin);
  }

  Object.entries(corsOptions).forEach(([key, value]) => {
    response.headers.set(key, value);
  });

  return response;
}

export const config = {
  matcher: "/api/:path*",
};

응답 생성하기

  • Response 또는 NextReponse 를 통해서 미들웨어에서 응답을 반환할 수 있다
  • Next.js v13.1.0 이후 버전부터 동작한다
import { NextRequest } from "next/server";
import { isAuthenticated } from "@lib/auth";

// /api/*로 시작하는 경로만 미들웨어가 동작한다
export const config = {
  matcher: "/api/:function*",
};

export function middleware(request: NextRequest) {
  // 요청에 대한 인증여부를 체크하는 함수를 통해 불린값을 받게된다
  if (!isAuthenticated(request)) {
    // 에러메세지를 포함한 json 데이터를 응답한다
    return Response.json({ success: false, message: "authentication failed" }, { status: 401 });
  }
}

waitUntilNextFetchEvent

  • NextFetchEvent 는 웹 기본 API인 FetchEvent 를 상속한다
  • 그리고 waitUntil 메소드를 포함하고있다
  • waitUntil 메서드는 Promise 를 인자로 받아서 작업이 완료될 때 까지 지속되게 해준다.
  • 해당 메서드는 백그라운드에서 작업을 할때 유용하다
import { NextResponse } from "next/server";
import type { NextFetchEvent, NextRequest } from "next/server";

export function middleware(req: NextRequest, event: NextFetchEvent) {
  event.waitUntil(
    fetch("https://my-analytics-platform.com", {
      method: "POST",
      body: JSON.stringify({ pathname: req.nextUrl.pathname }),
    })
  );

  return NextResponse.next();
}

추가 미들웨어 플래그들

  • v13.1에서 2개의 추가 플래그가 미들웨어에 포함됬다

skipTrailingSlashRedirect

  • 트레일링 슬래쉬 처리를 커스터마이징 할때 사용한다
  • 특정 경로는 트레일링 슬래시를 사용하지만 다른곳은 사용하지 않을때 등이 있다
  • 대규모 사이트를 점진적으로 업데이트 하는 등 다양한 URL 요구사항을 가진 복잡한 사이트를 관리할때 유용하다고한다
// next.config.mjs
module.exports = {
  skipTrailingSlashRedirect: true,
};
// middleware.ts
const legacyPrefixes = ["/docs", "/blog"];

export default async function middleware(req) {
  const { pathname } = req.nextUrl;

  if (legacyPrefixes.some((prefix) => pathname.startsWith(prefix))) {
    return NextResponse.next();
  }

  // 트레일링 슬래쉬 활성화
  if (!pathname.endsWith("/") && !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)) {
    req.nextUrl.pathname += "/";
    return NextResponse.redirect(req.nextUrl);
  }
}

skipMiddlewareUrlNormalize

  • URL을 정규화 할때 사용한다
  • 에를 들어 /_next/data/build-id/hello.json 같은 내부 URL을 /hello 로 정규화 한다
  • 비활성화를 하게되면 정규화가 안된 원본 URL을 받아올 수 있다
// next.config.mjs
module.exports = {
  skipMiddlewareUrlNormalize: true,
};
export default async function middleware({ req }: { req: NextRequest }) {
  const { pathname } = req.nextUrl;

  // GET /_next/data/build-id/hello.json

  console.log(pathname);
  // true로 설정하면 pathname이 /_next/data/build-id/hello.json가 된다
  // 기본값인 경우 /hello가 된다
}

런타임

  • 미들웨어는 오직 Edge 런타임만 지원한다
  • Node.js 런타임은 지원하지 않음