09. Supabase 연동 (Supabase Integration)

2026년 4월 30일 수정됨

📋 개요

Supabase와 Drizzle ORM을 연동하여 Auth, RLS, Edge Functions 등 다양한 기능을 활용하는 방법을 다룹니다.

📋 목차

Supabase는 단순한 DB가 아니라 Backend-as-a-Service 입니다. Drizzle을 Supabase의 다양한 기능(Auth, RLS, Edge Functions)과 함께 200% 활용하는 방법을 다룹니다.


🔌 1. 연결 설정: Transaction(6543) vs Session(5432)

Supabase 대시보드 > Settings > Database > Connection pooling 정보를 확인하세요.

모드포트용도비고
Session Mode5432Migrationdrizzle-kit은 직접 연결이 필요합니다.
Transaction Mode6543ApplicationNestJS, Next.js 등 앱 실행 시. (Supavisor 사용)

🚨 주의: prepare: false

Transaction Mode(6543)는 쿼리를 미리 준비하는 Prepared Statement 기능을 지원하지 않습니다. (Supavisor 제한사항). 따라서 Drizzle 설정에서 반드시 꺼야 합니다.

// src/db/index.ts
import postgres from 'postgres';
import { drizzle } from 'drizzle-orm/postgres-js';
 
const connectionString = process.env.DATABASE_URL!; // 6543 포트 주소
const client = postgres(connectionString, {
  prepare: false // 👈 이거 안 하면 "prepared statement already exists" 에러 발생
});
 
export const db = drizzle(client);

🔐 2. RLS (Row Level Security)와 Drizzle

Supabase의 강력한 보안 기능인 RLS는 "누가 요청했느냐" 에 따라 데이터를 보여주거나 숨깁니다.

Drizzle은 보통 "Admin"이다

NestJSNext.js API Route에서 실행되는 Drizzle은 보통 Service Role Key (관리자 키)를 사용하거나, DB 접속 정보를 직접 가지고 있습니다. 이 경우 RLS를 우회(Bypass) 합니다. (슈퍼 유저 권한)

따라서 백엔드 코드에서 직접 권한 검사를 해야 합니다.

// users.service.ts
async findMyPosts(userId: number) {
  return db.query.posts.findMany({
    // RLS가 자동으로 안 막아주므로, where절로 직접 필터링 필수
    where: (posts, { eq }) => eq(posts.authorId, userId),
  });
}

RLS를 적용하고 싶다면? (Advanced)

Drizzle 쿼리 실행 전에 현재 유저 세션을 DB에 설정해주는 방식을 씁니다. (Postgres 세션 변수 활용)

await db.transaction(async (tx) => {
  // 1. 현재 트랜잭션 내에서 유저 ID 설정 (Supabase가 인식하도록)
  await tx.execute(sql`set local request.jwt.claim.sub = ${userId}`);
 
  // 2. 이제 쿼리하면 RLS가 작동함
  return tx.query.posts.findMany();
});

🌍 3. Edge Functions (Deno 기반)

Supabase Edge Functions에서 Drizzle을 쓸 땐 postgres.js 대신 가벼운 드라이버나 HTTP 연결을 고려해야 합니다.

// deno.json
{
  "imports": {
    "drizzle-orm": "npm:drizzle-orm@...",
    "postgres": "npm:postgres@..."
  }
}

Deno 환경에서도 npm: 접두어를 통해 똑같이 Drizzle을 사용할 수 있습니다. 단, 연결 수 관리 를 위해 Transaction Mode(Pooling) 사용이 필수입니다.


🚀 4. 배포 전 체크리스트

  1. DATABASE_URL이 6543(Transaction) 포트인가?
  2. DIRECT_URL이 5432(Session) 포트인가?
  3. drizzle.config.tsDIRECT_URL을 바라보는가?
  4. 프로덕션 DB 백업은 설정되었는가? (Supabase Pro 플랜 이상 권장)

📝 마무리 퀴즈

Q1. Supabase에서 앱 런타임은 보통 6543(Transaction Mode), 마이그레이션은 5432(Session Mode)를 쓰는 이유는 무엇인가요?

정답: 앱은 풀링된 연결이 필요하고, Drizzle Kit 마이그레이션은 직접 세션 연결이 더 안전하기 때문입니다.

💡 상세 해설: Transaction Mode는 앱의 많은 요청을 풀링해 연결 수를 관리하는 데 적합합니다. 마이그레이션 도구는 세션 단위 동작이 필요할 수 있어 Direct URL을 분리하는 편이 안전합니다.

Q2. Supabase Transaction Mode에서 prepare: false를 설정해야 하는 이유는 무엇인가요?

정답: Supavisor의 Transaction Mode에서는 prepared statement가 제한될 수 있어 Drizzle/postgres.js의 prepared statement 사용을 꺼야 합니다.

💡 상세 해설: 설정을 빼먹으면 prepared statement already exists 같은 런타임 오류를 만날 수 있습니다. 로컬 5432 세션 연결에서는 괜찮다가 운영 6543 풀링 연결에서만 발생할 수 있어 더 위험합니다.

Q3. 영철이의 테스트 타임: 백엔드 Drizzle이 Service Role 권한으로 Supabase에 접속합니다. findMyPosts에서 where authorId = userId를 빼면 왜 위험한가요?

정답: Service Role은 RLS를 우회할 수 있으므로 백엔드 코드가 직접 소유권 필터를 걸지 않으면 다른 사용자의 데이터가 노출될 수 있습니다.

💡 상세 해설: Supabase RLS가 항상 자동으로 막아줄 거라고 가정하면 안 됩니다. 서버 코드가 관리자 권한으로 DB에 붙는 구조라면 인증된 사용자 ID를 쿼리 조건에 명시해야 합니다.

🐣 영철이의 퇴근 일기

오늘은 Supabase 연동이 URL 하나 복사해서 붙이는 일이 아니라는 걸 배웠다. 5432와 6543 포트가 단순 숫자가 아니라, 마이그레이션과 앱 런타임의 연결 방식 차이를 나타낸다는 게 인상적이었다.

가장 크게 남은 건 RLS였다. 백엔드가 Service Role로 접속한다면 Supabase가 알아서 막아줄 거라고 믿으면 안 된다. where authorId = userId 한 줄이 사용자 데이터 경계를 지키는 실제 방어선일 수 있다.

💡 "Supabase를 쓸 때 보안은 대시보드 설정과 백엔드 쿼리가 함께 완성한다."

내일부터는 DB URL, prepare: false, Direct URL, RLS 우회 여부를 체크리스트로 보려고 한다.

🔗 레퍼런스