
Supabase 연동 가이드 - 05편. DB 설계부터 RLS까지
Supabase와 PostgreSQL로 B2B 플랫폼의 데이터베이스를 설계합니다.
이 글에서는 프로젝트 생성, API 키 설정, 테이블 설계, RLS 보안 정책을 다룹니다.
🗄️ 데이터베이스가 필요했다
프로젝트 구조가 잡혔습니다. 이제 데이터를 저장할 곳이 필요했습니다.
Firebase를 써볼까 했는데, NoSQL이 익숙하지 않았습니다. 그리고 요금 폭탄 썰이 무서웠습니다. 갑자기 수십만 원 나왔다는 이야기를 들은 적이 있거든요.
Claude에게 물어봤습니다.
데이터베이스 서비스 추천해줘.
무료 플랜이 넉넉하고, SQL을 쓸 수 있으면 좋겠어.
Claude가 추천한 건 Supabase였습니다.
PostgreSQL 기반이라 SQL을 자유롭게 쓸 수 있고, 인증과 스토리지까지 올인원으로 제공합니다. 무료 플랜도 개인 프로젝트엔 충분했습니다.
| Firebase | Supabase | |
|---|---|---|
| DB | NoSQL | PostgreSQL |
| 쿼리 | 제한적 | SQL 자유롭게 |
| 가격 | 복잡 | 단순 |
| 오픈소스 | X | O |
선택은 쉬웠습니다.
🚀 Supabase 프로젝트 생성
supabase.com에 접속해서 GitHub 계정으로 로그인했습니다.
New Project를 클릭하고 설정을 입력했습니다.
Organization: (자동 생성됨)
Project name: kmetaro
Database Password: (복잡하게 설정)
Region: Northeast Asia (Tokyo)
리전은 한국과 가까운 도쿄를 선택했습니다. 2-3분 기다리니 프로젝트가 생성됐습니다.
대시보드가 열렸을 때 좀 설렜습니다. 진짜 데이터베이스가 생겼구나.
🔑 API 키 설정
Supabase를 코드에서 사용하려면 API 키가 필요합니다.
Settings → API에서 두 개의 키를 찾았습니다.
프로젝트 루트에 .env.local 파일을 만들었습니다.
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxxx...
두 가지 키의 차이
| 키 | 용도 | RLS 적용 |
|---|---|---|
anon (공개) |
클라이언트/일반 API | 적용됨 |
service_role (비밀) |
서버 전용/관리자 작업 | 우회 |
service_role 키는 보안 정책을 무시합니다. 서버에서만 쓰고, 절대 클라이언트에 노출하면 안 됩니다.
# 관리자 기능에만 사용 (서버 사이드에서만!)
SUPABASE_SERVICE_ROLE_KEY=eyJxxxx...
.env.local 파일은 .gitignore에 포함되어 있어서 GitHub에 올라가지 않습니다. 확인했습니다.
📊 테이블 설계
이제 진짜 중요한 부분입니다. 데이터 구조를 설계해야 했습니다.
Claude에게 이렇게 요청했습니다.
B2B 견적 플랫폼에 필요한 테이블을 설계해줘.
필요한 기능:
1. 회원 관리 (고객, 관리자)
2. 제품 관리
3. 문의 관리
4. 견적 관리
각 테이블의 컬럼과 관계를 설계해줘.
Claude가 SQL을 작성해줬습니다.
-- 1. 회원 테이블
create table users (
id uuid primary key default gen_random_uuid(),
email text unique not null,
name text,
phone text,
company text,
role text default 'customer',
created_at timestamptz default now()
);
-- 2. 제품 테이블
create table products (
id uuid primary key default gen_random_uuid(),
name_ko text not null,
name_ja text,
description_ko text,
description_ja text,
category text,
is_active boolean default true,
created_at timestamptz default now()
);
-- 3. 문의 테이블
create table inquiries (
id uuid primary key default gen_random_uuid(),
user_id uuid references users(id),
product_id uuid references products(id),
content text not null,
status text default 'pending',
created_at timestamptz default now()
);
-- 4. 견적 테이블
create table quotations (
id uuid primary key default gen_random_uuid(),
inquiry_id uuid references inquiries(id),
user_id uuid references users(id),
total_amount integer not null,
status text default 'draft',
valid_until date,
created_at timestamptz default now()
);
-- 5. 견적 항목 테이블
create table quotation_items (
id uuid primary key default gen_random_uuid(),
quotation_id uuid references quotations(id),
product_id uuid references products(id),
quantity integer not null,
unit_price integer not null,
created_at timestamptz default now()
);
Supabase 대시보드의 SQL Editor에서 이 쿼리를 실행했습니다.
테이블들이 생성됐습니다.
🔒 RLS: 보안의 핵심
테이블을 만들고 나서 Claude가 물었습니다.
RLS 정책도 설정해드릴까요?
보안을 위해 꼭 필요합니다.
RLS가 뭔지 몰랐습니다. 물어봤습니다.
Row Level Security. 행 단위로 접근을 제어하는 기능이었습니다.
"이 데이터는 누가 볼 수 있는가?"를 DB 레벨에서 제어합니다.
RLS가 없으면?
클라이언트에서 모든 데이터에 접근할 수 있습니다. 다른 사용자의 견적도 볼 수 있습니다. 위험합니다.
RLS가 있으면?
"자기 데이터만 볼 수 있다" 같은 규칙을 적용할 수 있습니다.
-- RLS 활성화
alter table quotations enable row level security;
-- 정책 생성: 사용자는 자기 견적만 볼 수 있다
create policy "Users can view own quotations"
on quotations
for select
using (user_id = auth.uid());
이렇게 하면 로그인한 사용자는 자기 견적만 조회할 수 있습니다. 다른 사람 건 안 보입니다.
자주 쓰는 패턴
프로젝트에서 사용한 RLS 정책들입니다.
-- 누구나 읽기 가능 (제품 목록)
create policy "Anyone can view products"
on products for select
using (is_active = true);
-- 로그인한 사용자만 문의 작성 가능
create policy "Authenticated users can insert"
on inquiries for insert
with check (auth.uid() is not null);
-- 자기 데이터만 수정 가능
create policy "Users can update own data"
on users for update
using (id = auth.uid());
-- 관리자는 모든 작업 가능
create policy "Admins have full access"
on quotations for all
using (
exists (
select 1 from users
where users.id = auth.uid()
and users.role = 'admin'
)
);
처음엔 복잡해 보였는데, 패턴을 알고 나니 반복이었습니다.
🛠️ Supabase 클라이언트 설정
테이블과 보안 정책이 준비됐습니다. 이제 코드에서 연결할 차례입니다.
npm install @supabase/supabase-js @supabase/ssr
Claude에게 클라이언트 파일을 요청했습니다.
Supabase 클라이언트 파일을 만들어줘.
브라우저용, 서버용 분리해서.
// 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/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options)
})
},
},
}
)
}
브라우저에서 쓸 때는 client.ts, 서버 컴포넌트나 Server Action에서 쓸 때는 server.ts를 씁니다.
📝 CRUD 사용법
이제 데이터를 다룰 수 있습니다.
조회
const supabase = await createClient()
const { data: products, error } = await supabase
.from('products')
.select('*')
.eq('is_active', true)
.order('created_at', { ascending: false })
삽입
const { data, error } = await supabase
.from('inquiries')
.insert({
user_id: userId,
product_id: productId,
content: '문의 내용입니다',
})
.select()
.single()
수정
const { error } = await supabase
.from('inquiries')
.update({ status: 'completed' })
.eq('id', inquiryId)
삭제
const { error } = await supabase
.from('products')
.delete()
.eq('id', productId)
SQL을 알면 직관적입니다.
⚠️ 삽질했던 것들
"Invalid API key"
환경 변수를 복사할 때 앞뒤 공백이 들어갔습니다. 그걸 찾느라 30분 헤맸습니다.
RLS로 데이터가 안 보임
테이블에 RLS를 활성화했는데 정책을 안 만들었습니다. 정책이 없으면 모든 접근이 차단됩니다.
개발 중엔 일단 이렇게 해두고 나중에 수정했습니다.
create policy "Allow all"
on products
for all
using (true);
💡 테이블 설계 팁
Claude에게 배운 것들입니다.
UUID를 쓰자
id uuid primary key default gen_random_uuid()
숫자 ID보다 안전하고, 분산 시스템에 유리합니다.
시간은 timestamptz
created_at timestamptz default now()
타임존 처리가 편합니다.
상태값은 문자열
status text default 'pending'
숫자(0, 1, 2)보다 읽기 쉽습니다. 나중에 봤을 때 뭔지 바로 알 수 있습니다.
📋 이번 편 요약
| 항목 | 내용 |
|---|---|
| 서비스 | Supabase (PostgreSQL 기반 BaaS) |
| 핵심 기능 | DB + Auth + Storage 올인원 |
| 보안 | RLS로 행 단위 접근 제어 |
| 클라이언트 | 브라우저용/서버용 분리 |
| 팁 | UUID 사용, timestamptz, 문자열 상태값 |
다음 편에서는 TypeScript 타입을 자동 생성하는 방법을 다룹니다.
💬 DB 설계에 대해 궁금한 점 있으면 댓글로 남겨주세요!
'바이브코딩' 카테고리의 다른 글
| 07. 빠른 스타일링 설정 Tailwind CSS + shadcn/ui (0) | 2026.01.14 |
|---|---|
| 06. Supabase CLI 활용법 -TypeScript 타입 자동 생성 (0) | 2026.01.13 |
| 04. 프로젝트 구조와 폴더 설계 (App Router) (0) | 2026.01.11 |
| 03. 개발 환경 세팅 - Claude Code 설치부터 (0) | 2026.01.11 |
| 02. 기술 스택 선정 - 왜 이 조합인가 (0) | 2026.01.11 |