Vercel 서버리스로 API 키 숨기고 배포하기 — 브라우저 프로토타입을 백엔드로 옮기기
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
시리즈 · 한국 주소 STT 보정 — 음성으로 받은 한국 주소의 인식 오류를 단계별로 고쳐 가는 6편 기록입니다.
- 1. 왜 자꾸 틀리나 — 문제 정의와 파이프라인 개요
- 2. 국토부 도로명주소 API를 정답지로 (JSONP→서버)
- 3. 자모 유사도 매칭 — 한글 분해 + 레벤슈타인
- 4. "이백팔십"을 280으로 — 한글 숫자 되돌리기
- 5. 규칙으로 안 되는 것만 LLM에게 (api vs cli)
- 6. 브라우저 프로토타입 → Vercel 서버리스 — 지금 보는 글
프로토타입이 브라우저 단독이었다는 것의 함정
처음 만든 것은 HTML 파일 하나였습니다. 음성 인식(STT)도 브라우저가 하고, 인식된 문자열을 실제 주소로 보정하는 로직도 브라우저 안에서 돌았습니다. 데모를 보여주기에는 이만한 게 없습니다. 파일 하나 열면 바로 동작하니까요.
문제는 그 보정 로직이 외부 API 두 개에 의존한다는 데 있었습니다. 하나는 국토교통부 도로명주소 API의 승인키, 다른 하나는 LLM 폴백에 쓰는 Anthropic API 키입니다. 브라우저 안에서 이 API들을 호출한다는 것은, 그 키들이 클라이언트 코드 안에 그대로 박혀 있다는 뜻입니다. 개발자 도구만 열면 누구나 볼 수 있습니다.
키를 남에게 그대로 넘겨주는 코드를, 데모라는 이유로 계속 들고 갈 수는 없었습니다. 이게 서버로 옮긴 첫 번째 이유입니다.
CORS 때문에 JSONP로 우회하던 흔적
두 번째 이유는 조금 더 기술적입니다. 국토부 도로명주소 API는 정식으로는 JSON을 돌려주는 엔드포인트를 제공합니다. 그런데 브라우저에서 이 JSON 엔드포인트를 직접 부르면 CORS(교차 출처 리소스 공유) 정책에 막힙니다. 다른 출처의 서버가 "브라우저에서 직접 불러도 된다"고 허용해주지 않으면, 브라우저는 응답을 읽지 못하게 막아버립니다.
그래서 프로토타입은 JSONP라는 우회로를 썼습니다. JSONP는 <script> 태그로 외부 스크립트를 불러오는 방식이라 CORS 제약을 받지 않는다는 점을 이용한 오래된 기법입니다. 동작은 하지만, 이건 정공법이 아닙니다. 브라우저라는 환경이 강제한 제약을 피하려고 억지 우회로를 하나 더 얹은 셈이니까요.
서버는 브라우저가 아닙니다. 서버 대 서버 호출에는 CORS라는 개념 자체가 없습니다. 로직을 서버로 옮기는 순간, JSONP 우회는 통째로 사라지고 국토부의 정식 JSON API(addrLinkApi.do)를 그냥 직접 호출하면 됩니다. 우회를 걷어내는 것 자체가 이식의 보상이었습니다.
Vercel 서버리스로 옮기며 정한 것들
이식 대상은 Vercel 서버리스 함수로 정했습니다. 구조는 단순합니다. POST /api/correct가 텍스트를 받아 보정 결과를 돌려주고, GET /api/health가 국토부 API 연결 상태를 점검합니다. 브라우저 쪽에는 STT와 화면만 남기고, 보정은 전부 /api/correct를 호출하도록 바꿨습니다.
키는 전부 서버 환경변수로 옮겼습니다. JUSO_CONFM_KEY, ANTHROPIC_API_KEY 모두 코드가 아니라 env에서 읽습니다. 클라이언트 코드에서 키의 흔적이 사라졌습니다.
리전도 명시적으로 고정했습니다. vercel.json에서 서울 리전(icn1)으로 못 박았습니다. 국토부 API를 부르는 서버가 한국에 가까이 있어야 지연이 줄고, 데이터가 어디에 위치하는지도 통제할 수 있기 때문입니다.
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"regions": ["icn1"],
"functions": {
"api/**/*.js": {
"maxDuration": 30
}
}
}
maxDuration 30초와 플랜의 현실
위 설정에서 눈여겨볼 것이 maxDuration: 30입니다. 이 함수가 최대 30초까지 돌 수 있게 허용한다는 뜻입니다. 왜 이렇게 넉넉하게 잡았을까요.
느린 경로 때문입니다. 어떤 입력은 국토부 API를 여러 단계로 검색하고, 그것도 모자라 LLM을 순차로 두 번까지 부릅니다. 이 경로가 한 번에 끝나지 않고 차곡차곡 쌓이면 수 초가 우습게 지나갑니다. 짧은 실행 상한을 걸어두면 이런 요청이 중간에 잘려버립니다.
여기서 현실적인 함정을 하나 짚어야 합니다. Vercel의 무료 등급인 Hobby 플랜은 함수 실행시간 상한이 10초입니다. vercel.json에 30초를 적어놔도, 느린 경로가 10초를 넘기면 Hobby에서는 그대로 잘립니다. 느린 요청까지 안정적으로 받아내려면 Pro 등급이 필요합니다. 설정값과 요금제가 따로 노는 지점이라, 이걸 모르면 "설정은 30초로 했는데 왜 잘리지"에서 한참 헤맵니다.
어디로든 이사 갈 수 있게 — 이식성 규칙 두 개
마지막으로, 이번엔 Vercel에 올렸지만 다음엔 다른 곳으로 갈 수도 있다는 전제를 코드에 심어뒀습니다. 규칙은 딱 두 개입니다.
첫째, 데이터베이스는 표준 Postgres 인터페이스로만 접근합니다. 특정 플랫폼 전용 기능에 의존하지 않습니다. 둘째, 모든 비밀키와 접속 정보는 환경변수로 둡니다. 코드에 하드코딩하지 않습니다. 접속 문자열조차 DATABASE_URL 같은 env로 주입받습니다.
이 두 가지만 지키면 플랫폼을 갈아타는 일이 데이터 덤프하고 복원하는 수준으로 단순해집니다. 처음부터 "여기 아니면 안 돼"를 만들지 않는 것, 그게 개인 프로젝트를 오래 들고 가는 방법이라고 생각합니다.
FAQ
Q. 브라우저에서도 잘 돌던데, 굳이 서버로 옮길 필요가 있었나요. 동작하느냐와 배포해도 되느냐는 다른 질문입니다. 브라우저 단독 버전은 API 키가 클라이언트에 그대로 노출됩니다. 데모로는 충분하지만, 그 상태로는 남에게 링크를 줄 수 없습니다. 키를 서버 뒤로 숨기는 것 하나만으로도 이식할 이유는 충분했습니다.
여섯 편을 돌아보며
1편에서 던진 질문은 단순했습니다. "음성으로 받은 주소는 왜 이렇게 자주 틀리고, 그걸 어떻게 실제 주소로 되돌릴 것인가."
그 뒤로 자모를 분해해 발음이 비슷한 후보를 찾는 유사도 매칭을 만들었고, "이백팔십"을 280으로 바꾸는 한글 숫자 변환을 붙였고, 국토부 API를 시도·시군구·읍면동 순으로 좁혀 부르는 다단계 검색을 짰고, 결정론 로직이 손을 드는 지점에서만 LLM을 부르는 폴백을 얹었습니다. 그리고 이번 편에서, 그 모든 것을 브라우저에서 꺼내 서버 뒤로 옮겼습니다.
돌아보면 이 프로젝트는 "한 번에 완성된 정답"이 아니라 "틀리는 방식을 하나씩 이해하고, 그때마다 도구를 하나씩 늘려간 과정"이었습니다. 유사도 매칭도, 숫자 변환도, LLM 폴백도, 서버 이식도, 전부 앞 단계가 못 잡은 오류를 보고 나서야 필요해진 것들이었습니다. 개인 프로젝트를 만든다는 건 결국 이렇게, 문제가 스스로 다음 할 일을 알려주게 두는 일 아닐까 싶습니다. 여섯 편을 함께 읽어주셔서 고맙습니다.
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
댓글
댓글 쓰기