본문 바로가기
TYLER SONGBlog
← Blog index
CS학습6 min read

환경 변수: 프로세스가 상속받는 key-value 설정으로 코드와 환경을 분리하는 방법

환경 변수(Environment Variable)는 프로세스가 실행될 때 운영체제로부터 전달받는 문자열 key-value 쌍이다. 자식 프로세스에 상속되는 특성 덕분에 코드를 수정하지 않고도 DB 주소, API 키, 실행 모드 같은 설정을 환경마다 다르게 줄 수 있다. 이 글에서는 셸 변수와의 차이, 상속 원리, Node.js(TypeScript)와 She

By 송민성

1. 개념

환경 변수(Environment Variable)는 프로세스(process)가 가지는 이름=값 형태의 문자열 쌍 모음이다. 이름과 값은 모두 문자열이며, 프로세스가 시작될 때 부모 프로세스로부터 복사되어 전달된다.

대표적인 환경 변수는 다음과 같다.

이름의미
PATH명령어를 찾을 디렉터리 목록(:로 구분)
HOME현재 사용자의 홈 디렉터리
USER현재 사용자 이름
SHELL사용자의 기본 셸 경로
LANG로케일(언어·문자 인코딩) 설정

셸 변수(shell variable)와 환경 변수는 다르다. 셸 변수는 해당 셸 안에서만 보이고, export를 거친 변수만 환경 변수가 되어 자식 프로세스에 전달된다.

2. 왜 사용하는가

  • 코드와 설정의 분리: DB 주소나 API 키를 소스코드에 박아 두면 환경(개발·스테이징·운영)마다 코드를 고쳐야 한다. 환경 변수를 쓰면 같은 코드·같은 빌드 결과물을 설정만 바꿔 실행할 수 있다.
  • 비밀 정보의 코드 저장소 유출 방지: 비밀번호와 토큰을 Git에 커밋하지 않고 실행 환경에서 주입할 수 있다.
  • 언어와 도구에 독립적: 환경 변수는 OS 수준 기능이므로 Python, Node.js, Go, Shell 스크립트 어디서든 같은 방식으로 읽는다.
  • 동작 제어: NODE_ENV=production, LANG=ko_KR.UTF-8처럼 프로그램의 동작 방식을 바깥에서 조절한다.

3. 동작 원리

3.1 프로세스 생성과 상속

리눅스에서 프로세스는 fork()로 복제된 뒤 exec 계열 함수로 새 프로그램을 실행한다. 이때 환경 변수 목록이 자식에게 넘어간다. 예를 들어 execve(path, argv, envp)의 세 번째 인자 envp가 이름=값 문자열 배열이다.

핵심 규칙은 두 가지다.

  1. 자식 프로세스는 부모의 환경 변수를 복사해서 받는다.
  2. 자식이 값을 바꿔도 부모에는 영향이 없다. 반대로 부모가 나중에 값을 바꿔도 이미 실행된 자식에는 반영되지 않는다.
text
셸(부모)  ──export API_URL=...──▶  node 프로세스(자식, 복사본 보유)
   │                                   │
   └─ 이후 값을 바꿔도                  └─ 자식 안에서 바꿔도
      실행 중인 자식에는 반영 안 됨         부모 셸은 그대로

3.2 셸 변수와 export

text
NAME=value          # 셸 변수: 현재 셸에서만 보임
export NAME=value   # 환경 변수: 자식 프로세스에도 전달됨

3.3 PATH 탐색

ls처럼 경로 없이 명령을 입력하면 셸은 PATH에 나열된 디렉터리를 왼쪽부터 차례로 검사해 처음 발견한 실행 파일을 실행한다. 그래서 PATH 앞쪽에 있는 디렉터리가 우선한다.

3.4 영속성

환경 변수는 프로세스 메모리에 있으므로 셸을 닫으면 사라진다. 계속 유지하려면 ~/.bashrc, ~/.zshrc, ~/.profile 같은 셸 시작 파일에 export 문을 적어 둔다. 어떤 파일이 읽히는지는 로그인 셸 여부와 셸 종류에 따라 다르다.

4. 코드 예제

4.1 Shell: 셸 변수와 환경 변수의 차이

shell
# 셸 변수만 설정 GREETING=hello sh -c 'echo "자식: [$GREETING]"' # 자식: [] (전달되지 않음) # export로 환경 변수 승격 export GREETING sh -c 'echo "자식: [$GREETING]"' # 자식: [hello] # 해당 명령 한 번에만 적용 (현재 셸에는 남지 않음) MODE=debug sh -c 'echo "MODE=$MODE"' echo "현재 셸: [$MODE]" # 현재 셸: [] # 조회와 삭제 printenv HOME env | grep GREETING unset GREETING

4.2 Shell: PATH에 디렉터리 추가

shell
mkdir -p "$HOME/bin" printf '#!/bin/sh\necho "my tool"\n' > "$HOME/bin/mytool" chmod +x "$HOME/bin/mytool" export PATH="$HOME/bin:$PATH" mytool # my tool

4.3 TypeScript(Node.js): 환경 변수 읽기와 검증

process.env의 값 타입은 string | undefined이다. 설정을 한 곳에서 읽고 검증하는 패턴이 안전하다.

typescript
// config.ts (실행: DATABASE_URL=postgres://localhost/app PORT=3000 npx tsx config.ts) function requireEnv(name: string): string { const value = process.env[name]; if (value === undefined || value === "") { throw new Error(`필수 환경 변수 ${name}가 설정되지 않았습니다.`); } return value; } function parsePort(raw: string | undefined, fallback: number): number { if (raw === undefined) return fallback; const port = Number(raw); if (!Number.isInteger(port) || port < 1 || port > 65535) { throw new Error(`PORT 값이 올바르지 않습니다: ${raw}`); } return port; } const config = { databaseUrl: requireEnv("DATABASE_URL"), port: parsePort(process.env.PORT, 8080), isProduction: process.env.NODE_ENV === "production", }; console.log(config);

4.4 Shell: .env 파일 로드

shell
# .env 파일 생성 (예시 값) cat > .env <<'EOF' DATABASE_URL=postgres://localhost/app PORT=3000 EOF # 파일의 모든 변수를 export 상태로 읽기 set -a . ./.env set +a sh -c 'echo "$DATABASE_URL $PORT"'

5. 시간 복잡도 또는 성능 특성

  • 환경 변수는 프로세스 시작 시 복사되는 문자열 배열이다. 일반적인 개수에서는 시작 비용이 무시할 만하다.
  • 리눅스에서 getenv()로 조회할 때 glibc는 환경 배열을 앞에서부터 순차 탐색하므로 변수 개수 n에 대해 O(n)이다. 변수 개수가 많지 않아 실무에서 병목이 되는 일은 드물다.
  • 환경 변수 전체와 인자(argv)의 합계 크기에는 커널 제한이 있다. 너무 큰 값을 넣으면 execve가 E2BIG 오류로 실패할 수 있다. 큰 데이터는 파일이나 별도 저장소로 전달하는 편이 맞다.

6. 실무 사용 사례

  • 12-Factor App: 설정을 환경 변수에 저장하라는 원칙이다. 같은 컨테이너 이미지를 환경별 설정만 바꿔 배포할 수 있다.
  • Docker / Kubernetes: docker run -e KEY=value, Compose의 environment, Kubernetes의 env, Secret, ConfigMap으로 주입한다.
  • CI/CD: GitHub Actions 등에서 시크릿(secret)을 환경 변수로 노출해 배포 토큰이나 레지스트리 인증 정보를 전달한다.
  • 런타임 동작 제어: NODE_ENV, TZ(시간대), LANG, HTTP_PROXY/HTTPS_PROXY 등이 있다.
  • 로컬 개발: .env 파일에 개발용 값을 두고 로더로 읽는다. 이 파일은 .gitignore에 추가한다.

7. 주의할 점

  • 비밀 정보 노출: 환경 변수는 암호화되지 않는다. 리눅스에서는 같은 사용자 권한으로 /proc/<pid>/environ을 읽을 수 있고, 크래시 리포트나 디버그 로그에 process.env를 통째로 출력하면 유출된다. 자식 프로세스로도 그대로 상속된다는 점을 기억해야 한다.
  • `.env` 커밋 금지: 실수로 커밋하면 Git 이력에 남는다. 대신 .env.example처럼 키 이름만 담은 템플릿을 저장소에 둔다.
  • 모든 값은 문자열: "false"는 비어 있지 않은 문자열이므로 JavaScript에서 참(truthy)이다. 숫자·불리언은 명시적으로 변환하고 검증한다.
  • 변경은 전파되지 않음: 이미 실행 중인 프로세스의 환경 변수는 바깥에서 바꿀 수 없다. 값을 바꾸면 프로세스를 재시작해야 한다.
  • 따옴표와 공백: 셸에서 $VAR를 따옴표 없이 쓰면 공백 포함 값이 분리된다. "$VAR"처럼 감싼다.
  • `export` 누락: 변수를 설정했는데 프로그램이 못 읽는다면 가장 먼저 export 여부를 확인한다.
  • `sudo`와 환경: sudo는 보안 정책에 따라 일부 환경 변수를 초기화한다. 필요하면 정책 설정을 확인한다.

8. 핵심 정리

  • 환경 변수는 프로세스가 가지는 문자열 이름=값 모음이며, 자식 프로세스에 복사되어 상속된다.
  • 셸 변수는 export해야 환경 변수가 된다. 자식의 변경은 부모에 영향을 주지 않는다.
  • 코드와 설정을 분리하기 위해 사용하며, 환경별 DB 주소나 실행 모드를 코드 수정 없이 바꿀 수 있다.
  • Node.js에서는 process.env로 읽고, 값은 string | undefined이므로 시작 시점에 검증한다.
  • 암호화되지 않으므로 비밀 정보 로그 출력과 .env 커밋을 피하고, 운영 환경에서는 시크릿 관리 기능을 함께 고려한다.