결론부터 말씀드릴게요. 웹페이지 코드에 API 키를 넣으면 그건 숨긴 게 아니라 화면에만 안 보이는 것입니다. 방문자 누구나 몇 번의 클릭으로 읽을 수 있고, 읽어간 사람은 내 결제 수단으로 API를 씁니다. 키는 브라우저로 내려가는 파일에 절대 들어가면 안 되고, 서버 쪽에 남겨둔 채 중계해서 써야 합니다.
내가 이 서비스의 사용료를 낼 사람이라는 걸 증명하는 문자열입니다. 비밀번호와 다른 점은, 사람이 로그인할 때가 아니라 프로그램이 자동으로 요청을 보낼 때 쓰인다는 것입니다. 그래서 키를 가진 쪽이 곧 요금을 내는 쪽이 됩니다.
이 글에서는 왜 위험한지, 사람들이 실제로 가장 많이 하는 실수 8가지, 그리고 정적 사이트에서도 AI 기능을 안전하게 붙이는 방법까지 순서대로 다룹니다.
01 왜 코드에 넣으면 안 되나요
브라우저로 내려간 파일은 전부 공개된 것입니다
웹사이트가 열리는 과정을 생각해보면 답이 바로 나옵니다. 서버는 HTML·CSS·자바스크립트 파일을 방문자의 컴퓨터로 복사해서 보냅니다. 브라우저는 받은 파일을 자기 컴퓨터에서 실행해 화면을 그립니다.
즉 내 사이트를 여는 순간, 그 사람 컴퓨터 안에 내 코드 사본이 통째로 들어갑니다. 화면에 안 보이는 이유는 숨겨져서가 아니라 그냥 그리지 않았을 뿐입니다.
확인하는 건 3초면 됩니다. 아무 사이트에서나 Ctrl+U(맥은 ⌘+⌥+U)를 눌러보세요. 그 페이지의 코드 전문이 그대로 뜹니다. 개발자도구의 Network 탭을 열면 불러온 자바스크립트 파일도 전부 열어볼 수 있습니다.
그래서 브라우저에서 도는 코드에는 비밀을 담을 자리가 원리적으로 없습니다. 난독화도 소용없습니다. 실행되려면 결국 원래 값으로 돌아와야 하고, 그 값은 개발자도구에서 그대로 보입니다.
"제 사이트는 아무도 안 보는데요"
사람이 안 봐도 기계가 봅니다. 공개된 저장소와 웹페이지를 자동으로 훑으면서 키 형태의 문자열을 수집하는 프로그램이 상시 돌아갑니다. 앤트로픽 키는 sk-ant-로 시작하는 정해진 형태라 기계가 찾기 아주 쉽습니다.
그래서 발견되는 데 걸리는 시간은 방문자 수와 상관이 없습니다. 배포하고 몇 분에서 몇 시간 만에 잡히는 경우가 흔합니다.
발견되면 무슨 일이 생기나요
- 요금이 내 결제 수단으로 청구됩니다. 남이 쓴 만큼 내가 냅니다. 상한을 걸어두지 않았다면 알아차릴 때까지 계속 늘어납니다
- 서비스가 갑자기 멈춥니다. 한도를 넘기거나 키가 정지되면 내 사이트의 기능부터 죽습니다
- 내 이름으로 이상한 사용이 기록됩니다. 누가 무엇에 썼는지는 로그에 남지만 이미 벌어진 뒤입니다
한 가지 다행인 점은 있습니다. 깃허브는 공개 저장소를 검사해 알려진 형태의 키를 찾아내고, 앤트로픽을 포함한 제휴 서비스에 통보합니다. 앤트로픽은 통보받은 키를 자동으로 비활성화하고 메일로 알려줍니다.
다만 이건 안전망이지 대책이 아닙니다. 탐지까지의 공백이 있고, 배포된 웹페이지 자체는 이 검사의 대상이 아닙니다. 저장소가 아니라 화면에 노출된 키는 아무도 대신 막아주지 않습니다.
02 자주 하는 실수 8가지
수업에서 실제로 반복해서 나오는 순서대로 정리했습니다. 앞의 세 개가 특히 많습니다.
HTML 안에 키를 그대로 적어둠
가장 흔합니다. 화면에 안 보이니까 숨겨졌다고 느끼지만, 페이지 소스 보기 한 번이면 끝입니다. 클로드 코드에게 '동작하게만 만들어줘'라고 하면 편의상 이렇게 나오는 경우가 있으니 결과물을 꼭 확인하세요.
.env는 만들었는데 .gitignore에 안 넣음
키를 분리하려는 좋은 시도가 정확히 반대 결과를 냅니다. .env라는 이름 자체가 스캐너가 최우선으로 찾는 파일명이라, 그냥 코드에 적어둔 것보다 더 빨리 발견됩니다. 첫 커밋 전에 .gitignore부터 만드세요.
VITE_ 나 NEXT_PUBLIC_ 접두사를 붙임
환경변수가 안 읽힌다는 에러를 만나면 검색 결과를 따라 접두사를 붙이게 되고, 그러면 실제로 에러가 사라집니다. 문제가 해결된 것처럼 보이지만 이 접두사는 '이 값을 브라우저로 내보내라'는 지시입니다. 에러가 났던 이유는 그 코드가 브라우저에서 도는 코드였기 때문이고, 진짜 해결책은 호출 자체를 서버로 옮기는 것입니다.
키를 지우고 새로 커밋하면 끝났다고 생각함
깃은 이력을 통째로 보관합니다. 현재 파일에서 사라져도 과거 커밋을 열면 그대로 있습니다. 공개 저장소라면 이미 복제됐을 수도 있습니다. 이력을 지우는 도구를 찾기 전에 키부터 폐기하세요. 폐기된 키는 이력에 남아 있어도 아무 쓸모가 없습니다.
비공개 저장소니까 괜찮다고 생각함
지금은 나 혼자여도 나중에 협업자가 들어오면 전원이 봅니다. 포트폴리오로 쓰려고 공개로 바꾸는 순간 과거 이력까지 전부 열립니다. 그리고 저장소가 비공개여도 깃허브 페이지로 배포한 결과물은 공개라, 그 안에 든 키는 그대로 노출됩니다.
스크린샷과 화면 녹화에 키가 찍힘
터미널 화면, 콘솔 설정 페이지, 편집기 화면을 캡처해 블로그나 단톡방에 올릴 때 자주 발생합니다. 모자이크를 씌워도 원본 이미지가 남는 편집기가 있으니, 캡처 전에 값을 지우거나 가짜 값으로 바꿔두는 편이 확실합니다.
도와달라며 키를 통째로 붙여넣음
질문 사이트, 커뮤니티, 채팅방에 코드를 통째로 붙여넣으면서 키까지 같이 올라갑니다. 앤트로픽도 지원 문의를 포함해 어디에서도 키를 공유하지 말 것을 권고합니다. 코드를 공유할 때는 키 자리를 YOUR_API_KEY로 바꾸는 습관을 들이세요.
결제 상한을 걸어두지 않음
이건 유출 자체는 아니지만 피해 규모를 결정합니다. 상한이 없으면 유출을 알아차릴 때까지 금액이 계속 늘어납니다. 키를 만들 때 사용 한도나 자동 충전 설정을 함께 점검해두면, 사고가 나도 손실이 한 선에서 멈춥니다.
03 그럼 어디에 넣어야 하나요
상황에 따라 답이 셋으로 갈립니다. 내 사이트가 어느 경우인지부터 판단하세요.
상황 A — 내 컴퓨터에서만 돌리는 코드
파이썬 스크립트, 자동화 도구, 클로드 코드로 만든 로컬 프로그램. 배포하지 않는다면 .env 파일이면 충분합니다.
# .env 파일 (프로젝트 폴더 안)
ANTHROPIC_API_KEY=sk-ant-여기에실제키
# .gitignore 파일 — 반드시 같이 만드세요
.env
.env.local
파이썬에서는 이렇게 읽습니다.
from dotenv import load_dotenv
import os
load_dotenv()
api_key = os.getenv("ANTHROPIC_API_KEY")
순서가 중요합니다. .gitignore를 먼저 만들고 .env를 만드세요. 반대로 하면 첫 커밋에 .env가 딸려 올라갑니다.
상황 B — 서버가 있는 웹사이트
넥스트, 버셀, 넷리파이 함수 등 서버에서 실행되는 코드가 있다면 그쪽에 키를 두면 됩니다. 호스팅 대시보드의 환경변수 설정에 넣고, 서버 코드에서 꺼내 씁니다.
단 접두사를 조심하세요. NEXT_PUBLIC_이나 VITE_가 붙은 변수는 브라우저로 내보내지는 값입니다. 이름 그대로 public입니다. 비밀 값에는 접두사를 붙이지 않습니다.
상황 C — 깃허브 페이지 같은 정적 사이트
여기가 진짜 문제입니다. 정적 호스팅에는 서버에서 실행되는 코드가 아예 없습니다. 그러니까 키를 둘 자리 자체가 없습니다.
해결책은 하나입니다. 키를 대신 들고 있어줄 아주 작은 서버를 하나 두는 것. 이걸 중계 서버 또는 프록시라고 부릅니다. 클라우드플레어 워커가 이 용도로 가장 쉽고, 소규모라면 무료로 충분합니다.
브라우저 → 내 워커 주소 → 앤트로픽 API → 워커 → 브라우저.
브라우저는 워커 주소만 압니다. 키는 워커 안에만 있고 밖으로 나가지 않습니다. 방문자가 개발자도구를 아무리 열어도 워커 주소밖에 안 보입니다.
04 클라우드플레어 워커로 감추기
1. 워커 만들기
클라우드플레어 대시보드 › Workers & Pages › Create에서 새 워커를 만듭니다. 이름은 ai-proxy처럼 알아보기 쉽게 짓습니다.
2. 키를 시크릿으로 넣기
워커 설정 › Variables and Secrets에서 Secret 타입으로 추가합니다. 이름은 ANTHROPIC_API_KEY.
여기서 일반 변수가 아니라 시크릿으로 넣는 게 핵심입니다. 시크릿은 암호화되어 저장되고, 저장한 뒤에는 대시보드에서도 값을 다시 볼 수 없습니다. 코드 안에는 키가 한 글자도 등장하지 않습니다.
터미널을 선호한다면 이 명령 하나입니다.
npx wrangler secret put ANTHROPIC_API_KEY
3. 중계 코드 작성
아래 코드를 그대로 쓰시고 ALLOWED의 주소만 본인 도메인으로 바꾸세요.
export default {
async fetch(request, env) {
const ALLOWED = "https://focusaiedu.com";
// 브라우저의 사전 확인 요청 처리
if (request.method === "OPTIONS") {
return new Response(null, { headers: cors(ALLOWED) });
}
if (request.method !== "POST") {
return json({ error: "POST만 허용" }, 405, ALLOWED);
}
// 1. 내 사이트에서 온 요청인지 확인
const origin = request.headers.get("Origin");
if (origin !== ALLOWED) {
return json({ error: "허용되지 않은 출처" }, 403, ALLOWED);
}
// 2. 사용자 입력은 길이만 받고, 나머지는 서버가 정한다
const body = await request.json().catch(() => null);
const text = (body?.text || "").slice(0, 2000);
if (!text) return json({ error: "text 필요" }, 400, ALLOWED);
// 3. 키는 시크릿에서 꺼내 쓴다. 코드에 값이 없다
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 500,
system: "너는 한국어로 답하는 도우미다.",
messages: [{ role: "user", content: text }],
}),
});
if (!res.ok) return json({ error: "요청 실패" }, 502, ALLOWED);
// 4. 필요한 것만 돌려준다
const data = await res.json();
const reply = data.content?.map(b => b.text || "").join("") || "";
return json({ reply }, 200, ALLOWED);
},
};
function cors(origin) {
return {
"Access-Control-Allow-Origin": origin,
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "content-type",
};
}
function json(obj, status, origin) {
return new Response(JSON.stringify(obj), {
status,
headers: { "content-type": "application/json", ...cors(origin) },
});
}
4. 프런트엔드에서 부르기
이제 웹페이지 쪽 코드에는 키가 없습니다. 워커 주소만 있습니다.
const res = await fetch("https://ai-proxy.내계정.workers.dev", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ text: 사용자입력 }),
});
const { reply } = await res.json();
이 코드가 막고 있는 것들
단순 중계만 하면 워커 주소를 알아낸 사람이 무한정 쓸 수 있습니다. 위 코드에는 네 가지 제한이 들어 있습니다.
- 출처 확인 — 내 도메인에서 온 요청만 처리합니다. 남의 사이트에 붙여 쓰는 걸 막습니다
- 모델과 시스템 프롬프트 고정 — 사용자가 바꿀 수 없게 워커 안에 박아둡니다. 내 서비스를 범용 챗봇으로 쓰지 못하게 하는 장치입니다
- 입력 길이 제한 — 긴 글을 밀어 넣어 비용을 키우는 걸 막습니다
- max_tokens 상한 — 응답 길이에 천장을 둡니다
완벽한 차단은 아닙니다. 출처 정보는 위조할 수 있어서 마음먹은 사람은 우회합니다. 하지만 키 자체가 유출되는 것과는 피해 규모가 완전히 다릅니다. 키가 새면 무제한이지만, 워커가 남용되면 제한된 범위 안에서만 소모되고 워커 주소를 바꾸면 즉시 끊깁니다.
본격적으로 운영한다면 IP당 호출 횟수 제한과 결제 상한을 추가로 걸어두세요.
05 이미 노출됐다면
순서가 중요합니다. 코드 고치기부터 하면 그 사이에 계속 쓰입니다.
- 키부터 폐기합니다. 콘솔의 API keys 페이지에서 해당 키를 삭제하세요. 폐기된 키는 어디에 남아 있든 무용지물이 됩니다
- 새 키를 발급해서 워커 시크릿이나 .env 등 안전한 자리에만 넣습니다
- 코드를 고칩니다. 이제 키가 코드에서 완전히 사라져야 합니다
- 사용 내역과 로그를 확인해 낯선 호출이 있었는지 봅니다
- 결제 상한을 점검합니다. 다음에 또 새더라도 손실이 한 선에서 멈춥니다
깃 이력 정리는 우선순위가 아닙니다. 이력에서 지워도 이미 복제됐을 수 있고, 폐기한 키는 이력에 남아 있어도 아무 일도 못 합니다. 폐기가 곧 해결입니다.
한눈에 보는 판단표
| 어떻게 두었나 | 판정 | 이유 | 대신 이렇게 |
|---|---|---|---|
| HTML·JS 파일에 키를 직접 작성 | 위험 | 브라우저로 전송되는 파일이라 누구나 열람 가능 | 서버 쪽으로 옮기기 |
| .env 파일에 저장 + .gitignore 등록 | 안전 | 내 컴퓨터에만 남고 저장소에 올라가지 않음 | 로컬 개발의 기본 |
| .env 파일을 만들었지만 .gitignore 미등록 | 위험 | 파일 자체가 커밋되어 저장소에 그대로 공개 | .gitignore에 .env 추가 |
| VITE_ · NEXT_PUBLIC_ 접두사 환경변수 | 위험 | 브라우저로 내보내라는 표시라 빌드 결과에 박힘 | 접두사 없는 서버 전용 변수 |
| 비공개 저장소에 키를 커밋 | 주의 | 협업자 전원 열람, 공개 전환 시 이력까지 노출 | 환경변수로 분리 |
| 깃허브 페이지·넷리파이 정적 배포 | 위험 | 서버 코드가 없어 키를 숨길 자리 자체가 없음 | 중계 서버 두기 |
| 클라우드플레어 워커·서버리스 함수 중계 | 안전 | 키가 서버에 남고 브라우저는 결과만 받음 | 호출 제한 함께 설정 |
| 호스팅 대시보드의 환경변수·시크릿 기능 | 안전 | 암호화 저장되고 코드에 포함되지 않음 | 배포 환경의 기본값 |
표가 잘리면 좌우로 밀어서 보세요
판단 기준은 하나입니다. 이 값이 방문자의 브라우저까지 내려가는가. 내려간다면 위험, 서버에서 멈춘다면 안전입니다.
정리
- 브라우저로 내려가는 파일에 든 값은 전부 공개된 것. 난독화도 소용없음
- 사람이 안 봐도 자동 수집 프로그램이 먼저 찾음. 방문자 수와 무관
- .gitignore를 .env보다 먼저 만들기
- VITE_ · NEXT_PUBLIC_ 접두사는 브라우저로 내보내라는 뜻. 비밀 값에 금지
- 지우고 다시 커밋해도 이력에 남음. 이력 정리보다 폐기가 먼저
- 정적 사이트에서 AI를 붙이려면 클라우드플레어 워커 같은 중계 서버가 답
- 중계 서버에는 출처 확인·프롬프트 고정·길이 제한·결제 상한을 함께 걸기
수업에서 이 부분을 항상 따로 떼어 다루는 이유가 있습니다. 만드는 건 클로드가 대신 해주지만, 그 결과물이 안전한지 판단하는 건 만든 사람만 할 수 있기 때문입니다. 기준은 딱 하나만 기억하면 됩니다. 이 값이 브라우저까지 내려가는가.
자주 묻는 질문
API 키를 코드에 넣으면 왜 위험한가요?
브라우저에서 동작하는 HTML·CSS·자바스크립트는 전부 방문자의 컴퓨터로 그대로 전송됩니다. 개발자도구를 열거나 페이지 소스를 보면 누구나 읽을 수 있습니다. 코드에 넣은 키는 숨겨진 것이 아니라 화면에 안 보일 뿐이며, 유출되면 내 결제 수단으로 다른 사람이 API를 사용하게 됩니다.
깃허브 저장소를 비공개로 하면 안전한가요?
훨씬 낫지만 안전하다고 볼 수는 없습니다. 협업자가 늘어나면 전원이 키를 보게 되고, 나중에 공개로 전환하면 과거 커밋 이력까지 전부 공개됩니다. 무엇보다 깃허브 페이지·넷리파이 같은 정적 호스팅으로 배포하면 저장소가 비공개여도 배포된 파일은 공개되므로 키가 그대로 노출됩니다.
코드에서 키를 지우고 다시 커밋하면 되지 않나요?
안 됩니다. 깃은 모든 변경 이력을 보관하므로 과거 커밋을 열면 키가 그대로 남아 있습니다. 이력을 정리하는 도구가 있지만 완전하지 않고, 이미 공개된 순간 누군가 복제해 갔을 수 있습니다. 유출을 확인했다면 이력 정리보다 키 폐기와 재발급이 먼저입니다.
내 사이트는 아무도 안 보는데 괜찮지 않나요?
방문자 수와 무관합니다. 공개된 저장소와 웹페이지를 자동으로 훑으며 키 형태의 문자열을 수집하는 프로그램이 상시 돌아갑니다. 사람이 발견하기 전에 기계가 먼저 찾습니다. 배포 직후 몇 분에서 몇 시간 안에 탐지되는 경우도 흔합니다.
VITE_ 또는 NEXT_PUBLIC_ 접두사를 붙인 환경변수는 안전한가요?
정반대입니다. 이 접두사들은 해당 값을 브라우저로 내보내겠다는 표시입니다. 빌드 시점에 결과 파일 안에 문자열로 그대로 박히기 때문에, .env 파일에 적어두었더라도 배포된 자바스크립트를 열면 값이 보입니다. 비밀 값에는 절대 이 접두사를 붙이면 안 됩니다.
클라우드플레어 워커를 쓰면 정확히 뭐가 달라지나요?
키가 브라우저를 떠나 서버 쪽에 남습니다. 브라우저는 내 워커 주소로만 요청을 보내고, 워커가 자기 안에 보관한 키로 API를 대신 호출한 뒤 결과만 돌려줍니다. 방문자가 개발자도구를 아무리 열어봐도 워커 주소만 보이고 키는 보이지 않습니다.
워커 주소가 공개되면 남이 마음대로 쓰지 않나요?
그래서 워커에 제한을 걸어야 합니다. 요청을 보낸 사이트 주소를 확인해 내 도메인에서 온 요청만 처리하고, 사용자가 모델이나 시스템 프롬프트를 마음대로 바꾸지 못하게 워커 안에 고정하고, 입력 길이와 호출 횟수에 상한을 두는 것이 기본입니다. 완벽한 차단은 아니지만 키 자체가 유출되는 것과는 피해 규모가 다릅니다.
API 키가 유출된 것 같으면 무엇부터 해야 하나요?
가장 먼저 콘솔에서 해당 키를 폐기합니다. 코드 수정이나 이력 정리보다 폐기가 먼저입니다. 그다음 새 키를 발급해 안전한 위치에 넣고, 사용 내역과 로그에서 낯선 호출이 있었는지 확인합니다.
깃허브가 알아서 막아주지 않나요?
깃허브는 공개 저장소를 검사해 알려진 형태의 키를 탐지하고, 앤트로픽을 포함한 제휴 서비스에 통보해 해당 키를 자동으로 비활성화하는 절차를 운영합니다. 도움은 되지만 안전망일 뿐입니다. 탐지되지 않는 형식도 있고, 탐지 전까지의 공백 동안 사용될 수 있으며, 배포된 웹페이지 자체는 이 검사의 대상이 아닙니다.
공부용이라 금방 지울 건데 그때도 지켜야 하나요?
네. 유출된 키에는 '연습용'이라는 표시가 없습니다. 오히려 급하게 만든 실습 코드가 그대로 공개 저장소에 남아 사고로 이어지는 경우가 많습니다. 처음부터 환경변수로 시작하면 습관이 잡혀서 나중에 따로 배울 필요가 없습니다.
정책과 화면 구성은 변경될 수 있습니다. 최신 정보는 공식 문서에서 확인하세요.