본문 바로가기
고민, 이슈 정리/이슈, 버그

[해결] Next.js GCP AppEngine 배포 환경변수 분기

by 3.dev 2023. 8. 11.
반응형

Next.js 프로젝트를 완료한 후 배포환경을 설정하던 도중 예상치 못한 난관에 마주했다.

Google Cloud Platform의 App Engine을 사용해 배포하는데

문제는 project-dev서비스와 project-prd서비스로 나누어 각각 환경변수를 다르게 사용하려했다.

dev서비스에는 .env.development 파일을, prd서비스에는 .env.production파일을 사용하도록 하려는데

지금까지 시도해본 모든 방법, 설정은 모두 한쪽만 다르게 설정해내지 못했다.

 

 

내가 생각한 순서도에 따르면 다음과 같다.

내 머릿속의 순서도

"scripts": {
    "dev": "next dev",
    "lint": "next lint",
    "start:dev": "APP_ENV=development next start",
    "start": "APP_ENV=production next start",
    "build:dev": "APP_ENV=development next build",
    "build": "APP_ENV=production next build",
    "deploy:dev": "gcloud app deploy --project='project-dev' -q --appyaml=dev_app.yaml",
    "deploy": "gcloud app deploy --project='project-prd' -q --appyaml=prd_app.yaml"
  }

deploy를 통해서 GCP App Engine에 업로드

runtime: nodejs18
service: next-project-develop

entrypoint: "npm run start:dev"

env_variables:
  APP_ENV: "development"

App Engine "next-project-develop"에서 app.yaml을 읽어 빌드 -> entrypoint를 읽어 npm run start:dev 실행

 const dotenv = require("dotenv");
 const path = require("path");
 
 const nextConfig =()=> {
     const envPath = path.resolve(__dirname, `.env.${process.env.APP_ENV}`);
     const envConfig = dotenv.config({ path: envPath }).parsed;
     return {
       env: envConfig,
       reactStrictMode: false,
       swcMinify: true,
     };
 }
 
 module.exports = nextConfig();

이 과정에서 next.config.js를 읽는데 env는 APP_ENV이름에 따라 주입되도록 했으니

development는 .env.development를 주입하고

production은 .env.production을 주입해야 맞다.

 

로컬에서 npm run build:dev, npm start:dev를 실행했을때 완벽히 내가 원하는 결과물이 도출된다.
또한 :dev를 제외하고 실행하면 production이 잘 주입된다.

문제는 App Engine Service에서는 둘다 production만 주입된다는 점

 

원인.

아무래도 빌드자체가 npm run build만 실행되는듯 보인다.

시도방법

 1. next.config.js에서 phase를 이용

   마찬가지로 app engine에서 next start명령을 하기때문에 production으로 인식한다.

 2. .env파일명 변경

  의미가 없었다. .env, .env.local, .env.development, .env.production, .env.test 등
  지원되는 모든 .env파일명을 시도해봤다.

 3. yaml에서 env valuables 직접설정

  주입되지 않는다. 원인은 파악중.

 

해결방법

 

1. next.config.ts에서 output, distDir을 사용해서 빌드파일만 분리 한다.

const dotenv = require("dotenv");
const { PHASE_PRODUCTION_BUILD } = require("next/dist/shared/lib/constants");
const path = require("path");

const nextConfig = (phase) => {
  const envPath = path.resolve(__dirname, `.env.${process.env.APP_ENV}`);
  const envConfig = dotenv.config({ path: envPath }).parsed;

  if (phase === PHASE_PRODUCTION_BUILD) {
    return {
      env: envConfig,
      reactStrictMode: false,
      swcMinify: true,
      output: "export",
      distDir: "../build",
    };
  } else {
    return {
      env: envConfig,
      reactStrictMode: false,
      swcMinify: true,
    };
  }
};

module.exports = nextConfig;

설정된 내용은 다음과 같다.
1. local docker develop환경을 위해 phase값을 읽어 PHASE_PRODUCTION_BUILD인 경우에만 output, distDir을 사용
2. static으로 사용해도 무방한(b2b admin) 서비스이기 때문에 output을 사용해 html추출

3. 루트프로젝트 바깥으로 build(static build html)추출

2. 빌드를 시도한다.

  "scripts": {
    "dev": "next dev",
    "lint": "next lint",
    "start:dev": "APP_ENV=development node app.js",
    "start": "node app.js",
    "build:dev": "APP_ENV=development next build",
    "build": "APP_ENV=production next build",
  },

development 배포 시에는 npm run build:dev
production 배포 시에는 npm run build

 

 

3. 루트프로젝트 바깥으로 디렉토리를 이동해 파일을 설정한다.

바깥에서도 간단히 설정해줄 것이 있다.

우선 app_dev.yaml, app_prd.yaml을 생성해 각각 서비스명만 다르게 작성한다.

runtime: nodejs18 # or another supported version
service: next-project-develop

 

이후 app.js를 하나 생성해준다.

const express = require("express");
const app = express();
const server = require("http").createServer(app);

app.get("/", function (req, res) {
  res.sendFile(__dirname + "/build/index.html", {
    etag: false,
    lastModified: false,
  });
});
app.use(
  "/",
  express.static(__dirname + "/build", { etag: false, lastModified: false })
);
app.get("/*", function (req, res) {
  res.sendFile(__dirname + "/build/index.html", {
    etag: false,
    lastModified: false,
  });
});
server.listen(8080, function () {
  console.log("listening on port 8080");
});

추출된 static build 를 실행해주는 파일이다.

 

4. 마지막으로 package.json을 다음과같이 작성하고 npm install, deploy해준다.

{
  "name": "barofactory",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "homepage": ".",
  "scripts": {
    "start": "node app.js",
    "deploy:dev": "gcloud app deploy --project='project-dev' -q --appyaml=dev_app.yaml",
    "deploy": "gcloud app deploy --project='project-prd' -q --appyaml=prd_app.yaml"
  },
  "author": "",
  "engines": {
    "node": ">=8"
  },
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1"
  }
}

start는 app.js를 사용해서 하도록하고

각각 배포될 GCP AppEngine 저장소를 분기해 명령어를 작성해 두었다.

빌드 된 파일들을 development쪽으로 배포하려면 npm deploy:dev

production으로 배포하려면 npm deploy를 실행하면 드디어 환경변수도 정상 작동하는 것을 볼 수 있다.

 

주의할 점으로는 빌드 되어 추출된 파일만 업로드 하므로

환경변수 적용이 빌드가 dev로 되었다면 dev가 배포되고 production으로 되었다면 production으로 빌드된다.

요점은 빌드를 조심하자 라는 것이다.

 

이번 고민을 해결하면서 하나 더 장점으로 얻어가는 것이 있다면

기존 배포방식에서 용량이 4~500MB였고 시간은 약 배포 당 15분 소요되었다.

새 배포방식에서는 용량이 8.1MB로 고정됐고 시간은 약 배포당 8분가량으로 획기적인 단축이 이뤄졌다.

 

Server Side Rendering이 무조건 적으로 필요하지 않다면

가볍고 빠른 배포, 수정이 가능한 이 방법이 좋을 것 같다는 생각이 든다.

 

다음엔 이 귀찮은 배포를 자동으로 해결해줄 CI/CD를 알아볼 예정이다.

 

해결완료!!

반응형