거의 알고리즘 일기장

cra 환경에서 배포환경에 따라 .env 나누기 & 동작에 대해 톺아보기(with: env-cmd) 본문

web

cra 환경에서 배포환경에 따라 .env 나누기 & 동작에 대해 톺아보기(with: env-cmd)

건우권 2023. 3. 13. 17:46

원래 배포환경에 따라 달라지는게 없는 프로덕트를 수정할 일이 생겼다.


cra의 내장된 .env 파일 사용시의 제약

이 수정사항 중 배포환경(dev or prod)에 따라서 환경변수가 몇개 바뀌어야 하는 상황이었는데, 

cra 환경에서는 이런식으로 .env.* 이런식으로 파일을 나눠서 환경변수를 관리할수 있다.

 

근데, 이러한 제약이 존재한다.

/...
this feature is available with react-scripts@1.0.0 and higher. (웬만한 프로젝트는 react-scripts1.0.0 버전상위다.)

// -> 이 순서로 우선순위가 있다.
npm start: .env.development.local, .env.local, .env.development, .env
npm run build: .env.production.local, .env.local, .env.production, .env
npm test: .env.test.local, .env.test, .env (note .env.local is missing)

https://create-react-app.dev/docs/adding-custom-environment-variables/

 

Adding Custom Environment Variables | Create React App

Note: this feature is available with react-scripts@0.2.3 and higher.

create-react-app.dev

 

이게 왜 문제가 되냐면,

 

만약, dev환경으로 배포하고 싶을때는 .env.development 파일을 쓰고 싶을텐데 위의 우선순위를 보면,

 

npm run build 시에는 .env.development라는 파일을 읽지 않는다.쿄쿄쿄

 

결과적으로 해결책은

1. cloud에서 사용할 모든 환경변수를 dev or product trigger에 따라 맞춰 주입하도록 한다. (이 프로젝트는 gcp cloud build를 사용중임)

2. 다른 라이브러리를 이용해서 강제로 dev build일때 .env.development 파일을 억지로 먹히도록 설정한다.

 

필자는 2번을 선택했다.


env-cmd을 이용하여 custom env 설정

https://github.com/toddbluhm/env-cmd

 

GitHub - toddbluhm/env-cmd: Setting environment variables from a file

Setting environment variables from a file. Contribute to toddbluhm/env-cmd development by creating an account on GitHub.

github.com

좀 서칭해보니 이러한 라이브러리를 찾았다.

 

이 라이브러리의 이 옵션을 쓰면 .env file의 커스텀이 가능하다.

-f, --file [path] Custom env file path (default path: ./.env)

 

그리고 하단의 명령어를 실행해보았더니

"build:development": "env-cmd -f .env.development craco build",

빌드결과

.env.development에 있는 설정들이 잘들어감을 확인할수있었다.


궁금증

그렇다면 궁금증이 생긴다.

어떻게 이 동작이 가능할까? 그래서 코드를 좀 까보았다.

 

실행되는 명령어는 다음과 같다.(dev의 경우)

env-cmd -f .env.development craco build

 

우리는 env-cmd -f .env.development의 동작만 확인하면 된다.

 

저 명령어를 실행하면 이게 실행된다.

#! /usr/bin/env node
require('../dist').CLI(process.argv.slice(2))

 

CLI라는 function의 동작은 EnvCmd라는 async function을 실행한다.

/**
 * Executes env - cmd using command line arguments
 * @export
 * @param {string[]} args Command line argument to pass in ['-f', './.env']
 * @returns {Promise<{ [key: string]: any }>}
 */
export async function CLI (args: string[]): Promise<{ [key: string]: any }> {
  // Parse the args from the command line
  const parsedArgs = parseArgs(args)

  // Run EnvCmd
  try {
    return await (exports.EnvCmd(parsedArgs) as Promise<{ [key: string]: any }>)
  } catch (e) {
    console.error(e)
    return process.exit(1)
  }
}

 

그렇다면, EnvCmd function에서 핵심적인 동작을 할것같다.

 

요약하자면, 여기서는 cmd에서 받은 옵션과 인자들을 바탕으로 실행되는데, 

우리가 실행한 명령어에서는 아래 그림과같이 동작할것이다.

그림설명

그렇다면 결과적으로 우리가 실행하는 명령어는 아래와 같다.

REACT_APP_ENV=dev REACT_APP_TEST=test craco build

 

이렇게 되면 cra build 안에서는 .env.production의 환경변수를 읽겠지만, .env.production의 환경변수와 동일한 값이 context에 있다면 override할것이다.

그림설명

/**
 * The main env-cmd program. This will spawn a new process and run the given command using
 * various environment file solutions.
 *
 * @export
 * @param {EnvCmdOptions} { command, commandArgs, envFile, rc, options }
 * @returns {Promise<{ [key: string]: any }>} Returns an object containing [environment variable name]: value
 */
export async function EnvCmd (
  {
    command,
    commandArgs,
    envFile,
    rc,
    options = {}
  }: EnvCmdOptions
): Promise<{ [key: string]: any }> {
  let env: { [name: string]: string } = {}
  try {
    env = await getEnvVars({ envFile, rc, verbose: options.verbose })
  } catch (e) {
    if (!(options.silent ?? false)) {
      throw e
    }
  }
  // Override the merge order if --no-override flag set
  if (options.noOverride === true) {
    env = Object.assign({}, env, process.env)
  } else {
    // Add in the system environment variables to our environment list
    env = Object.assign({}, process.env, env)
  }

  if (options.expandEnvs === true) {
    command = expandEnvs(command, env)
    commandArgs = commandArgs.map(arg => expandEnvs(arg, env))
  }

  // Execute the command with the given environment variables
  const proc = spawn(command, commandArgs, {
    stdio: 'inherit',
    shell: options.useShell,
    env
  })

  // Handle any termination signals for parent and child proceses
  const signals = new TermSignals({ verbose: options.verbose })
  signals.handleUncaughtExceptions()
  signals.handleTermSignals(proc)

  return env
}

예전에는 cra가 참 편하다고 생각했는데
요즘에는 더 간편한 대체제들이 많이나와서 잘 안쓰려고 하는거 같다

 

그래도 오랜만에 만나서 즐거웠다 cra야~ 

반응형
Comments