Supabase 프로젝트를 처음 만들면 대시보드에 키가 두 개 보인다. 어떤 키를 어디에 써야 하는지 모른 채 둘 다 프론트엔드에 넣으면 보안 사고로 이어진다. 세 가지 환경 변수의 역할을 정확히 이해하면 실수를 원천 차단할 수 있다.
| 변수명 | 값 예시 | 용도 |
|---|---|---|
SUPABASE_URL | https://xxx.supabase.co | 프로젝트 API 엔드포인트 |
SUPABASE_ANON_KEY | eyJ... (JWT) | 클라이언트 공개 키 |
SUPABASE_SERVICE_ROLE_KEY | eyJ... (JWT) | 서버 전용 관리자 키 |
SUPABASE_URL은 단순한 엔드포인트 주소다. 공개되어도 무방하지만, 키 없이는 아무것도 할 수 없다.
SUPABASE_ANON_KEY는 Row Level Security(RLS) 정책을 통과해야만 데이터에 접근할 수 있는 공개 키다. 브라우저에 노출되는 것을 전제로 설계되어 있다.
SUPABASE_SERVICE_ROLE_KEY는 RLS를 완전히 우회한다. 이 키가 클라이언트에 노출되면 테이블 전체를 읽고 쓰고 지울 수 있다.
RLS(Row Level Security): PostgreSQL의 행 단위 접근 제어 기능. Supabase는 이를 기반으로 사용자별 데이터 격리를 구현한다.
클라이언트 (브라우저) 서버 (Next.js API Route / Edge Function)
| |
ANON_KEY SERVICE_ROLE_KEY
| |
v v
Supabase API ----> RLS 정책 적용 Supabase API ----> RLS 우회
| |
허용된 행만 반환 전체 행 반환
anon 키는 RLS 정책이 없는 테이블에서도 기본적으로 모든 데이터를 차단한다. 반드시 RLS를 활성화하고 정책을 작성해야 데이터가 보인다.
.env.local 파일에 아래와 같이 작성한다.
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
# 서버에서만 사용 — NEXT_PUBLIC 접두사 금지
SUPABASE_SERVICE_ROLE_KEY=eyJ...
Next.js에서 NEXT_PUBLIC_ 접두사가 붙은 변수는 번들에 포함되어 브라우저에서 읽힌다. SERVICE_ROLE_KEY에는 절대 이 접두사를 붙이지 않는다.
// lib/supabase/client.ts — 브라우저용
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// lib/supabase/admin.ts — 서버 전용
import { createClient } from '@supabase/supabase-js'
export const adminClient = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
)
adminClient는 API Route, Server Action, Edge Function 안에서만 import해야 한다. 'use client' 컴포넌트에서 import하면 키가 번들에 포함된다.
빌드 후 번들에 서비스 키가 포함됐는지 확인하는 방법이다.
# Next.js 빌드 후 정적 파일에서 키 검색
grep -r "SERVICE_ROLE" .next/static
결과가 나오면 해당 키는 즉시 Supabase 대시보드에서 재발급해야 한다.
RLS가 비활성화된 테이블은 anon 키만으로 전체 데이터를 읽을 수 있다. 테이블을 만들 때 반드시 RLS를 켜고, 최소 권한 원칙에 따라 정책을 작성한다.
-- RLS 활성화
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- 본인 행만 읽기 허용
CREATE POLICY "본인 프로필만 조회"
ON profiles FOR SELECT
USING (auth.uid() = user_id);
SERVICE_ROLE_KEY는 관리자 작업(배치 처리, Webhook, 어드민 페이지)에만 쓰고, 나머지는 anon 키와 RLS 조합으로 처리하는 것이 원칙이다.