지난 편에서 코드는 같고 설정만 환경별로 갈아 끼운다고 했잖아요. 그럼 그 설정을 실제로 어떻게 밖에서 주입하느냐, 이번 편의 주제가 바로 그거예요. 여기서 등장하는 게 환경 변수시크릿인데, 둘은 비슷해 보여도 다루는 방식이 꽤 달라요. 특히 시크릿은 잘못 다루면 비밀번호가 통째로 새어 나가는 사고로 이어지니, 이 편은 마음 단단히 먹고 읽어 주시면 좋겠어요. 제가 현장에서 본 가장 아찔한 사고들이 대부분 여기서 나왔거든요.


이번 편에서는 환경 변수가 뭔지, 왜 설정을 코드에 박으면 안 되는지, 시크릿은 뭐가 다른지, 어떻게 안전하게 관리하는지, 그리고 브라우저 쪽 환경 변수의 특별한 함정까지 짚어 볼게요. 개념만 알면 어렵지 않은데, 모르고 지나치면 크게 데는 부분이라 하나씩 꼼꼼히 풀어 볼게요. 저도 여기서 여러 번 데었으니, 여러분은 덜 아프게 배우셨으면 해요.


환경 변수가 대체 뭐예요?


환경 변수는 프로그램 바깥에서 프로그램 안으로 넣어 주는 설정값이에요. 코드 안에 값을 적어 두는 게 아니라, 프로그램이 실행되는 환경이 값을 쥐고 있다가 프로그램한테 건네주는 거죠. 그래서 같은 코드라도 개발 환경에선 개발용 값이, 운영 환경에선 운영용 값이 들어가요. 코드는 그저 이름을 부르면 그 환경이 준비해 둔 값을 받아 쓰는 식이에요. 이 단순한 구조가 앞 편에서 말한 산출물은 하나, 설정은 환경별로를 실제로 가능하게 만들어 줘요.


코드에서 환경 변수를 읽는 모습은 대개 이래요. 예를 들어 서버 코드라면 const dbUrl = process.env.DATABASE_URL처럼 이름으로 값을 꺼내 와요. 여기서 DATABASE_URL이라는 이름의 값이 개발에선 연습용 데이터베이스 주소로, 운영에선 진짜 데이터베이스 주소로 채워져 있는 거죠. 코드는 이름만 알 뿐 실제 값이 뭔지는 몰라도 잘 돌아가요. 그게 바로 이 방식의 매력이에요. 값이 바뀌어도 코드는 한 글자도 안 고쳐도 되니까요.


환경 변수로 넣는 값은 참 다양해요. 데이터베이스 주소, 연결할 바깥 서비스의 주소, 지금이 어느 환경인지 알려 주는 표시, 특정 기능을 켤지 끌지 정하는 스위치 같은 것들이요. 공통점은 전부 환경마다 달라지거나 나중에 바뀔 수 있는 값이라는 거예요. 이렇게 변할 수 있는 것을 코드에서 떼어 밖으로 빼 두면, 코드는 변하지 않는 논리에만 집중할 수 있어 훨씬 깔끔해져요.


저는 새 프로젝트를 시작할 때, 어떤 값이 환경마다 달라질까를 먼저 목록으로 뽑아요. 그 목록이 곧 환경 변수 목록이 되거든요. 이걸 처음에 잘 정리해 두면, 나중에 여기저기 흩어진 설정을 찾아 헤매는 고생을 안 해요. 반대로 대충 시작하면, 값이 코드 곳곳에 박혀 나중에 환경을 하나 추가할 때 온 코드를 뒤지는 지옥을 만나죠.


왜 설정을 코드에 박으면 안 돼요?


설정값을 코드 안에 그냥 적어 두면 당장은 편해요. 데이터베이스 주소를 코드에 딱 써 두면 잘 도니까요. 그런데 이게 왜 문제냐면, 앞 편에서 강조한 재현성이 무너지기 때문이에요. 값이 코드에 박혀 있으면, 환경마다 다른 값을 쓰려고 환경마다 코드를 다르게 만들어야 하거든요. 그럼 검증에서 확인한 코드랑 운영에서 도는 코드가 달라져서, 검증의 의미가 사라져요.


더 무서운 건 보안이에요. 데이터베이스 비밀번호나 서비스 열쇠 같은 걸 코드에 박아 두면, 그 코드가 저장소에 올라가는 순간 비밀이 그대로 노출돼요. 코드 저장소는 여러 사람이 보고, 실수로 공개되기도 하고, 기록이 영원히 남거든요. 한번 저장소에 올라간 비밀은 나중에 지워도 기록에 남아 완전히 없애기가 어려워요. 저는 이걸 몰랐던 후배가 열쇠를 코드에 넣어 올렸다가, 그 열쇠로 엉뚱한 요금이 청구된 사고를 옆에서 지켜본 적이 있어요.


그래서 원칙은 딱 하나예요. 변하는 값과 비밀은 코드 밖에 둔다. 코드는 누구나 봐도 되는 공개해도 안전한 것만 담고, 환경마다 다른 값이나 비밀은 전부 환경 변수로 빼는 거죠. 이렇게 하면 코드를 공개해도 비밀은 안전하고, 환경을 바꿔도 코드는 그대로예요. 저는 이 원칙을 코드와 설정의 분리라 부르며, 어떤 프로젝트에서도 제일 먼저 세우는 규칙으로 삼아요.


실무에서 이걸 지키려고, 개발할 땐 .env 같은 파일에 값을 적어 두고 프로그램이 그걸 읽게 해요. 그런데 이 파일은 절대 저장소에 올리면 안 돼요. 그래서 .gitignore.env를 꼭 넣어, 실수로도 올라가지 않게 막죠. 대신 어떤 이름의 값이 필요한지만 적은 견본 파일을 .env.example로 두어, 새로 합류한 사람이 무엇을 채워야 하는지 알게 해요. 값은 빼고 이름표만 공유하는 거예요.


시크릿은 환경 변수랑 뭐가 달라요?


환경 변수 중에서도 새어 나가면 큰일 나는 것들을 따로 시크릿이라 불러요. 데이터베이스 비밀번호, 결제 서비스 열쇠, 로그인 서명에 쓰는 비밀 같은 것들이죠. 지금이 어느 환경인지 알려 주는 표시 같은 건 새어도 별일 아니지만, 시크릿은 남의 손에 들어가면 그대로 피해로 이어져요. 그래서 같은 환경 변수라도 시크릿은 훨씬 조심스럽게 다뤄야 해요.


둘의 차이를 저는 주소와 열쇠로 비유해요. 집 주소는 남이 알아도 큰일이 안 나지만, 집 열쇠는 남이 가지면 바로 털리잖아요. 서비스 주소 같은 건 주소에 가깝고, 비밀번호나 열쇠 같은 건 말 그대로 열쇠에 가까워요. 그래서 저는 환경 변수를 정리할 때 이걸 주소인가 열쇠인가로 나눠 보고, 열쇠에 해당하는 건 따로 더 엄격하게 보관해요.


시크릿을 다룰 때 가장 중요한 원칙은 아무 데도 평문으로 남기지 않는 거예요. 코드에도, 기록에도, 화면에도 시크릿의 실제 값이 글자 그대로 남으면 안 돼요. 특히 오류가 났을 때 상황을 기록에 남기다가, 시크릿까지 통째로 기록되는 실수가 흔해요. 저는 기록을 남기는 코드를 짤 때 혹시 여기 시크릿이 섞여 들어가나를 늘 한 번 더 살펴요. 한 줄의 부주의한 기록이 비밀을 영원히 남기거든요.


시크릿은 또 주기적으로 갈아 주는 게 좋아요. 아무리 잘 숨겨도 시간이 지나면 어디선가 샐 위험이 쌓이니까, 열쇠를 가끔 새것으로 바꿔 옛 열쇠를 무용지물로 만드는 거죠. 이걸 열쇠 교체라 부르는데, 사고가 났을 때도 얼른 새 열쇠로 갈면 샌 옛 열쇠가 쓸모없어져 피해를 끊을 수 있어요. 그래서 저는 시크릿을 언제든 갈아 끼울 수 있게 만들어 둬요. 갈기 어렵게 짜 두면 정작 급할 때 못 갈거든요.


시크릿은 어떻게 안전하게 관리해요?


가장 흔한 방법은 배포 도구나 호스팅이 제공하는 시크릿 보관함을 쓰는 거예요. 값을 거기에 암호화해서 넣어 두면, 프로그램이 실행될 때만 꺼내 환경 변수로 넣어 줘요. 사람은 한번 넣고 나면 그 값을 다시 볼 수 없게 막혀 있는 경우가 많죠. 예를 들어 wrangler secret put DATABASE_URL 같은 명령으로 넣으면, 값은 안전한 곳에 암호화돼 저장되고 코드나 설정 파일엔 흔적조차 안 남아요.


팀 규모가 커지면 전용 시크릿 관리 서비스를 두기도 해요. 시크릿을 한곳에 모아 두고, 누가 어떤 시크릿에 접근했는지 기록하고, 접근 권한을 사람마다 잘게 나누는 거죠. 이렇게 하면 시크릿이 흩어지지 않고, 문제가 생겼을 때 누가 언제 봤는지를 추적할 수 있어요. 다만 작은 팀엔 과할 수 있으니, 저는 팀 크기에 맞게 골라요. 처음부터 무거운 걸 들이기보다, 호스팅 기본 보관함으로 시작해 필요해지면 옮기는 식이죠.


어떤 방법이든 공통으로 챙겨야 할 건 접근을 꼭 필요한 사람한테만 주는 거예요. 운영 시크릿을 팀 전원이 볼 수 있게 열어 두면, 그만큼 샐 구멍이 늘어나거든요. 알아야 할 사람만 안다는 원칙으로 접근을 좁히고, 사람이 떠나면 그 사람이 알던 시크릿은 갈아 주는 게 안전해요. 저는 팀원이 바뀔 때마다 시크릿 접근 목록을 한 번씩 훑어, 이제 필요 없는 접근을 정리해요.


한 가지 더, 시크릿은 환경마다 반드시 다른 값을 써야 해요. 개발이랑 운영이 같은 열쇠를 쓰면, 개발 쪽이 뚫렸을 때 운영까지 위험해지거든요. 저는 개발용 열쇠랑 운영용 열쇠를 완전히 갈라 두고, 개발용은 실수해도 피해가 적게 권한을 최소로 묶어 둬요. 열쇠 하나가 모든 문을 여는 만능 열쇠는 되도록 안 만들려 해요. 그런 열쇠는 하나만 새도 전부가 무너지니까요.


브라우저 쪽 환경 변수는 왜 위험해요?


여기서 정말 많은 분이 데는 함정을 하나 짚을게요. 브라우저에서 도는 코드, 그러니까 프론트엔드 코드에 넣은 환경 변수는 비밀이 될 수 없어요. 빌드할 때 그 값이 코드 안에 그대로 박혀서, 사용자한테 전달되는 파일에 담겨 나가거든요. 사용자가 브라우저 도구로 파일을 열면 그 값이 훤히 보여요. 그러니 프론트엔드 환경 변수는 애초에 공개될 값만 넣어야 해요.


이걸 모르고 결제 열쇠 같은 걸 프론트엔드 환경 변수에 넣으면, 사용자한테 비밀을 그대로 나눠 주는 셈이 돼요. 저는 신입 시절에 이 원리를 몰라, 시크릿을 프론트엔드 변수에 넣고 변수니까 안전하겠지 하고 안심했던 적이 있어요. 나중에 빌드된 파일을 열어 보니 그 값이 고스란히 있어서 등골이 서늘했죠. 다행히 큰 열쇠는 아니었지만, 그 뒤로 브라우저에 가는 건 다 공개라는 원칙을 뼈에 새겼어요.


그래서 빌드 도구들은 프론트엔드에 담길 환경 변수엔 특별한 이름표를 붙이게 해요. 예를 들어 이름 앞에 VITE_가 붙은 것만 브라우저 코드에 넣어 주는 식이죠. 이 이름표는 이건 공개돼도 되는 값이라는 표시이기도 해요. 그러니 VITE_ 같은 접두어를 붙이기 전에, 저는 늘 이게 정말 공개돼도 괜찮나를 한 번 더 물어요. 이름표가 곧 공개 딱지인 셈이거든요.


그럼 브라우저에서 비밀 열쇠가 필요한 일은 어떻게 하냐면, 비밀은 서버에 두고 브라우저는 서버한테 부탁하게 만들어요. 예를 들어 바깥 서비스에 비밀 열쇠로 요청해야 하면, 브라우저가 직접 하지 말고 우리 서버가 대신 그 요청을 하고 결과만 브라우저에 돌려주는 거죠. 이러면 열쇠는 서버 안에서만 살고 브라우저엔 안 나가요. 비밀이 필요한 일은 항상 서버를 거친다는 이 습관이 프론트엔드 보안의 기본이에요.


환경 변수를 다룰 때 흔한 실수는요?


첫째 실수는 값을 넣어 두고 프로그램을 안 다시 켜는 거예요. 환경 변수는 프로그램이 시작할 때 읽어 들이는 경우가 많아서, 값을 바꿨는데 프로그램을 그대로 두면 옛 값으로 계속 도는 일이 생겨요. 저는 값을 바꿨는데 왜 안 먹지 하고 한참 헤매다, 프로그램을 다시 켜니 그제야 먹더라는 경험을 여러 번 했어요. 값을 바꿨으면 다시 시작이 필요한지부터 확인하세요.


둘째 실수는 이름을 오타 내는 거예요. DATABASE_URLDATABSE_URL로 잘못 적으면, 프로그램은 그 이름의 값을 못 찾아 비어 있는 채로 돌다가 엉뚱한 곳에서 터져요. 원인을 찾기가 참 어렵죠. 그래서 저는 프로그램이 시작할 때 꼭 필요한 환경 변수가 다 있는지 먼저 검사하고, 하나라도 비었으면 대놓고 멈추게 만들어요. 조용히 비운 채 돌다 나중에 터지는 것보다, 시작부터 크게 소리치는 게 훨씬 낫거든요.


셋째 실수는 환경 변수를 문서로 안 남기는 거예요. 어떤 값이 필요한지, 각 값이 무슨 뜻인지 정리해 두지 않으면, 새로 합류한 사람이 뭘 채워야 할지 몰라 헤매요. 저는 앞서 말한 견본 파일에 각 값이 무엇이고 어디서 얻는지를 짧게 적어 둬요. 이 작은 문서가 새 팀원의 첫날을 며칠에서 몇 시간으로 줄여 줘요. 환경 변수는 흩어지기 쉬우니, 한곳에 정리하는 습관이 정말 중요해요.


정리하면, 환경 변수는 변하는 값을 코드 밖에서 주입하는 장치고, 그중 새면 큰일 나는 시크릿은 더 엄격하게 보관해야 해요. 브라우저에 가는 값은 다 공개된다는 걸 잊지 말고, 비밀은 반드시 서버 안에서만 다뤄야 하죠. 이 원칙들만 지켜도 배포에서 가장 위험한 사고 하나는 확실히 막을 수 있어요. 다음 편에서는 이 설정을 품고 실제 산출물이 만들어지는 과정, 그러니까 빌드 이야기로 넘어가 볼게요.