Next.js의 Middleware 활용해보기

정유정 | yujeong choung·2023년 2월 25일
1

Next

목록 보기
2/3
post-thumbnail

Middleware를 만나게 되기까지

회사에서 제공하고 있는 서비스에서 사용자들이 로그인을 하지 않은채로 (혹은 토큰이 만료된 상태로) 접근 불가능한 페이지에 직접 url을 입력하고 접근하려고 하는 상황이 많이 존재했다.
그렇게 유효한 토큰없이 페이지에 접근 시도를 하게 되면 500 에러가 날아오면서 로그인 화면으로 리다이렉트 되는 상태가 지속 됐었다. (아래 이미지는 위와 같은 상황이 발생했을 때 slack에 연결된 webhooks로 날라오는 메세지이다!)

현재 코드상에도 토큰이 없을때 리다이렉트 되는 로직은 존재하였으나 접근 시도한 페이지에서 불러와지는 api 콜과 리다이렉트 되는 과정이 중첩되면서 api 콜 실패로 잠깐이나마 500 에러 화면이 비쳐지고 리다이렉트 되고 있었다. (아마 api 콜이 없었더라면 해당 페이지가 잠깐 보여진 상태로 리다이렉트 됐을것이라 생각된다.)

이런 상황이 지속되다보니 실제로 크리티컬한 이슈 메세지가 날라왔을 때 한번에 알아차리가 쉽지 않고 사용자 경험상으로도 500 Oops 페이지가 비춰지지 않고 리다이렉션 되는 편이 더 좋다는 생각이 들어 해당 부분을 개선하는 작업에 들어갔다.


개선하면서 신경쓰고자 했던 부분

1. 사용자에게 에러 화면 노출없이 리다이렉트 시키기
2. 해당 내용을 한곳에서 처리하기

기존에도 리다이렉트 되는 로직은 존재했으나 해당 페이지에 잠깐이나마 접근이 되고 리다이렉트 되고 있었기에 기존과는 다른 방식이 필요했고, 모든 페이지에 ssr 단에서 처리해주기에는 페이지 별로 중복되는 코드뿐만 아니라 리다이렉트 시키는 로직들이 너무 분산될 수 있다는 생각이 들었다.

그렇게 여러가지를 고민하던 중 next.js의 middleware를 만나게 되었다!

Middleware란?

Middleware는 request가 완료되기 이전에 코드를 조작할 수 있게 해준다. 특정 request에 따라 특정 request에 reponse를 rewriting, redirecting, modifying 할 수 있다.

예를들어서 토큰없이 접근하지 못하는 페이지가 /auth 하위에 존재한다고 가정한다면 아래와 같이 request url에 /auth를 포함하는 요청들을 구분해서 응답값을 조절 할 수 있는 것이다.

if (!token && req.nextUrl.pathname.includes('/auth')) {
	...
}

미들웨어는 기존에 next.js v12.0.0에서 Beta 버전으로 처음 소개가 됐었는데 next.js가 v12.2.0으로 올라오면서 안정화된 부분을 확인 할 수 있었다.

현재 회사에서는 next.js v12.2.0을 사용중이라 안정성에 대해서는 확보가 되었고 공식 홈페이지에서도 Middleware 사용 예시중 하나가 authentication 부분이라는 문구를 발견하여 Middleware를 적용하기 시작했다.

Middleware 사용하기

기존에 next.js 버전이 v12.0.0이상이라면 Middleware를 사용하는 방법은 간단하다.

  1. middleware.ts를 pages와 같은 레벨에 생성해준다.
  2. middleware.ts 파일에서 middleware function을 export 해준다.
import { NextFetchEvent, NextRequest, NextResponse } from 'next/server'

export function middleware(req: NextRequest, ev: NextFetchEvent) {

}

이렇게만 해주면 middleware 적용이 끝이난다.
참고로 middleware는 프로젝트의 매 라우팅에서 실행된다. (아래는 실행 순서이다.)

  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, etc.)
  6. afterFiles (rewrites) from next.config.js
  7. Dynamic Routes (/blog/[slug])
  8. fallback (rewrites) from next.config.js

Middleware를 실행시킬 경로를 설정해주는 두가지 방법

Middleware는 특정 request에 따라 실행시키게 만드는 방법에는 두가지가 존재한다.

  1. Custom matcher config
  2. Conditional statements

1. Custom matcher config

Middleware 파일에서 config를 하나 설정해주어 middleware.ts 파일 자체가 config에 matcher 경로에 해당하는 경우에만 실행될 수 있도록 필터링을 해주는 방법이다.

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

const COOKIES = {
  JWT_TOKEN: 'jwt_token',
}

export const config = {
  matcher: '/auth/:path*',
}

export function middleware(req: NextRequest) {
	const token = req.cookies.get(COOKIES.JWT_TOKEN)
    // /auth 하위에 모든 경로를 접속하려 하였을 때 cookie에 token이 존재하지 않는다면 아래의 조건물이 실행된다.
	if(!token){
    	...
    }
}
// 한번에 여러 경로도 설정이 가능하다.
export const config = {
  matcher: ['/auth/:path*', '/dashboard/:path*'],
}

2. Conditional statements

2번은 필자가 사용한 방법이기도 하다. (request url에 따라서 다른 로직 처리가 필요할 수도 있을 것 같아 해당 방법을 사용했다.)

이름에서도 알 수 있듯이 조건문 형태로 request로 들어오는 특정 url을 판별하는 방법이다.

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

const COOKIES = {
  JWT_TOKEN: 'jwt_token',
}

export function middleware(req: NextRequest) {
	const token = req.cookies.get(COOKIES.JWT_TOKEN)
    if (!token && req.nextUrl.pathname.includes('/auth')) {
      ...
    }
}

리다이렉트 시키기

request에 url에 따라 리다이렉트 시키는 부분에서는 NextResponse api가 사용된다.
NextResponse api로도 할 수 있는 일들이 정말 많다.

  • 들어오는 요청을 다른 URL로 redirect 시켜준다.
  • 주어진 URL을 디스플레이하면서 response를 rewrite 하게 해준다.
  • API Routes를 위한 request 헤더를 설정 (getServerSideProps), 목적지를 rewrite
  • cookies 응답값을 설정
  • headers 응답값을 설정

들어오는 요청을 다른 URL로 redirect 시켜주는 부분이 필자가 사용한 부분인데 대략적으로 아래와 같이 사용하였다!

참고로 2. Conditional statements를 활용하여 request url을 판별하였다고 하였으나 1번의 config matcher가 존재하는 이유는 static과 public 파일들을 구별 해내기 위함이다.

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

const COOKIES = {
  JWT_TOKEN: 'jwt_token',
}

export const config = { matcher: '/((?!.*\\.).*)' }

export function middleware(req: NextRequest) {
  const token = req.cookies.get(COOKIES.JWT_TOKEN)

  // 해당 pathname은 임의로 적어본 pathname이다.
  if (!token && req.nextUrl.pathname.includes('/test')) {
    const clonedUrl = req.nextUrl.clone()
    
    // local과 production 조건에 따라 hostname 설정
    clonedUrl.hostname = '~~' 

    // local과 production에 따라 port 구분
    if (
      //production 인경우 
    ) {
      clonedUrl.protocol = 'https:'
      clonedUrl.port = '443'
    }

    // 로그인전에 접근 시도하려 했던 페이지로 리다이렉트 시키기
    clonedUrl.search = `redirect_url=${encodeURIComponent(req.nextUrl.pathname)}`

    return NextResponse.redirect(clonedUrl.href)
  }
}

이렇게 미들웨어를 적용하고 나니 실제로 error가 날라오는 수가 현저히 줄어들었고 실제로 확인해봤을 때도 매끄럽게 리다이렉트 되는 부분을 확일 할 수 있었다!

많은 기능을 가진 Middleware

사실 필자가 사용한 Middleware의 기능은 정말 빙산의 일각일정도로 훨씬 많은 기능들이 존재한다.

{ 
	ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
    browser: { name: 'Chrome', version: '108.0.0.0', major: '108' },
    engine: { name: 'Blink', version: '108.0.0.0' },
    os: { name: 'Mac OS', version: '10.15.7' },
    device: { vendor: undefined, model: undefined, type: undefined },
    cpu: { architecture: undefined },
    isBot: false 
}

이번 글에서는 실제로 코드에서 활용한 내용만을 다뤄보긴 했지만 Next.js가 13버전이 나오면서 추가된 기능들도 여럿 존재해서 Middleware에 대해서 조금 더 공부하고 더 많이 활용할수 있도록 해봐야겠다.

profile
언제나 새로운 도전을 꿈꾸는 개발자

0개의 댓글