환경변수를 넣었는데 프로세스가 못 읽는 이유: .env·export·실행 시점 구분하기
설정 파일 저장과 셸 변수, 자식 프로세스 환경 상속을 구분한다. Bash·Python·Node.js 합성 테스트 6개로 dotenv 파서 차이, source의 실행 위험, 적용 시점과 안전한 확인 순서를 살펴본다.
설정 파일과 프로세스 사이의 전달 경계를 표현한 생성형 개념 이미지. 실제 실행 화면이 아니다.
환경변수를 파일에 적었는데 개발 도구에서는 값이 보이지 않는다. 반대로 터미널에서는 되는데 백그라운드 작업에서는 실패하기도 한다.
답은 파일 위치만으로 결정되지 않는다. 누가 파일을 읽는지, 어떤 문법으로 읽는지, 어느 프로세스를 언제 시작했는지를 함께 확인해야 한다. 파일 저장, 셸 변수 설정, 자식 프로세스에 환경 전달은 서로 다른 단계다.
.env는 자동으로 적용되는 설정이 아니다
Node.js 공식 문서는 dotenv에 공통의 정식 규격이 없어서 자체 문법을 정의한다고 설명한다. 따라서 확장자가 같아도 모든 프로그램이 같은 방식으로 읽는다고 가정하면 안 된다. 주석, 따옴표, 공백, 변수 확장과 덮어쓰기 정책은 사용하는 로더를 확인해야 한다. Node.js 환경변수 문서
예를 들어 다음 내용은 Node.js의 dotenv 문법에서는 유효하다.
그러나 Bash의 대입문은 등호 양옆에 공백을 넣지 않는다. 같은 파일을 source로 실행하면 같은 의미가 되지 않는다.
특정 프로그램이 허용된 키만 읽도록 만든 전용 설정 파일도 있다. 그런 파일에 개발 도구용 항목을 함께 추가하면 파서가 거부할 수 있다. 이는 모든 dotenv 로더의 동작이 아니라 애플리케이션이 정한 계약이다. 파일을 공유하기 전에 실제 로딩 코드와 허용 키를 먼저 확인한다.
셸에 있는 값과 자식이 받는 값은 다르다
Bash의 export는 변수를 이후 실행되는 명령의 환경으로 전달하도록 표시한다. 변수에 값을 넣는 것만으로는, 새로 만든 일반 셸 변수가 외부 프로세스에 전달되지 않는다. Bash Environment
다음은 비밀값 없는 Bash·Python 예제다. 괄호 안에서 실행하므로 바깥 셸의 DEMO_MODE를 바꾸지 않는다.
실행 결과는 순서대로 missing, local이다.
한 명령에만 전달하려면 앞에 대입문을 붙일 수 있다.
이 방식은 이후의 모든 프로그램에 변수를 남기지 않고 해당 실행에 범위를 좁힌다. 실제 비밀값을 명령줄에 직접 적는 예제로 바꾸지는 않는다. 셸 기록이나 작업 로그에 남을 수 있다.
source는 파일 읽기가 아니라 코드 실행이다
Bash의 점 명령과 source는 현재 셸 문맥에서 파일의 명령을 실행한다. 단순한 키·값 파서가 아니다. 파일에 명령 치환이나 다른 명령이 들어 있다면 그것도 실행 대상이다. 신뢰할 수 없는 파일을 source하거나 eval로 해석하는 방법을 dotenv 로딩의 대안으로 삼으면 안 된다. Bash 내장 명령
데이터 형식의 설정 파일이라면 해당 프로그램이 제공하는 로더를 우선 사용한다. 셸 스크립트로 관리하기로 한 파일은 별도로 구분하고 내용과 수정 권한을 검토한다.
아래는 직접 만든 임시 파일을 Node.js 로더에 전달하는 예제다. Bash와 --env-file을 지원하는 Node.js에서 실행한다. 이 글에서는 Node.js 24.22.1을 사용했다.
출력은 demo다. Node.js에서는 이미 존재하는 환경변수가 파일값보다 우선하므로, 예제는 DEMO_MODE를 먼저 unset했다. Node.js --env-file 문서
이는 Node.js의 로더 동작을 확인하는 예제이지, 임의의 dotenv 구현이나 셸에서 같은 파일이 통한다는 보장은 아니다.
이미 실행 중인 프로세스에는 소급 적용되지 않는다
부모가 자식을 시작할 때 환경을 전달한다. 나중에 부모 셸에서 export를 해도 기존 자식의 환경을 자동으로 바꾸지는 않는다. 파일을 수정했다는 사실만으로 실행 중인 프로그램의 설정이 바뀌는 것도 아니다.
Python의 subprocess는 기본적으로 현재 프로세스의 환경을 상속하며, env 인수를 주면 새 프로세스에 전달할 환경을 명시할 수 있다. 다음 코드는 비밀값 없이 상속 시점을 확인한다. Python subprocess 문서
결과는 old다. 자식이 실행된 뒤 Python 딕셔너리를 바꾸어도 자식에게 전달된 환경은 바뀌지 않았다. 프로그램이 별도의 설정 다시 읽기 기능을 제공한다면 그 기능은 따로 검증해야 한다.
실제로 확인한 범위
Linux 로컬 환경의 Bash 5.2.37, Python 3.12.14, Node.js 24.22.1에서 합성값만 사용해 여섯 항목을 검사했다.
- export 없는 셸 변수는 Python 자식에게 전달되지 않았다.
- export 후 실행한 자식은 값을 받았다.
- 명령 앞의 대입은 해당 명령에 전달됐고 바깥 셸에는 남지 않았다.
- Node.js 로더는 공백을 포함한 예제 dotenv 대입을 읽었다.
- 같은 대입문을 Bash source로 읽으면 실패했다.
- 이미 실행한 자식은 이후 바꾼 딕셔너리의 새 값을 받지 않았다.
이는 위 예제에 대한 실행 결과다. 특정 개발 도구, 운영 서버, IDE 또는 서비스 관리자의 설정 로딩까지 검증한 결과는 아니다.
문제를 찾는 순서
실무에서는 다음 순서로 좁히면 된다.
- 실제 읽는 주체를 찾는다. 파일을 직접 여는 코드인지, dotenv 라이브러리인지, 부모가 주는 환경을 읽는지 확인한다.
- 실행 경로와 시점을 확인한다. 터미널, IDE, 서비스가 같은 부모 환경에서 시작됐다고 가정하지 않는다.
- 문법과 우선순위를 확인한다. 허용 키, 따옴표, 중복 키, 기존 환경과 파일 값 중 어느 것이 우선인지 확인한다.
- 목표 프로세스에서 존재 여부만 검사한다. 환경 전체나 키 값을 출력하지 않는다.
- 필요한 실행만 다시 시작한다. 저장한 파일과 새 프로세스의 로딩 결과를 함께 확인한다.
Bash 시작 파일도 실행 방식에 따라 다르다. 대화형 비로그인 셸의 .bashrc 설정이 모든 서비스나 GUI 앱에 자동으로 전달되는 것은 아니다. 서비스와 IDE는 각 실행기의 공식 환경 설정 경로를 확인해야 한다. Bash Startup Files
존재 여부 확인은 실제 애플리케이션 코드나 같은 실행 경로의 진단 지점에 넣어야 한다. 별도 터미널에서만 검사하면 그 터미널의 상태를 본 것에 그친다.
환경변수 문제의 핵심은 “어느 파일에 넣을까”에서 끝나지 않는다. 저장된 설정이 어떤 로더를 거쳐 어느 새 프로세스에 전달됐는가까지 확인해야 해결된다.