정적 블로그에서 백엔드 API를 안전하게 연결하기

목차
블로그의 HTML을 정적으로 배포하더라도 뉴스레터 등록이나 AI 코드 설명처럼 외부 서비스와 통신하는 기능은 필요했습니다. 이 글은 예전에 Netlify Functions를 사용하던 경험에서 시작합니다. 현재 블로그는 Cloudflare Pages와 Pages Functions를 사용하므로 과거 플랫폼의 폴더나 요금표를 그대로 현재 설정으로 보시면 안 됩니다.
이전 글은 “정적 사이트에서는 로그인과 JWT가 불가능하다”, “환경변수가 요청을 인증한다”고 설명했습니다. 둘 다 잘못입니다. 정적 프론트엔드도 인증 서비스와 통신해 로그인하고 사용자 토큰을 전달할 수 있습니다. 서버 환경변수는 비밀을 보관하는 수단이지 누가 API를 호출했는지 확인하는 장치가 아닙니다.
세 경계로 나누기
브라우저 ── 사용자 요청 ──> 서버리스 API ── 서비스 자격 증명 ──> 외부 API·DB
인증·인가 입력 검증·제한 최소 권한·비밀 보관정적 파 일은 서버에서 저장해 둔 HTML·CSS·JavaScript를 전달합니다. 브라우저에서 실행되는 코드와 빌드 결과는 방문자가 읽을 수 있습니다. 클라이언트에 포함된 값은 비밀로 취급하면 안 됩니다. Gatsby에서 공개용 환경변수는 빌드 산출물에 포함될 수 있으므로, 비밀을 브라우저 코드에 참조하거나 출력하지 않는지 확인합니다.
서버리스 함수는 요청이 들어올 때 서버 환경에서 코드를 실행합니다. 비밀을 함수에만 제공하면 방문자가 그 값을 직접 받지 않도록 설계할 수 있습니다. 다만 함수가 요청을 검증하지 않고 비밀로 외부 API를 호출한다면, 공격자는 키를 몰라도 함수를 대리 호출해 비용을 발생시키거나 데이터를 조작할 수 있습니다.
공개 읽기, 사용자 작업, 관리자 작업은 다릅니다
| 작업 | 요청자 확인 | 서버가 별도로 할 일 |
|---|---|---|
| 공개 글 읽기 | 보통 불필요 | 공개해도 되는 콘 텐츠만 정적 파일에 포함 |
| 사용자별 데이터 조회·수정 | 인증된 사용자와 대상 권한 확인 | 토큰 검증, 소유권·역할 확인 |
| 관리자 구독자 목록·삭제 | 관리자 인증과 인가 | 최소 권한, 감사 로그, 실패 시 접근 거부 |
| 공개 뉴스레터 신청·AI 설명 | 익명 요청을 허용할 수 있음 | 입력 크기 제한, 속도 제한, 남용·비용 관리 |
JWT는 자격 증명을 전달하는 한 방식입니다. 서명·issuer·audience·만료와 역할을 서버에서 검증해야 하며, 토큰이 있다는 사실만으로 권한이 결정되지는 않습니다. 브라우저의 localStorage는 XSS에 노출될 수 있고, HttpOnly 쿠키는 CSRF·SameSite 등 다른 설계가 필요합니다. 하나가 모든 상황의 정답은 아닙니다.
Supabase 공개 키와 서비스 키
Supabase의 publishable 또는 anon 키는 클라이언트에서 사용하는 것을 전제로 하지만, 접근 제한은 Row Level Security 정책과 사용자 권한으로 적용해야 합니다. service role 또는 secret 키는 높은 권한을 가지므로 브라우저에 넣지 않고 서버에서만 사용해야 합니다. 서버에서 service 키로 대신 실행하면 RLS를 우회할 수 있으므로, 함수가 사용자의 권한을 대신 검증하지 않으면 비밀을 감춰도 데이터는 보호되지 않습니다. Supabase API 키 안내와 RLS 안내를 함께 확인하세요.
예를 들어 구독 신청은 누구나 할 수 있지만 구독자 전체 목록은 관리자에게만 보여줘야 합니다. 요청의 method와 경로별로 공개 범위를 나누고, 인증 누락이나 설정 오류는 거부해야 합니다. 에러 응답과 로그에는 비밀 값이나 외부 서비스의 민감한 응답을 그대로 담지 않습니다.
CORS는 인증이 아닙니다
CORS는 브라우저가 다른 출처의 응답을 JavaScript에 공개할지 제한하는 정책입니다. curl이나 서버 간 요청에는 같은 보호를 기대할 수 없고, 허용한 출처에서 보낸 모든 사용자가 관리자라는 뜻도 아닙니다. 허용 origin을 필요한 범위로 제한하되, 인증과 인가는 함수에서 별도로 수행해야 합니다. 쿠키 인증을 쓴다면 CSRF도 함께 검토합니다.
공개 API의 비용과 안정성
AI 설명처럼 익명 호출이 필요한 API에도 최대 요청 본문 크기, 허용 입력 형식, 모델 출력 길이와 예산·알림을 두는 편이 좋습니다. IP 기반 제한은 우회와 공유 IP의 오탐을 감안해야 하고, 인스턴스 로컬 메모리 카운터는 여러 인스턴스 사이에 공유되지 않습니다. 정확한 전역 제한이 필요하면 원자적 연산을 지원하는 공유 저장소나 플랫폼 제한 기능을 사용합니다.
캐시도 보안 경계입니다. 사용자별 결과가 공용 캐시로 새지 않도록 키와 저장 범위를 구분하고, 민감 정보가 포함된 코드는 저장 정책을 따로 정해야 합니다. 캐시 모델·프롬프트 버전이 바뀌면 기존 응답을 무효화하거나 새 키를 사용합니다.
배포 후 확인할 항목
- 브라우저의 HTML·JavaScript·네트워크 응답에 비밀이 포함되지 않는지 확인합니다.
- 인증 없는 요청으로 관리자 목록 조회·삭제가 거부되는지 검증합니다. 유효한 사용자도 타인의 데이터에는 접근하지 못해야 합니다.
- 공개 요청의 크기·빈도·비용 한도와 장애 시 응답을 시험합니다.
- API 로그에 토큰, 키, 개인 데이터가 남지 않는지 확인하고 키를 교체할 절차를 마련합니다.
- 현재 배포 플랫폼의 설정과 API 경로를 기준으로 검증합니다. 이 글의 이전 Netlify 예제나 가격은 현재 서비스의 구성·요금을 뜻하지 않습니다.
정적 배포와 인증은 서로 배타적이지 않습니다. 브라우저와 서버의 경계를 나눈 다음, 누가 어떤 작업을 할 수 있는지와 서버가 어떤 비밀을 사용할 수 있는지를 각각 설계해야 합니다.
