architecture2025-02-25·9 min·173/348

Vercel에서 외부 DB 연결 (Supabase)

Vercel에서 Supabase 데이터베이스를 연결하고 최적화하는 방법을 알아봅니다.

Vercel에서 외부 DB 연결 (Supabase)

Introduction

Supabase는 오픈소스 Firebase 대안으로 PostgreSQL 기반의 완전한 백엔드 서비스를 제공합니다. Vercel에서 Supabase를 연결하고 최적화하는 방법을 알아보겠습니다.

Environment

  • Vercel 프로젝트 (Next.js 14+)
  • Supabase 프로젝트
  • Prisma ORM
  • PostgreSQL

Problem

Vercel에서 Supabase 연결 시 발생하는 문제:

// 연결 풀 문제
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
);

// 서버리스 환경에서 연결 한도 초과

Analysis

Supabase 연결 고려사항:

  1. 연결 풀: PostgreSQL 연결 풀 크기 제한
  2. 네트워크 지연: Vercel과 Supabase 간 네트워크 지연
  3. Cold Start: 서버리스 함수 시작 시 연결 지연
  4. 비용: 연결 수에 따른 비용 발생

Solution

1. Supabase 클라이언트 설정

// lib/supabase.js
import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;

// 클라이언트용 (브라우저)
export const supabase = createClient(supabaseUrl, supabaseAnonKey);

// 서버용 (서비스 롤)
export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey, {
  auth: {
    autoRefreshToken: false,
    persistSession: false,
  },
});

2. Prisma와 Supabase 연결

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}
# .env.local
DATABASE_URL="postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres?pgbouncer=true&connection_limit=20"
DIRECT_URL="postgresql://postgres:[password]@db.[project-ref].supabase.co:5432/postgres"

3. 연결 풀 최적화

// lib/prisma.js
import { PrismaClient } from '@prisma/client';

const globalForPrisma = globalThis;

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === 'development' ? ['query'] : ['error'],
  datasources: {
    db: {
      url: process.env.DATABASE_URL,
    },
  },
});

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}

// 연결 상태 확인
export async function checkDatabaseConnection() {
  try {
    await prisma.$connect();
    console.log('Database connected successfully');
    return true;
  } catch (error) {
    console.error('Database connection failed:', error);
    return false;
  }
}

4. Supabase Realtime 구독

// lib/supabase-realtime.js
import { supabase } from './supabase';

export function subscribeToTable(tableName, callback) {
  return supabase
    .channel(`${tableName}-changes`)
    .on(
      'postgres_changes',
      { event: '*', schema: 'public', table: tableName },
      (payload) => {
        console.log('Change received:', payload);
        callback(payload);
      }
    )
    .subscribe();
}

// 사용 예시
const subscription = subscribeToTable('messages', (payload) => {
  if (payload.eventType === 'INSERT') {
    console.log('New message:', payload.new);
  }
});

5. 서버리스 함수에서 사용

// app/api/data/route.js
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';

export async function GET() {
  try {
    const data = await prisma.user.findMany({
      take: 10,
      orderBy: { createdAt: 'desc' },
    });
    
    return NextResponse.json(data);
  } catch (error) {
    console.error('Database query failed:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

export async function POST(request) {
  try {
    const body = await request.json();
    
    const user = await prisma.user.create({
      data: {
        email: body.email,
        name: body.name,
      },
    });
    
    return NextResponse.json(user, { status: 201 });
  } catch (error) {
    console.error('Database insert failed:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

6. 에러 핸들링 강화

// lib/prisma-safe.js
import { prisma } from './prisma';

export async function safeQuery(queryFn) {
  try {
    const result = await queryFn(prisma);
    return { success: true, data: result };
  } catch (error) {
    console.error('Query failed:', error);
    
    // 연결 에러인 경우 재시도
    if (error.code === 'P1001' || error.code === 'P1017') {
      await prisma.$disconnect();
      // 재시도 로직
    }
    
    return { success: false, error: error.message };
  }
}

// 사용 예시
const { success, data, error } = await safeQuery((prisma) =>
  prisma.user.findMany()
);

7. 환경 변수 설정

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
DATABASE_URL=your-database-url
DIRECT_URL=your-direct-url

Lessons Learned

  1. 연결 풀 관리: PostgreSQL 연결 풀 크기를 적절히 설정해야 합니다.
  2. Prisma 싱글톤: Prisma 클라이언트를 싱글톤으로 관리하면 연결을 효율적으로 사용할 수 있습니다.
  3. 에러 핸들링: 연결 에러에 대한 적절한 핸들링과 재시도 로직이 필요합니다.
  4. 보안: 서비스 롤 키는 서버에서만 사용하고 클라이언트에 노출되지 않도록 해야 합니다.
  5. 모니터링: 데이터베이스 연결 상태를 모니터링하여 문제를 조기에 발견해야 합니다.

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.