소개
Next.js의 12버전에서 Middleware 기능이 공개되었다.
12.2부터는 정식 기능이 되었다. (사용법도 12.2부터 많이 다르다.)
공개 시점부터 관심을 가진 기술이다.
기존에 알고 있던 미들웨어의 개념과 전혀 다른 개념이 아니다.
단지 Next.js에서 미들웨어 개념이 생긴 것이다.
이 개념이 없었어도 Node.js를 이용해 충분히 미들웨어 기능이 구현 가능했다.
Next.js의 Middleware가 뭐냐면
간단히 말하면 '사용자의 요청을 처리하여 응답을 반환하기 전에 뭔가 조작하여 응답할 수 있는 기능'이다.
여기서 조작할 수 있는 뭔가의 개념은 상당히 제한적이다.
기본적으로 요청 헤더, 응답 헤더, 쿠키, 경로 등을 수정할 수 있다. (난 이 기본적인 기능만으로 당장 업무에 해볼 수 있는 게 너무 많이 떠올라서 관심이 생겼다.)
상당히 제한적인 이유는 Node.js의 API들을 이용할 수 없기 때문이다.
Next.js는 Middleware를 Edge Functions 기반으로 만들어졌다. Node.js가 아니라고 보면 된다.
Edge Functions는 쉽게 말하면 V8 엔진 기반의 웹 표준 API의 하위 집합으로 이루어진 아주 아주 작은 크기 (1MB)의 함수 집합이다.
(제공하는 API: https://edge-runtime.vercel.app/features/available-apis#network-apis )
아주 작게 만든 이유는 성능을 위해서다.
아주 아주 작기 때문에 부팅도 빠르고 사용자 응답을 빠르게 해줄 수 있다.
즉 요약하면 Next.js의 Edge Functions는 아주 아주 빠르고 보안도 우수하다.
여담으로 보안이 우수하단 점 때문에 기존 시스템에 도입할 때 고생을 했다. 왜냐하면 Edge Functions는 Node.js API를 실행할 수 있는 경로를 아예 차단하고 있기 때문에 코드를 동적으로 실행시킬 수 있는 기능도 제한하고 있다. Eval, new Function 같은 것을 금지하고 있다. 가장 난관을 겪었던 것은 개발 환경에선 약한 경고로 끝내고 빌드가 될 때 에러를 발생시킨다. 그래도 난 이 기능의 가치를 포기하기가 싫어서 기존 시스템을 고치기로 팀원들을 설득했다..
Next.js의 Middleware는 Edge Functions를 기반으로 동작한다.
그래서 Middleware로 구체적으로 무엇을 할 수 있나?
Next.js의 설명으로는
이런 것들을 할 수 있다고 소개한다.
인증 (Authentication)을 활용한 예시로 소개하자면
사용자가 로그인 되었는지를 확인하고 로그인 되었으면 메인페이지로, 로그인이 안 되었으면 로그인 페이지로 라우팅 시킨다고 생각해보자.
기존에는 Next.js를 사용한다고 하더라도 useEffect를 통해 CSR 방식으로 사용자의 로그인 상태를 확인하고 페이지를 라우팅 시킬 것이다.
이렇게 되면 문제가 useEffect는 비동기적으로 화면이 렌더링되고 동작하기 때문에 사용자에게 잠시 동안 초기 상태의 화면이 보인다는 것이다. 그리고 페이지 요청도 결국 브라우저에서 두 번 해야 한다.
이런 문제를 해결하기 위해 추가적으로 코드가 들어가야 한다.
그러나 이런 문제를 Middleware로 구현한다면 아주 우아하고 쉽게 해결할 수 있다.
왜냐하면 Middleware의 개념 자체가 사용자에게 요청이 오면 응답 전에 '뭔가'를 처리해서 줄 수 있는 것이기 때문이다.
요청이 오면 로그인 되었는지 확인하고 각 페이지로 가도록 redirect 시키면 끝이다.
이렇게 되면 하나의 요청에 처리 할 수 있어서 응답 시간도 개선할 수 있고
앞서 말한 사용자 경험 문제도 향상시킬 수 있다.
Next.js 12.2에서의 변화
12.2부터 middleware가 stable 버전에 들어오면서
사용법도 많이 바뀌었다.
자세한 내용은 https://nextjs.org/blog/next-12-2#middleware-stable
요약하자면 기존에는 pages 폴더 아래 _middleware.ts 파일로 각 디렉토리별로 중첩하여 정의할 수 있었지만
이제는 무조건 루트 디렉토리에 middleware.ts 하나만 정의해야 한다.
이렇게 변경된 이유는 곧 도입될 rfc 레이아웃에도 적합한 구조가 아니고
무엇보다 middleware가 여러 개라서 여러 번 중첩되어 호출이 일어나게 된다. (이것이 다 비용이다)
업무에 활용하기
나는 이 기능을 우리 핀다 웹서비스에 도입하면서
팀원들을 설득하기 위해 한가지 예시를 생각해봤다.
일단 리덕스에 익숙한 프론트엔드 개발자에게
미들웨어 개념이 낯선 것은 아닐지라도
왜 필요한지 설득이 필요하다.
설득하기 위해 필요한 것은 2가지였다.
1. 기존 Node.js 기반 미들웨어보다 우수한 것은 무엇인가?
2. 왜 기존 시스템과 충돌을 해결하면서까지 이제 막 stable 버전에 들어온 기능을 도입해야 하는가?
1번은 Edge Functions의 가벼움을 강조했고, Next.js에 통합하여 움직인다는 점이 장기적으로 미들웨어를 직접 우리 코드로 만드는 것보다 안전하고 성능적으로 이득이 클 것이라고 강조했다.
2번은 기존 시스템의 문제를 찾아서 코드로 완성시킨 다음에 제시했다.
핀다의 메인페이지는 모바일에서 접속시 무려 61.6MB의 리소스를 잡아먹는다. (죄송합니다...)
이렇게 된 이유는 Next.js의 프리렌더링 때문이다.
모바일에서 접속한 사용자와 데스크탑에서 접속한 사용자에겐 각자 다른 화면을 보여줘야 하는데
그 화면 구분을 일단 페이지에 진입 후 useEffect에서 화면 사이즈로 분기하여 모바일 화면, 데스크탑 화면을 보여준다.
이렇게 되니까 모바일에서 접속하게 되면 SSG에 의해 첫 프리렌더링된 파일이 데스크탑용으로 되어 있기 때문에 모바일 사용자가 핀다 홈페이지에 들어오면 데스크탑용 html을 받게 되고, html 파서가 읽으면서 리소스를 요청하기 때문에 결과적으로 모바일 사용자인데 데스크탑용 리소스를 불필요하게 다운받게 된다.
그러다가 useEffect에 의해 다시 모바일용 리소스를 추가 요청하게 된다. (....)
Middleware를 활용해 위 문제를 해결한 코드는 다음과 같다.
import { NextResponse, userAgent } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/') {
const { device } = userAgent(request)
const viewport = device.type === 'mobile' ? 'mobile' : 'desktop'
request.nextUrl.pathname = `_viewport/${viewport}`
return NextResponse.rewrite(request.nextUrl)
}
return NextResponse.next();
}
// matcher에 해당하는 경로일 경우만 위 middleware가 실행된다. (과부하 방지)
export const config = {
matcher: ['/'],
}
메인페이지 ('/') 경로로 요청이 오면 미들웨어가 실행되어 요청 헤더에서 사용자 디바이스 정보(userAgent)를 구한다.
그 뒤 디바이스 타입에 따라 모바일과 데스크탑 값을 url 뒤에 붙여서 rewrite 한다. (rewrite 덕분에 사용자는 _viewport/mobile 같은 경로로 바뀐다는 것을 알지 못한다.)
그리고서 원래 기존 페이지는 _viewport 이름으로 하위폴더를 만들고 그 안에 desktop.tsx, mobile.tsx 파일을 만든다. (각각 프리렌더링이 되기 위해)
그리고 @/ui/home이 가리킨 위치에서 useRouter를 이용해 route 값에서 _viewport 부분을 읽고 그 값을 기준으로 리소스를 출력하게 하면 된다.
이렇게 하면 문제가 해결된다.
핀다 웹사이트에선 이렇게 해서 리소스를 모바일 기준으로 60% 정도 줄였다.
결론
요청이 왔을 때 뭔가를 처리해서 응답을 줄 수 있는 것이 아주 매력적인 기능 같다.
ssr인 getServerSideProps랑은 처리 범위부터 완전히 다르다.
아직까진 하루에 수십억씩 오가는 서비스에서 장애를 겪는 건 매우 위험하기 때문에
조심스럽지만
동작 방식이 아주 심플해서 차츰 업무에서 많이 활용해볼 생각이다.
참고
https://vercel.com/blog/introducing-the-edge-runtime
https://edge-runtime.vercel.app/
https://nextjs.org/docs/advanced-features/middleware
'업무' 카테고리의 다른 글
[Next.js] Script 추가하기 (채널톡 추가하기, next/script) (0) | 2022.09.28 |
---|---|
Prefetching을 활용한 웹뷰 서비스 설계 및 회고 (0) | 2022.09.24 |
커밋시 eslint 속도 개선 (lint-staged, husky) (0) | 2022.09.04 |
배포 자동화 도구 개발 회고 (0) | 2022.09.03 |
리액트 리소스 로딩 속도 개선하기 (0) | 2022.08.30 |