Next 14에서 HTTPS 로컬 환경 구성 + Proxy middleware로 CORS 오류 해결

JUNG MINU·2024년 4월 21일
0

로컬 환경에서 프론트엔드 개발을 할 때 반드시 HTTPS와 Proxy가 동시에 필요한 시점이 옵니다.
예를 들어 백엔드 개발 환경에 API 요청을 한다거나, 인증을 위해 백엔드에서 Cookie설정을 하는 경우 기본적인 로컬 개발환경에서는 한계가 있습니다.

이러한 불편을 해결하기 위해 로컬 환경에서도 개발 환경과 동일한 조건에서 테스트를 진행하기 위한 환경 구성하는 것이 필요한데요,
Webpack 또는 Vite의 DevServer를 이용하는 일반적인 환경의 React나 Vue 프로젝트의 경우 위 조건을 구성하는 것이 매우 간단합니다.

하지만 Next는 풀스택 프레임워크의 특성상 지금껏 했던 것과는 약간 다른 방법으로 개발 환경을 구성해야 했습니다.

이 글에서는 Next 14를 이용한 프론트엔드 로컬 개발환경에서 HTTPS로 서버를 실행하고, Proxy middleware를 통해 서버와 통신하기 위해 제가 구성한 방법을 기록하겠습니다.

개발 환경
Next 14.2.1
macOS Sonoma 14.2

목표

  • 로컬 프론트엔드 서버가 개발환경의 백엔드와 API 통신을 문제없이 할 수 있습니다.

구현

HTTPS

기존에 주로 사용한 Vite를 예로 들면 HTTPS서버를 로컬에서 실행하는 것은 아주 간단했는데요,

// ./package.json

"scripts": {
  	"dev:https": "HTTPS=true SSL_CRT_FILE=localhost.pem SSL_KEY_FILE=localhost-key.pem vite",
  ...
}

HTTPS서버를 위한 인증 pem키를 발급받고, 위 스크립트만 실행하면 HTTPS 로컬 환경을 구성할 수 있었기 때문입니다.

하지만 Next 14에서는 별도의 Custom server를 이용해 프론트엔드 서버를 직접 구성해줘야 합니다.

1. SSL 키 발급

우선 SSL인증을 위해 프로젝트 root 경로에서 mkcert를 이용해 pem키를 발급받습니다.

아래 방법은 macOS를 기준으로 작성되었습니다.

$ brew install mkcert

$ mkcert -install

$ mkcert localhost

또는,
공동으로 작업하는 환경의 경우 shell script를 이용해 더 간편하게 키를 발급받을 수 있습니다.

// ./init-ssl.sh

#!/bin/bash

MKCERT_INSTALLED=$(which mkcert)

if [ -z $MKCERT_INSTALLED ];then
    brew install mkcert
fi

mkcert -install
mkcert localhost

이후 package.json에 해당 shell을 실행하는 script를 생성합니다.

// ./package.json

"script": {
  "init:ssl": "sh init-ssl.sh",
  ...
}

그럼 localhost.pem, localhost-key.pem 두 파일이 생성됩니다.

2. Custom server 구성

HTTPS 서버 환경 구성을 위해 Custom server로 프론트엔드 로컬 서버를 구축할 것입니다.

Root 경로에 server.js를 생성합니다.

node.js로 서버를 구성하는데요, 라우팅 등 프론트엔드 서버로서의 기본적인 역할은 next가 알아서 해줄테니 걱정하지 않아도 됩니다.
저희는 특정 로컬 포트로 서버를 열어주기만 할거에요.

우선 필요한 패키지들을 import해줍니다.

// ./server.js

const http = require('http');
const https = require('https');
const fs = require('fs');
const { parse } = require('url');
const next = require('next');

production환경에서는 실행되지 않도록 NODE_ENV를 확인하고, NextServer를 선언합니다.

// ./server.js

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

NextServer가 구성되면 HTTP와 HTTPS 서버를 지정된 포트로 열어줍니다.
HTTPS서버를 생성할 때에는 option에 아까 생성한 pem키를 넣어줍니다.

// ./server.js

app.prepare().then(() => {
  const HTTP_PORT = process.env.HTTP_PORT;
  const HTTPS_PORT = process.env.HTTPS_PORT;

  const httpServer = http.createServer((req, res) => {
    const parsedUrl = parse(req.url, true);
    handle(req, res, parsedUrl);
  });

  const httpsServer = https.createServer(
    {
      cert: fs.readFileSync(`${process.env.HTTPS_CERT_PATH}.pem`),
      key: fs.readFileSync(`${process.env.HTTPS_KEY_PATH}.pem`),
    },
    (req, res) => {
      const parsedUrl = parse(req.url, true);
      handle(req, res, parsedUrl);
    },
  );
  
  httpServer.listen(HTTP_PORT, err => {
    if (err) throw err;
    console.log(`> Ready on http://localhost:${HTTP_PORT}`);
  });

  httpsServer.listen(HTTPS_PORT, err => {
    if (err) throw err;
    console.log(`> Ready on https://localhost:${HTTPS_PORT}`);
  });
})

package.json에 next를 실행하는 대신 방금 만든 server.js를 실행하는 script를 추가합니다.

// ./package.json

"script": {
  "dev:https": "node server.js",
  ...
}

터미널에 해당 명령어를 입력하면 정상적으로 HTTPS서버가 실행됩니다.

$ npm run dev:https

> app-name@0.1.0 dev:https
> node server.js

> Ready on http://localhost:3000
> Ready on https://localhost:3001

Proxy

Custom server를 사용하지 않을 때

지금까지 HTTPS로 로컬 서버를 실행해보았는데요, Custom server를 사용하지 않을 때는 Next의 rewrites option을 활용하면 특정 요청의 origin이나 path를 변형하는 것이 매우 간단합니다.

//next.config.mjs

const nextConfig = {
  async rewrites() {
    return [
      {
        source: "/api/:path*",
        destination: `https://.../:path*`
      },
    ];
  },
};

이런식으로 nextConfig에 rewrites를 설정해주면 특정 source에서 가는 요청의 도메인을 바꿔 브라우저에서 동일한 origin의 통신으로 인식하게 해 CORS 오류가 발생하지 않습니다.

또는 로컬과 개발 환경에서 api path가 상이할 경우에도 쉽게 path를 변형해 api를 요청할 수 있습니다.

Custom server를 사용할 때

하지만 custom server를 이용해 HTTPS로 로컬 서버를 구성했을 경우, nextConfig의 Proxy 설정이 적용되지 않았습니다.

이런 경우 next에서 제공되는 middleware를 이용해 간단하게 전역 Proxy middleware를 구성할 수 있습니다.

우선 app 폴더와 동일한 최상위 경로에 middleware.ts파일을 생성해줍니다. 저는 ./src 하위에 app 폴더를 생성했으므로 ./src/middleware.ts를 생성해줬습니다.

middleware 함수를 선언해줍니다. NextResponse의 rewrite 메서드를 이용하면 전송되는 api의 path를 요청 앞단에서 변경해줍니다.

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

export function middleware(request: NextRequest) {
  const rewritePath = `${process.env.NEXT_PUBLIC_API_URL}/${request.nextUrl.pathname}`;
  
  console.log(`[Proxy]: ${request.url} => ${rewritePath}`);
  
  return NextResponse.rewrite(new URL(rewritePath));
}

단,
이 방법으로 middleware를 구성할 경우 모든 api요청, next의 정적 리소스를 포함하여 모든 요청들이 이 middleware를 타면서 path가 rewrite됩니다.

그러므로 특정 uri path에서만 middleware가 작동하도록 설정을 해주어야 합니다.

export const config = {
  matcher: ['/api/:path*'],
};

또는,
여러 path에 대해 조건부로 middleware의 동작이 필요할 경우 함수 내부에 조건문으로 분기해줄 수 있습니다.

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/api')) {
    const rewritePath = `${process.env.NEXT_PUBLIC_API_URL}${request.nextUrl.pathname}`;

    console.log(`> [Proxy] ${request.url} => ${rewritePath}`);
    return NextResponse.rewrite(new URL(rewritePath, request.url));
  }

  return NextResponse.next();
}

이렇게 설정하면 /api/... 로 시작하는 모든 endpoint의 요청은 특정 origin의 요청 헤더를 가지고 요청되므로 브라우저의 CORS정책에 위배되지 않습니다.

...
 ✓ Compiled /src/middleware in 217ms (71 modules)
 
[Proxy]: https://localhost:3000/api/v1/game/cnt => https://.../api/v1/game/cnt

 GET /favicon.ico 200 in 5ms
...

NODE ENV가 'development'인 경우에만 middleware를 동작하게 하기 위해 함수의 첫줄에 조건문을 하나 적어줍니다.

if (process.env.NODE_ENV !== 'development') return NextResponse.next();

정상적으로 구현이 된다면,

  • Next는 로컬에서 server.js의 custom server를 통해 실행되고, 발급받은 pem키를 이용한 SSL인증으로 HTTPS로 서버를 실행합니다.

  • Proxy를 이용해 개발환경의 백엔드 서버와 CORS문제 없이 JSON을 주고 받습니다.

  • Google 등 SSO를 이용해 OAuth2로 로그인 할 때 로컬에서도 문제 없이 로그인 할 수 있습니다.

  • Origin이 다른 백엔드 서버와 API통신을 할 때에도 브라우저가 CORS 오류를 발생시키지 않습니다.

  • Refresh token 등 사용자 인증을 위해 백엔드에서 httpOnly, sameSite 등 보안 설정이 된 Set-Cookie 헤더의 데이터가 문제없이 쿠키에 저장됩니다.

즉, 로컬 서버의 프론트엔드를 개발환경에 배포된 프론트엔드처럼 동일한 동작을 테스트해볼 수 있습니다.

profile
감각있는 프론트엔드 개발자 정민우입니다.

0개의 댓글