레이블이 개발도구/환경인 게시물을 표시합니다. 모든 게시물 표시
레이블이 개발도구/환경인 게시물을 표시합니다. 모든 게시물 표시

금요일

.env 파일인데 환경변수가 undefined — dotenv 로딩 에러 완전 해결

🔍 검색 키워드: process.env undefined, dotenv not working, .env 파일 적용 안됨, python-dotenv not loading, 환경변수 undefined, docker 환경변수 안됨, next.js env not working

상황

.env 파일을 분명히 만들었는데 코드에서 undefined가 찍힌다.

# .env
DATABASE_URL=postgresql://localhost/mydb
SECRET_KEY=mysecretkey
console.log(process.env.DATABASE_URL); // undefined ???

또는 Python에서:

import os
print(os.getenv("SECRET_KEY"))  # None ???

배포 환경에서만 안 된다거나, Docker 안에서만 안 된다거나, 로컬은 되는데 서버에선 안 된다거나. 패턴은 다양하지만 원인은 몇 가지로 좁혀진다.


Node.js — dotenv 관련

원인 1: dotenv를 아예 안 불렀거나 너무 늦게 불렀다

// 잘못된 예 — DB 모듈보다 나중에 dotenv 로드
const db = require('./database'); // 이미 process.env 읽음
require('dotenv').config();       // 너무 늦었다
// 올바른 예 — 진입점 파일 최상단에서 먼저
require('dotenv').config();

const db = require('./database');
const app = require('./app');

TypeScript / ES Modules:

import 'dotenv/config'; // 이 방법이 제일 깔끔하다

// 또는
import dotenv from 'dotenv';
dotenv.config();

import { createConnection } from './db';

원인 2: .env 파일 경로가 다르다

기본적으로 dotenvprocess.cwd() 기준으로 .env를 찾는다. 실행 위치가 프로젝트 루트가 아니면 못 찾는다.

import path from 'path';
import dotenv from 'dotenv';

dotenv.config({
  path: path.resolve(__dirname, '../.env'),
});

원인 3: .env 파일이 .gitignore에 있고 서버에 없다

로컬에서만 됐던 이유가 이거다. .env는 보통 .gitignore에 들어가 있어서 서버에 배포가 안 된다.

# GitHub Actions — 시크릿 주입
steps:
  - name: Create .env file
    run: |
      echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env
      echo "SECRET_KEY=${{ secrets.SECRET_KEY }}" >> .env

원인 4: 변수명에 공백이나 따옴표 문제

# 잘못된 .env
DATABASE_URL = postgresql://localhost/mydb   # = 주변 공백 금지

# 올바른 .env
DATABASE_URL=postgresql://localhost/mydb
SECRET_KEY=mysecret
APP_NAME="My App"  # 값에 공백이 필요하면 따옴표 사용

Python — python-dotenv

from dotenv import load_dotenv
import os

# .env 파일 로드 (현재 디렉토리 기준)
load_dotenv()

# 또는 경로 명시
load_dotenv(dotenv_path='/path/to/.env')

# 이미 설정된 환경변수를 덮어쓰려면
load_dotenv(override=True)

print(os.getenv("DATABASE_URL"))
💡 load_dotenv()는 이미 시스템에 설정된 환경변수는 덮어쓰지 않는다. 테스트 환경에서 .env 값으로 강제하려면 override=True를 써야 한다.
# Django settings.py
import os
from pathlib import Path
from dotenv import load_dotenv

BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.getenv('DB_NAME'),
        'USER': os.getenv('DB_USER'),
        'PASSWORD': os.getenv('DB_PASSWORD'),
        'HOST': os.getenv('DB_HOST', 'localhost'),
        'PORT': os.getenv('DB_PORT', '5432'),
    }
}

Next.js / React — 프레임워크별 규칙

Next.js는 dotenv를 직접 쓰지 않고 자체 env 로딩 시스템을 쓴다.

  • 서버에서만 쓰는 변수: DATABASE_URL=value
  • 클라이언트(브라우저)에서도 쓰는 변수: NEXT_PUBLIC_API_URL=value (반드시 NEXT_PUBLIC_ 접두사)
// 서버 컴포넌트 / API Route에서만 접근 가능
process.env.DATABASE_URL // ✅

// 클라이언트에서 접근하려면 NEXT_PUBLIC_ 접두사 필수
process.env.NEXT_PUBLIC_API_URL // ✅
process.env.API_URL // 클라이언트에서는 undefined ❌

환경 파일 우선순위:

.env.local > .env.development.local > .env.development > .env
⚠️ NEXT_PUBLIC_ 변수는 빌드 시 정적으로 교체된다. 빌드 후 값을 바꿔도 소용없다. 반드시 빌드 전에 설정해야 한다.

Docker / Docker Compose

# docker-compose.yml
services:
  app:
    build: .
    # 방법 1: env_file 지정 (권장)
    env_file:
      - .env

    # 방법 2: 직접 명시
    environment:
      - DATABASE_URL=postgresql://db:5432/mydb
      - SECRET_KEY=${SECRET_KEY}  # 호스트 환경변수에서
# Docker run
docker run --env-file .env myapp
docker run -e DATABASE_URL=xxx -e SECRET_KEY=yyy myapp
⚠️ Dockerfile에서 COPY .env .를 하면 이미지에 시크릿이 박힌다. .dockerignore.env를 넣고, 런타임에 주입하는 방식을 써야 한다.
# .dockerignore
.env
.env.*

상황별 체크리스트

증상원인확인/해결
로컬만 됨, 서버 안 됨.env가 서버에 없음CI/CD 시크릿 주입 또는 서버에 .env 생성
dotenv.config() 했는데 undefinedimport 순서 문제진입점 최상단에서 dotenv 먼저 로드
특정 변수만 undefined변수명 오타, 공백.env 파일 문법 확인
Docker 안에서 undefinedenv_file 미설정env_file 또는 -e 옵션으로 주입
Next.js 클라이언트에서 undefinedNEXT_PUBLIC_ 접두사 누락접두사 추가 후 재빌드
Python에서 Noneload_dotenv() 미호출import 후 load_dotenv() 호출
기존 시스템 변수가 우선override 미설정load_dotenv(override=True)

디버깅 팁

// 로드된 환경변수 키 목록 확인
console.log('ENV keys:', Object.keys(process.env).filter(k => !k.startsWith('npm_')));

// .env 경로 확인
const path = require('path');
console.log('Looking for .env at:', path.resolve(process.cwd(), '.env'));
# .env 파일에서 읽은 값만 확인 (시스템 변수 제외)
from dotenv import dotenv_values
config = dotenv_values(".env")
print(config)

마무리

환경변수 에러는 대부분 세 가지 중 하나다: .env 파일이 없거나, dotenv 로드를 너무 늦게 했거나, 프레임워크별 규칙을 무시했거나.

서버에 배포했을 때 갑자기 안 된다면 .env가 서버에 실제로 존재하는지부터 확인하고, Docker면 --env-file이나 env_file로 주입됐는지 확인한다. Next.js면 클라이언트에서 쓰는 변수에 NEXT_PUBLIC_ 붙이고 재빌드. 이 세 가지가 90%다.

npm install이 안 된다고? 원인 파악 없이 --force 치지 마라

npm install 에러 npm ERR! code ERESOLVE node_modules 삭제 후 재설치 npm ci 차이 package-lock.json 충돌 peer dependency 에러

프로젝트 클론하고 npm install 한 번에 되면 그날은 운이 좋은 날이다. 실무에서 이게 한 번에 되는 경우가 얼마나 되냐면, 팀이 클수록, 프로젝트 오래될수록 확률이 줄어든다.

문제는 에러 메시지를 제대로 읽지 않고 npm install --force 또는 npm install --legacy-peer-deps를 무지성으로 치는 경우다. 이러면 당장은 되는 것처럼 보이지만 나중에 런타임에서 이상한 에러로 돌아온다. 진짜 실무자는 에러 메시지를 읽는다.

에러 메시지별 원인과 해결

ERESOLVE: peer dependency 충돌

npm ERR! code ERESOLVE
npm ERR! ERESOLVE unable to resolve dependency tree
npm ERR!
npm ERR! While resolving: myapp@1.0.0
npm ERR! Found: react@18.2.0
npm ERR!   react@"^18.2.0" from the root project
npm ERR!
npm ERR! Could not resolve dependency:
npm ERR! peer react@"^17.0.0" from some-library@2.1.0

이 에러는 some-library가 React 17을 요구하는데 프로젝트에는 React 18이 깔려 있다는 뜻이다. npm 7 버전부터 peer dependency를 엄격하게 검사하기 시작해서 이 에러가 갑자기 늘었다.

선택지는 세 가지다.

1. 라이브러리를 업데이트한다 (가장 좋음)

npm install some-library@latest

해당 라이브러리가 최신 버전에서 React 18을 지원하면 끝난다. 이게 제일 먼저 시도해야 할 옵션이다.

2. --legacy-peer-deps 옵션 (차선책)

npm install --legacy-peer-deps

npm 6 시절 방식으로 peer dependency를 그냥 무시하고 설치한다. 대부분의 경우 문제없이 동작하지만, 진짜 비호환 라이브러리가 섞이면 런타임 에러가 난다. 팀 전체가 이 옵션을 공유한다면 .npmrc에 박아두는 게 낫다.

# .npmrc
legacy-peer-deps=true

3. --force는 마지막 수단

npm install --force

--force는 캐시도 무시하고 버전 충돌도 무시하고 그냥 밀어붙인다. 개발 의존성이나 급할 때 쓰는 거고, CI/CD에서 이걸 쓰고 있다면 뭔가 잘못된 거다.

ENOENT: node_modules 꼬임

npm ERR! code ENOENT
npm ERR! syscall open
npm ERR! path /project/node_modules/.package-lock.json
npm ERR! errno -2
npm ERR! enoent ENOENT: no such file or directory

node_modules가 중간에 망가진 경우다. 삭제하고 다시 설치하면 된다.

# node_modules 통째로 삭제
rm -rf node_modules package-lock.json

# 클린 설치
npm install

Windows라면:

Remove-Item -Recurse -Force node_modules
Remove-Item package-lock.json
npm install

그냥 node_modules만 지우고 재설치하면 되는데, package-lock.json도 같이 지우는 게 나을 때가 있다. package-lock.json이 실제 package.json과 어긋나 있는 경우에 이 에러가 나기도 해서다. 단, package-lock.json을 지우면 의존성 버전이 최신으로 바뀔 수 있으니 팀 공유 프로젝트에선 조심해야 한다.

EACCES: 권한 문제

npm ERR! code EACCES
npm ERR! syscall mkdir
npm ERR! path /usr/local/lib/node_modules
npm ERR! errno -13
npm ERR! Error: EACCES: permission denied

글로벌 설치할 때 자주 나온다. sudo npm install -g로 해결했다면 잠깐은 괜찮지만, 이 방식이 습관되면 나중에 권한 문제가 더 복잡해진다. 올바른 방법은 npm 기본 디렉토리를 사용자 홈으로 옮기는 거다.

# npm 글로벌 디렉토리를 홈 폴더로 변경
mkdir ~/.npm-global
npm config set prefix '~/.npm-global'

# PATH에 추가 (~/.zshrc 또는 ~/.bashrc)
export PATH=~/.npm-global/bin:$PATH

# 적용
source ~/.zshrc

ETIMEDOUT / ECONNRESET: 네트워크 이슈

npm ERR! code ETIMEDOUT
npm ERR! errno ETIMEDOUT
npm ERR! network request to https://registry.npmjs.org/lodash failed

회사 네트워크나 VPN 환경에서 자주 나온다. 몇 가지 확인 포인트:

# npm 레지스트리 확인
npm config get registry

# 회사 사설 레지스트리 쓰고 있다면
npm config set registry https://your-company-registry.com/

# 원복
npm config set registry https://registry.npmjs.org/

# 프록시 환경이면
npm config set proxy http://proxy.company.com:8080
npm config set https-proxy http://proxy.company.com:8080

타임아웃 에러는 단순히 npm 서버 응답이 느린 경우도 있어서 재시도 먼저 해본다. 그래도 계속 나오면 레지스트리 설정 확인.

npm install vs npm ci — 이 차이 모르는 사람 많다

CI/CD 파이프라인에서 npm install을 쓰고 있다면 npm ci로 바꿔라.

구분npm installnpm ci
package-lock.json 없으면새로 생성에러로 중단
버전 범위(^, ~)최신 버전으로 설치 가능lock 파일 버전 그대로
node_modules있으면 그대로 두고 업데이트무조건 지우고 재설치
용도개발 환경CI/CD, 배포

npm installpackage.json의 버전 범위 안에서 최신 버전을 가져올 수 있다. "react": "^18.0.0"이면 18.x 최신을 잡는다는 얘기다. CI에서 이걸 쓰면 빌드할 때마다 버전이 달라질 수 있다. npm cipackage-lock.json에 적힌 정확한 버전만 설치한다. 재현 가능한 빌드를 원한다면 CI에서 npm ci가 맞다.

자주 보는 실수 모음

package-lock.json을 .gitignore에 넣는 경우

가끔 이렇게 된 프로젝트가 있다. package-lock.json은 반드시 커밋해야 한다. 이게 없으면 팀원마다 설치되는 패키지 버전이 달라지고, "내 로컬에선 되는데 왜 CI가 터지냐"는 상황이 만들어진다.

npm 버전이 팀마다 다른 경우

프로젝트 루트에 .nvmrcpackage.jsonengines 필드로 버전 명시해두는 습관을 들이자.

{
  "engines": {
    "node": ">=18.0.0",
    "npm": ">=9.0.0"
  }
}

yarn/pnpm 섞어쓰기

package-lock.json(npm), yarn.lock(yarn), pnpm-lock.yaml(pnpm)이 동시에 존재하는 프로젝트가 있다. 이 상태로 팀에서 사람마다 다른 패키지 매니저 쓰면 lock 파일 충돌 지옥이 된다. package.jsonpackageManager 필드로 통일해두자.

{
  "packageManager": "npm@10.2.0"
}

정리

npm 에러는 대부분 세 가지 중 하나다: peer dependency 충돌, node_modules 꼬임, 네트워크/권한 문제. 에러 메시지 첫 줄에 나오는 code를 읽으면 원인이 나온다. --force는 진짜 마지막 수단이고, CI에선 npm ci 써라.

Git Merge Conflict 완전 정복 — 겁먹지 말고 읽어봐라

🔍 검색 키워드: git merge conflict 해결, git 충돌 해결, merge conflict 뜨는 이유, rebase conflict, git pull 충돌, git 머지 에러

왜 merge conflict가 생기는가

두 사람이 같은 파일의 같은 줄을 각자 다르게 수정하면 Git은 어느 쪽을 선택해야 할지 모른다. 그래서 멈추고 사람한테 결정을 넘긴다. 그게 전부다. 무서운 게 없다.

레벨 1 — 기초: 충돌 마커 읽기

충돌이 생기면 Git은 파일 안에 이런 마커를 심어준다.

<<<<<<< HEAD
const greeting = "안녕하세요";  // 내 변경사항
=======
const greeting = "Hello";       // 상대방 변경사항
>>>>>>> feature/english-greeting
  • <<<<<<< HEAD ~ ======= : 현재 브랜치(내 것)
  • ======= ~ >>>>>>> : 병합 대상 브랜치(상대 것)

해결 방법은 단순하다. 둘 중 하나를 고르거나, 둘 다 합치거나, 완전히 새로 쓰거나. 마커 3개(<<<<<<<, =======, >>>>>>>)를 모두 제거하고 원하는 최종 코드만 남기면 된다.

# 충돌 파일 확인
git status

# 수동 편집 후
git add src/greeting.js
git commit

레벨 2 — 실무: 자주 마주치는 상황별 대처

상황 1: git pull 했더니 충돌 폭탄

git pull origin main
# CONFLICT (content): Merge conflict in src/api.js
# Automatic merge failed; fix conflicts and then commit the result.
단계명령어설명
1git status충돌 파일 목록 확인
2에디터에서 파일 열기마커 찾아서 수동 편집
3git add <파일>해결된 파일 스테이징
4git commit머지 커밋 생성 (메시지 자동 입력됨)

상황 2: merge 중 충돌, 그냥 포기하고 싶을 때

git merge --abort

--abort 하면 merge 시작 전 상태로 되돌아간다. 깔끔하게 포기할 수 있다.

상황 3: rebase 중 충돌

git rebase main
# CONFLICT (content): Merge conflict in src/user.js

rebase는 커밋 하나씩 재적용하기 때문에 충돌도 커밋 단위로 난다.

# 각 충돌 해결 후
git add src/user.js
git rebase --continue  # 다음 커밋으로 진행

# 포기할 때
git rebase --abort

상황 4: 특정 파일을 그냥 한 쪽으로 덮어쓰고 싶을 때

# 내 것(HEAD) 으로 덮어쓰기
git checkout --ours src/config.js

# 상대 브랜치 것으로 덮어쓰기
git checkout --theirs src/config.js

git add src/config.js

레벨 3 — 고급: 도구 활용 및 예방

VS Code에서 충돌 해결

VS Code는 충돌 파일에 시각적 버튼을 표시해준다.

  • Accept Current Change → HEAD 것 선택
  • Accept Incoming Change → 병합 대상 것 선택
  • Accept Both Changes → 둘 다 유지
  • Compare Changes → diff 보기

터미널보다 훨씬 빠르다. 파일이 많을 때 특히 유용하다.

git mergetool 사용

# VS Code를 mergetool로 설정
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait $MERGED'
git mergetool

Python 프로젝트: requirements.txt 충돌

<<<<<<< HEAD
requests==2.28.0
=======
requests==2.31.0
>>>>>>> feature/update-deps

보통 높은 버전을 선택하되, 호환성 깨지는 메이저 버전 업이면 테스트 먼저.

Node.js: package-lock.json 충돌

package-lock.json 충돌은 직접 해결하려 하지 말고 이게 낫다.

# 충돌난 package-lock.json 삭제 후 재생성
git checkout --theirs package-lock.json
npm install

# 또는 아예 새로 생성
rm package-lock.json
npm install
git add package-lock.json

Java/Spring: application.yml 충돌

server:
  port: 8080         # <-- HEAD vs 8081 충돌 예시
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dev_db

환경별 설정이 섞이는 케이스. application-dev.yml, application-prod.yml로 분리하면 근본 해결된다.

Nginx 설정 파일 충돌

location /api {
    proxy_pass http://localhost:3000;
    proxy_set_header X-Real-IP $remote_addr;  # incoming 것 추가
}

두 변경사항을 합치는 게 맞다. 헤더 추가된 버전에 로컬 주소를 넣거나, 환경변수로 빼거나.

충돌 자주 나는 상황 진단표

상황원인예방법
pull할 때마다 충돌브랜치가 너무 오래됨자주 rebase/merge
같은 파일 반복 충돌역할 분리 안 됨파일 소유권 명확히
package-lock.json 항상 충돌여러 명이 npm installCI에서만 lock 갱신
config 파일 충돌환경 설정 공유환경별 파일 분리
이진 파일(이미지 등) 충돌.gitattributes 없음이진 파일 전략 설정

.gitattributes로 이진 파일 전략 설정

# 이진 파일은 충돌 없이 무조건 덮어쓰기
*.png binary
*.jpg binary
*.pdf binary

# package-lock.json은 theirs 전략
package-lock.json merge=theirs

실수하기 쉬운 것들

1. 마커 제거 안 하고 커밋

# 커밋 전 마커 잔존 여부 확인
grep -r "<<<<<<< " src/

CI에 충돌 마커 검사 스텝 추가해두면 실수 방지된다.

2. rebase 후 force push

rebase 완료 후 원격에 올릴 때는 --force 대신 이걸 써라.

git push --force-with-lease origin feature/my-branch

남이 push한 게 있으면 실패해서 안전하다.

요약

  • 충돌 마커(<<<<<<<, =======, >>>>>>>) 3개 찾아서 제거하고 원하는 코드 남기면 끝
  • 포기하고 싶으면 git merge --abort 또는 git rebase --abort
  • VS Code mergetool 설정해두면 시간 절약
  • package-lock.json은 직접 고치지 말고 재생성
  • 충돌 자주 나면 브랜치 수명을 짧게 가져가는 게 근본 해결책