46장. 환경변수와 보안의 기본
![[9부] 환경변수와 보안의 기본](https://images.tilnote.io/pages/cf4d2a73-c1ed-4f2e-9cd2-cbb40cf5482d.webp)
![[9부] 환경변수와 보안의 기본](https://images.tilnote.io/pages/a4775801-e3fc-4ec3-bc61-126d59635e32.webp)
![[9부] 환경변수와 보안의 기본](https://images.tilnote.io/pages/4e704633-8328-4fde-81d0-b27bb359da5b.webp)
배포 직전, 가장 위험한 실수는 기능 부족이 아니다
서비스를 인터넷에 올리기 직전, 초보자가 가장 많이 신경 쓰는 것은 보통 화면입니다. 버튼 색이 어색하지 않은지, 글쓰기 기능이 잘 되는지, 모바일에서도 깨지지 않는지 같은 것들 말이죠. 물론 그것들도 중요합니다. 하지만 배포 직전 진짜로 더 위험한 문제는 따로 있습니다. 바로 비밀 정보가 코드에 섞여 있는 상태입니다.
회원가입이 아직 조금 투박한 것은 다음 배포에서 고칠 수 있습니다. 하지만 API 키, 데이터베이스 비밀번호, 서비스 시크릿이 공개 저장소나 브라우저 코드에 노출되면 이야기가 달라집니다. 한 번 새어 나간 비밀은, 마치 이미 복사된 집 열쇠처럼 되돌리기 어렵습니다. 그래서 이 장에서는 "코드를 잘 짜는 법"보다 먼저, 비밀을 코드 밖으로 빼는 법을 익히겠습니다.
환경변수는 무엇인가
환경변수는 프로그램이 실행될 때 바깥에서 주입받는 설정값입니다. 말이 조금 딱딱해 보이지만, 쉽게 생각하면 됩니다. 요리책에는 "간장 2큰술"이 적혀 있어도, 냉장고 비밀번호까지 적지는 않죠. 레시피와 비밀금고 비밀번호는 보관 위치가 달라야 합니다.
웹 서비스에서도 비슷합니다. 코드에는 기능과 흐름을 적고, 민감한 값은 환경변수로 분리합니다. 예를 들어 이런 값들이 대표적입니다.
데이터베이스 접속 주소
로그인 토큰을 서명하는 시크릿 값
OpenAI나 외부 결제 서비스의 API 키
관리자 권한이 들어 있는 서버 전용 키
반대로 모든 값이 비밀인 것은 아닙니다. 공개되어도 괜찮은 서비스 URL, 공개용 분석 ID, 브라우저에서 써도 되는 공개 키는 성격이 다릅니다. 핵심은 아주 단순합니다. "사용자 브라우저가 알아도 되는 값인가?" 이 질문에 "아니오"라면 코드에 직접 넣지 말고 환경변수로 분리해야 합니다.
Next.js에서 꼭 기억할 한 가지 규칙
Next.js에서는 환경변수를 꽤 편하게 쓸 수 있습니다. .env 계열 파일의 값을 process.env로 읽어올 수 있고, 기본 템플릿은 이런 파일이 Git에 올라가지 않도록 처리해 둡니다.1 여기서 초보자가 반드시 기억해야 할 규칙은 딱 하나입니다.
NEXT_PUBLIC_가 붙은 변수는 브라우저로 나갑니다.
즉, 이름 앞에 NEXT_PUBLIC_를 붙이면 클라이언트 코드에서도 읽을 수 있게 되고, 그 값은 빌드 시점에 프론트엔드 번들 안으로 들어갑니다.1 다시 말해 사용자가 개발자 도구를 열거나, 번들 파일을 분석하거나, 네트워크 흐름을 살피면 확인할 수 있는 값이라는 뜻입니다.
그래서 다음처럼 나누면 됩니다.
NEXT_PUBLIC_APP_URL같은 값은 공개 가능NEXT_PUBLIC_SUPABASE_ANON_KEY같은 공개용 키는 사용 가능DATABASE_URL,OPENAI_API_KEY,SUPABASE_SERVICE_ROLE_KEY같은 값은 절대 공개 금지
여기서 특히 중요한 점이 하나 더 있습니다. NEXT_PUBLIC_ 변수는 빌드할 때 값이 박혀 들어가기 때문에, 배포 후에 값을 바꾸더라도 다시 빌드하지 않으면 반영되지 않을 수 있습니다.1 초보자 입장에서는 이 부분이 꽤 헷갈립니다. "분명 대시보드에서 바꿨는데 왜 화면에서 예전 값이 보이지?" 같은 일이 생기는 이유가 바로 이것입니다.
우리 서비스에서는 무엇을 공개하고 무엇을 숨길까
이 책에서 만드는 풀스택 서비스 기준으로 보면, 환경변수는 대략 이렇게 나눌 수 있습니다.
| 변수 예시 | 공개 여부 | 이유 |
|---|---|---|
NEXT_PUBLIC_APP_URL | 공개 가능 | 사이트 주소는 숨길 대상이 아니다 |
NEXT_PUBLIC_SUPABASE_URL | 공개 가능 | 브라우저에서 접속해야 하는 엔드포인트다 |
NEXT_PUBLIC_SUPABASE_ANON_KEY | 공개 가능 | 클라이언트용 키다. 단, 권한 정책이 안전해야 한다 |
DATABASE_URL | 비공개 | DB 접속 정보는 서버만 알아야 한다 |
SUPABASE_SERVICE_ROLE_KEY | 비공개 | 강한 권한을 가지므로 절대 클라이언트에 두면 안 된다 |
OPENAI_API_KEY | 비공개 | 비용과 권한이 걸린 서버 전용 비밀이다 |
여기서 anon key라는 이름 때문에 "익명이니까 아무렇게나 써도 되겠네?" 하고 오해하면 안 됩니다. 이 키는 브라우저에서 사용하도록 설계된 공개용 키일 뿐, 아무 제약 없이 다 열어둬도 된다는 뜻은 아닙니다. 실제 안전성은 데이터베이스 권한 정책과 RLS 같은 설정이 함께 받쳐줘야 합니다.
가장 실용적인 파일 구성
초보자에게는 아래 조합이 가장 실용적입니다.
.env.local
내 컴퓨터에서 실제로 사용하는 비밀값을 넣는 파일입니다. 이 파일은 저장소에 올리지 않습니다.1
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=public-anon-key
DATABASE_URL=postgresql://...
SUPABASE_SERVICE_ROLE_KEY=service-role-key
OPENAI_API_KEY=sk-....env.example
팀원이나 미래의 나를 위한 안내서입니다. 실제 값은 비워 두고, 어떤 키가 필요한지만 적어 둡니다.2
NEXT_PUBLIC_APP_URL=
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
DATABASE_URL=
SUPABASE_SERVICE_ROLE_KEY=
OPENAI_API_KEY=이 패턴의 장점은 아주 큽니다. 누군가 프로젝트를 처음 받았을 때 "무슨 키가 필요한지" 한눈에 알 수 있고, 실수로 진짜 시크릿을 저장소에 올릴 가능성도 줄어듭니다.
배포할 때는 저장소가 아니라 배포 플랫폼에 넣는다
로컬에서는 .env.local을 쓰고, 배포할 때는 Vercel 같은 배포 플랫폼의 환경변수 설정 화면에 값을 넣는 방식이 가장 안전합니다. 개발 환경, 미리보기 환경, 실제 운영 환경의 값을 따로 관리하면 더 좋습니다. 개발용 키와 운영용 키를 분리해 두면, 실험하다가 운영 데이터를 건드리는 사고를 줄일 수 있습니다.2
중요한 원칙은 하나입니다. 진짜 비밀값은 GitHub 저장소가 아니라 실행 환경에 둔다는 것입니다. 저장소는 설계도를 올리는 곳이고, 비밀금고는 따로 있어야 합니다.
초보자가 자주 하는 위험한 실수
환경변수는 개념 자체는 단순하지만, 실수는 정말 쉽게 납니다. 특히 아래 실수는 자주 등장합니다.
비밀 키를 코드에 직접 적는다
가장 흔한 실수입니다.
const apiKey = "sk-live-..."처음에는 편합니다. 복사해서 붙여 넣으면 바로 동작하니까요. 하지만 이 순간부터 키는 코드 파일, 커밋 기록, PR 화면, 화면 공유, 심지어 블로그 예제까지 따라다닐 수 있습니다.
비밀 키에 NEXT_PUBLIC_를 붙인다
브라우저에서 접근이 안 되니 접두어만 붙이면 되겠지, 하고 생각하기 쉽습니다. 하지만 그건 "문이 안 열리니 벽을 허문다"에 가깝습니다. NEXT_PUBLIC_OPENAI_API_KEY 같은 이름은 거의 "이 비밀을 세상에 공개합니다"라고 쓰는 것과 비슷합니다.1
클라이언트 컴포넌트에서 서버 전용 값을 쓰려 한다
"use client"가 붙은 컴포넌트에서 DATABASE_URL이나 SUPABASE_SERVICE_ROLE_KEY를 읽으려 하면, 보통은 undefined가 나오거나 잘못된 흐름으로 이어집니다.1 이때 해결책은 공개 접두어를 붙이는 것이 아니라, 서버 함수나 API 라우트로 책임을 옮기는 것입니다.
AI 채팅창에 진짜 키를 통째로 붙여 넣는다
Claude Code나 Codex에게 도움을 받다 보면, 오류를 빨리 해결하고 싶어서 .env.local 전체를 복사해 넣고 싶은 유혹이 생깁니다. 하지만 그건 아주 나쁜 습관입니다. AI에게는 실제 값 대신 변수 이름과 형태만 알려주면 충분한 경우가 대부분입니다.
예를 들어 이렇게 말하면 됩니다.
OPENAI_API_KEY,SUPABASE_SERVICE_ROLE_KEY를 쓰는 Next.js 프로젝트인데, 서버 액션에서만 읽도록 구조를 바꾸고 싶어.
이렇게 하면 실제 키를 노출하지 않고도 충분히 도움을 받을 수 있습니다.
자동화 환경에서 API 키를 공개 영역에 둔다
Codex 같은 도구를 자동화나 CI/CD에서 쓸 때는 API 키 인증을 사용하게 되는데, 이런 키는 반드시 환경변수나 시크릿 저장소로 주입해야 합니다. 공식 문서도 프로그램 방식의 Codex 워크플로에서는 API 키 인증을 권장하고, 신뢰할 수 없는 공개 환경에 Codex 실행을 노출하지 말라고 안내합니다.3
값이 없으면 조용히 지나가지 말고, 시작할 때 바로 잡아내기
환경변수 문제는 이상하게도 "처음에는 멀쩡해 보이다가, 사용자가 특정 버튼을 눌렀을 때만 터지는" 경우가 많습니다. 그래서 더 무섭습니다.
가장 좋은 습관은 앱이 시작될 때 필요한 환경변수가 있는지 먼저 검사하는 것입니다.2 초보자라면 거창한 도구 없이도 간단히 시작할 수 있습니다.
const requiredEnv = [
"DATABASE_URL",
"SUPABASE_SERVICE_ROLE_KEY",
"OPENAI_API_KEY",
]
for (const key of requiredEnv) {
if (!process.env[key]) {
throw new Error(`${key} 환경변수가 없습니다.`)
}
}이렇게 해 두면, 서비스가 한참 돌아가다가 이상한 곳에서 무너지는 대신, 시작할 때 바로 "뭐가 비어 있는지" 알려줍니다. 초보자에게는 이런 조기 실패가 오히려 친절합니다.
보안을 지키는 작은 습관이 큰 사고를 막는다
보안은 거대한 해킹 영화가 아닙니다. 대부분은 생활 습관에 가깝습니다. 아래 체크리스트를 배포 전에 한 번씩만 확인해도 사고 가능성이 크게 줄어듭니다.
.env.local과 실제 시크릿 파일은 절대 저장소에 커밋하지 않는다.1공개용 값과 비공개 값을 이름부터 구분한다.
NEXT_PUBLIC_는 공개 선언이라고 생각한다.1개발용 키와 운영용 키를 분리한다.2
에러 로그, 스크린샷, 화면 공유에 키가 보이지 않는지 확인한다.
AI에게 코드를 보여줄 때도 실제 시크릿은 가리고 보낸다.
운영에 쓰는 키는 가능한 권한을 최소화한다. 읽기만 필요한데 관리자 키를 쓰지 않는다.
이 습관들은 화려하지 않습니다. 하지만 보안은 원래 화려한 기술보다, 지루할 정도로 반복되는 기본기에서 강해집니다.
GitHub는 안전벨트가 될 수 있다
아무리 조심해도 실수는 납니다. 그래서 GitHub의 보안 기능을 켜 두는 것이 좋습니다. GitHub의 secret scanning은 커밋이나 저장소 안에서 노출된 비밀값을 감지해 경고해 줄 수 있습니다.4 물론 이 기능이 모든 문제를 해결해 주는 것은 아닙니다. 경고를 받았다고 끝이 아니라, 유출된 키를 폐기하고 새로 발급하는 것이 핵심입니다.4
즉, 비밀이 노출되었을 때의 올바른 순서는 이렇습니다.
노출된 키를 즉시 비활성화하거나 재발급한다.
코드와 환경설정에서 새 키로 교체한다.
문제가 된 커밋과 로그를 정리한다.
왜 노출되었는지 원인을 기록하고 같은 실수를 막는다.
여기서 중요한 사실이 하나 있습니다. Git 히스토리에 올라간 비밀은 파일만 지워도 끝나지 않을 수 있다는 점입니다. 그래서 "삭제했으니 괜찮겠지"보다 "이미 새 키로 교체했는가"를 먼저 생각해야 합니다.
AI와 함께 작업할 때 더 조심해야 하는 것
이 책은 AI와 협업하는 흐름을 다루기 때문에, 보안도 AI 시대에 맞게 생각해야 합니다. 특히 아래 원칙은 꼭 지키세요.
첫째, AI에게는 가능한 한 실제 값이 아니라 자리표시자를 전달합니다. YOUR_OPENAI_API_KEY 같은 방식이면 충분합니다.
둘째, 에러 로그를 붙여 넣을 때는 토큰, 쿠키, 세션값, 이메일, 전화번호 같은 정보가 포함되지 않았는지 먼저 봅니다.
셋째, AI가 "브라우저에서 바로 쓰려면 NEXT_PUBLIC_를 붙이세요"라고 제안하더라도, 그 값이 정말 공개 가능한 값인지 다시 판단해야 합니다. AI는 빠르게 도와주지만, 비밀의 무게까지 대신 판단해 주지는 않습니다.
넷째, Claude Code나 Codex 같은 도구를 자동화에 연결할 때도 인증 정보는 환경변수로 주입하고, 저장소나 스크립트에 하드코딩하지 않습니다.3
이번 장 정리
환경변수는 어렵지 않습니다. 하지만 중요합니다. 오히려 너무 단순해서 방심하기 쉽습니다.
이번 장에서 기억해야 할 핵심은 세 가지입니다.
첫째, 비밀 정보는 코드가 아니라 환경변수에 둔다.
둘째, NEXT_PUBLIC_가 붙은 값은 공개된다고 생각한다.
셋째, 유출이 의심되면 숨기려 하지 말고 바로 교체한다.
배포 직전의 서비스는 종종 덜 예쁘고, 아직 조금 서툴 수 있습니다. 그건 괜찮습니다. 하지만 비밀이 새는 서비스는 작동하더라도 안전하지 않습니다. 좋은 서비스는 기능만 돌아가는 서비스가 아니라, 지켜야 할 것을 지키는 서비스입니다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.