[Next.js] middleware를 활용하여 페이지 리다이렉션 적용하기

문지은·2023년 11월 23일
0

Next.js - Page Router

목록 보기
11/11
post-thumbnail

Next.js Middleware

  • 미들웨어를 사용하면 요청이 완료되기 전에 코드를 실행할 수 있으며, 요청에 따라 응답을 수정할 수 있다.
  • 들어오는 요청에 따라 응답을 수정하거나, 요청 또는 응답 헤더를 수정하거나, 직접 응답을 하거나 리다이렉션 할 수 있다.
  • 미들웨어를 정의할 때는 프로젝트 루트 경로에 middleware.ts(또는 .js) 파일을 생성하여 사용한다.
    • pagesapp과 같은 레벨에 두거나 필요한 경우 src 안에 생성

middlware 작성 예시

middleware.ts

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

// 이 함수는 내부에서 'await'를 사용하는 경우 'async'로 표시될 수 있음.
export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}

// 미들웨어를 호출할 라우트 - 아래 Matching Paths 참조 
export const config = {
  matcher: '/about/:path*',
}

Matchig Paths

  • 미들웨어는 프로젝트의 모든 라우트에서 호출되며, 실행 순서는 다음과 같다.
  1. headers from next.config.js
  2. redirects from next.config.js
  3. Middleware (rewritesredirects, etc.)
  4. beforeFiles (rewrites) from next.config.js
  5. Filesystem routes (public/_next/static/pages/app/, etc.)
  6. afterFiles (rewrites) from next.config.js
  7. Dynamic Routes (/blog/[slug])
  8. fallback (rewrites) from next.config.js

미들웨어가 실행될 경로를 정의하는 방법은 두 가지가 있다.

Matcher

  • Matcher를 사용하여 특정 경로에서만 미들웨어를 실행하도록 설정할 수 있다.
export const config = {
  matcher: '/about/:path*',
}
  • 단일 경로 또는 배열 구분을 사용해서 여러 경로와 일치시킬 수도 있다.
export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}
  • Matcher 설정은 완전한 정규식을 지원하므로 부정적 전방 탐색(negative lookahead)이나 문자 일치(character matching)와 같은 매칭이 가능하다.
    • 특정 경로를 제외한 모든 경로와 일치시키는 부정 전방 탐색의 예는 다음과 같다.
export const config = {
  matcher: [
    /*
     * 다음과 같이 시작하는 경로를 제외한 모든 요청 경로와 일치:
     * - api (API 라우트)
     * - _next/static (정적 파일)
     * - _next/image (이미지 최적화 파일)
     * - favicon.ico (파비콘 파일)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}

참고 : Matcher 값은 반드시 정적으로 분석될 수 있어야 하므로 상수이어야 한다. 변수와 같은 동적 값은 무시된다.

Matcher 작성 방법

  • 경로는 반드시 /로 시작해야 한다.
  • 이름이 지정된 매개변수를 포함할 수 있다.
    • 예를 들어, /about/:path/about/a/about/b와 일치하지만 /about/a/c와는 일치하지 않는다.
  • 이름이 지정된 매개변수에는 수정자(modifier)를 사용할 수 있다. 수정자는 콜론(:)으로 시작한다.
    • 예를 들어, /about/:path**이 0개 이상이기 때문에 /about/a/b/c와 일치한다.
    • ?는 0개 또는 1개, +는 1개 이상을 의미한다.
  • 괄호로 둘러싼 정규식을 사용할 수 있다.
    • 예를 들어, /about/(.*)/about/:path*와 동일하다.
  • 더 자세한 내용은 path-to-regexp 문서 참고

침고 : 이전 버전과의 호환성을 위해 Next.js는 항상 /public/public/index로 간주한다.

Conditional Statements

  • NextRequest의 프로퍼티를 이용해 라우트의 미들웨어 작동을 분기 처리 할 수 있다.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/about')) {
    return NextResponse.rewrite(new URL('/about-2', request.url))
  }
 
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.rewrite(new URL('/dashboard/user', request.url))
  }
}

Next Response

  • Next Response를 사용하면 다음과 같은 작업을 수행할 수 있다.
    • 들어오는 요청을 다른 URL로 Redirection할 수 있다.
    • 지정된 URL을 표시하여 응답을 재작성할 수 있다.
    • API 라우트, getServerSideProps 및 Redirection 대상을 위해 요청 헤더를 설정할 수 있다.
    • 응답 쿠키와 응답 해더를 설정할 수 있다.
  • 미들웨어에서 응답을 생성하기 위해 다음과 같은 작업을 수행할 수 있다.
    • 응답을 생성하는 라우트(Page 또는 Edge Page API Route)로 Rewrite
    • 직접 NextResponse를 반환한다.

Using Cookies

  • 쿠키는 일반적인 헤더이다. 요청에서는 쿠키가 Cookie 헤더에 저장되고, 응답에서는 Set-Cookie 헤더에 저장된다.
  • Next.js 는 NextRequestNextResponse의 cookies 확장을 통해 쿠키에 간편하게 접근하고 조작할 수 있는 편리한 방법을 제공한다.
  • 들어오는 요청에 대해, 쿠키는 get, getAll, set, delete 쿠키와 같은 메서드를 제공한다.
    • has를 사용하여 쿠키의 존재를 확인하거나 clear를 사용하여 모든 쿠키를 제거할 수 있다.
  • 나가는 응답에 대해서는, 쿠키는 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=/test` 헤더가 있을 것

  return response
}

Setting Headers

  • NextResponse API를 사용하여 요청 및 응답 헤더를 설정할 수 있다.
  • 큰 헤더를 설정하면 백엔드 웹 서버 구성에 따라 431 Request Header Fields Too Large 오류가 발생할 수 있으므로 주의.
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // 요청 헤더를 복제하고 새 헤더 `x-hello-from-middleware1` 설정
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-hello-from-middleware1', 'hello')

  // NextResponse.rewrite에서도 요청 헤더를 설정할 수 있음
  const response = NextResponse.next({
    request: {
      // 새 요청 헤더
      headers: requestHeaders,
    },
  })

  // 새 응답 헤더 `x-hello-from-middleware2` 설정
  response.headers.set('x-hello-from-middleware2', 'hello')
  return response
}

Producing a Response

  • 미들웨어에서 직접 Response 또는 NextResponse 인스턴스를 반환하여 응답할 수 있다.
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 }
    )
  }
}

Advanced Middleware Flags

skipTrailingSlashRedirect

  • Next.js의 기본 리디렉션을 비활성화하여 미들웨어 내에서 맞춤 처리를 할 수 있도록 해준다.
  • 이를 통해 일부 경로에 대해 후행 슬래시를 유지하고 다른 경로에 대해서는 유지하지 않음으로써 migration을 용이하게 할 수 있다.

next.config.js

module.exports = {
  skipTrailingSlashRedirect: true,
}

middleware.js

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

  • Next.js가 직접 방문과 클라이언트 전환을 동일하게 처리하기 위해 URL을 정규화하는 것을 비활성화한다.
  • 원본 URL을 사용하여 완전한 제어가 필요한 일부 사례에서 이 기능을 활용할 수 있다.

next.config.js

module.exports = {
  skipMiddlewareUrlNormalize: true,
}

middleware.js

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

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

  console.log(pathname)
  // skipMiddlewareUrlNormalize가 true일때 /_next/data/build-id/hello.json
  // skipMiddlewareUrlNormalize가 false일때 /hello
}

페이지 접근 권한 설정하기

  • 로그인 여부나 사용자의 권한에 따라 접근 제한을 설정해야하는 페이지가 있다.
    • 로그인 페이지는 비로그인 사용자만 접근 가능
    • 마이페이지는 로그인 사용자만 접근 가능 등..
  • React 에서는 중첩 라우팅을 사용하여 페이지 접근 권한을 설정했었는데, Next 에서 그렇게 할 경우, 화면 노출이 잠깐 발생했다가, 페이지가 리다이렉션되는 현상이 발생하였다.
    • 미들 웨어를 사용하면 접근 제어를 서버에서 미리 처리할 수 있으므로 문제를 해결할 수 있다.

middleware.ts 파일 작성하기

  • 로그인 여부에 따른 페이지 접근 권한을 설정해보자.
  • pages 폴더와 동일한 수준의 위치에 middleware.ts 파일을 생성한다.

  • 인증 확인 로직 작성하기
    • 쿠키에 토큰 존재 여부에 따라 로그인 여부를 판단하도록 구현하였다.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
  const accessToken = request.cookies.get('accessToken')?.value;
  const { pathname } = request.nextUrl;

  // 로그인 페이지 접근 시, 이미 로그인 되어 있다면 메인 페이지로 리다이렉트
  if (pathname === '/login') {
    if (accessToken) {
      return NextResponse.redirect(new URL('/', request.url));
    }
    // 로그인 페이지는 보호되지 않은 경로이므로, accessToken이 없어도 접근 가능
    return NextResponse.next();
  }

  if (!accessToken) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

// See "Matching Paths" below to learn more
export const config = {
  matcher: [
    '/login',
    '/',
		// 인증이 필요한 경로 전체 작성
  ],
};

References

profile
코드로 꿈을 펼치는 개발자의 이야기, 노력과 열정이 가득한 곳 🌈

0개의 댓글