Supabase + Next.js 풀스택 앱 만들기 — 2026년 실전 프로젝트 구축 완전정복
이 글을 끝까지 읽으면, Supabase와 Next.js를 연동한 풀스택 웹 앱을 처음부터 직접 구축할 수 있습니다. 백엔드 지식이 부족해도 실전 코드와 단계별 가이드로 바로 따라 할 수 있도록 구성했습니다.
안녕하세요, ICT리더 리치입니다. 혹시 이런 고민 해보신 적 있으신가요? "아이디어는 있는데, 백엔드 서버 설정부터 막막하다"는 느낌 말이죠. 저도 몇 년 전, 사이드 프로젝트를 시작할 때마다 AWS 설정, DB 세팅, 인증 구현에만 며칠을 쏟아붓고 정작 핵심 기능 개발은 못 했던 기억이 있습니다. 그런데 Supabase를 만난 순간, 그 흐름이 완전히 바뀌었습니다.
Supabase는 PostgreSQL 기반의 오픈소스 백엔드 플랫폼으로, 인증·DB·스토리지·실시간 기능을 한 번에 제공합니다. 여기에 Next.js 14의 App Router와 Server Actions를 결합하면, 놀랄 만큼 빠르게 프로덕션급 풀스택 앱을 완성할 수 있습니다. 오늘 포스팅에서는 프로젝트 초기 설정부터 인증, DB 연동, 실시간 기능, 배포까지 실전 코드와 함께 완전히 정복해 드리겠습니다.
📌 바로가기 목차
1. 왜 Supabase + Next.js 조합인가? — 풀스택 선택 이유 비교
2026년 현재, 개발자 커뮤니티에서 가장 빠르게 성장하는 풀스택 조합 중 하나가 바로 Supabase + Next.js입니다. Stack Overflow 2025 Developer Survey에 따르면, Next.js는 프론트엔드 프레임워크 중 실사용 만족도 1위를 유지하고 있으며, Supabase는 BaaS(Backend as a Service) 분야에서 Firebase를 제치고 오픈소스 진영의 강자로 자리 잡았습니다. 이 두 기술을 조합하면 서버 한 줄 없이도 인증, 실시간 DB, 파일 스토리지를 한 번에 해결할 수 있죠.
특히 Next.js 14의 App Router와 Server Actions가 등장하면서, 기존에 별도 API Route를 만들던 번거로움이 크게 줄었습니다. 서버 컴포넌트에서 Supabase 클라이언트를 직접 호출하면, 보안도 강화되고 코드도 훨씬 간결해집니다. 사이드 프로젝트부터 스타트업 MVP까지 이 스택이 선택받는 데는 충분한 이유가 있습니다.
💡 다음 섹션 예고: 말만 들어선 와닿지 않죠? 바로 실전 환경 세팅으로 들어갑니다. 프로젝트 생성부터 .env 설정까지, 흔히 막히는 포인트를 정확히 짚어드립니다.
2. 프로젝트 초기 세팅 — 실수 없이 환경 구성하는 법
처음 세팅할 때 가장 많이 막히는 부분이 바로 환경변수 설정과 Supabase 클라이언트를 "어디서" 초기화하느냐입니다. Next.js App Router 환경에서는 서버 컴포넌트용 클라이언트와 클라이언트 컴포넌트용 클라이언트를 분리해야 합니다. 이 차이를 모르면 세션이 유지 안 되거나 인증이 풀리는 버그를 만납니다. 여러분은 혹시 이런 상황 겪어보셨나요?
| 구분 | 사용 위치 | 초기화 방법 | 주의사항 |
|---|---|---|---|
| Server Client | Server Component, Server Actions | createServerClient (cookies) | 쿠키 직접 읽기/쓰기 필요 |
| Browser Client | Client Component ('use client') | createBrowserClient | 싱글톤 패턴 권장 |
| Middleware | middleware.ts (Edge Runtime) | createServerClient (request/response) | 세션 갱신 및 리디렉션 처리 |
| 환경변수 | .env.local | NEXT_PUBLIC_ 접두사 필수 | Service Role Key는 서버에서만 |
아래는 Next.js + Supabase 프로젝트를 처음부터 세팅하는 실전 코드입니다. @supabase/ssr 패키지를 사용하는 2026년 최신 방식입니다.
// lib/supabase/server.ts — 서버 컴포넌트 & Server Actions 전용 클라이언트
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
// Next.js 14+ App Router: cookies()는 async 함수
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) {
// 쿠키 설정 (Server Actions에서 세션 갱신 시 사용)
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// Server Component에서 호출 시 에러 무시 (읽기 전용)
}
},
},
}
)
}
// lib/supabase/client.ts — Client Component 전용 (싱글톤)
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
3. Supabase Auth 연동 — 소셜 로그인 구현 실전 가이드
인증 구현은 풀스택 앱의 핵심이면서 가장 실수가 잦은 영역입니다. Supabase Auth는 이메일/비밀번호, Google, GitHub, Kakao 등 소셜 로그인을 단 몇 줄로 처리할 수 있습니다. 특히 middleware.ts에서 세션을 자동으로 갱신해주는 구조가 Next.js와 완벽하게 맞아떨어집니다. 아래 핵심 구성 요소들을 반드시 모두 구현해야 인증이 올바르게 동작합니다.
- middleware.ts 설정: 모든 라우트에서 세션을 자동 갱신하고, 비인증 사용자를 로그인 페이지로 리디렉션합니다. 이 파일이 없으면 페이지 새로고침 시 세션이 풀립니다.
-
OAuth Callback Route:
app/auth/callback/route.ts에서 인증 코드를 세션으로 교환합니다. Supabase 대시보드의 Redirect URL 설정과 반드시 일치해야 합니다. - Server Action으로 로그인 처리: 클라이언트에서 직접 Supabase를 호출하는 대신 Server Action을 사용하면, 민감한 정보가 브라우저에 노출되지 않아 보안이 강화됩니다.
- Google OAuth 설정: Supabase 대시보드 → Authentication → Providers → Google에서 Client ID와 Secret을 입력하고, Google Cloud Console에서 승인된 리디렉션 URI를 추가합니다.
-
세션 확인 (Server Component):
supabase.auth.getUser()를 사용하세요.getSession()은 보안상 권장하지 않습니다.
// middleware.ts — 세션 자동 갱신 및 보호 라우트 설정
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
// 응답 쿠키 설정 (세션 갱신)
cookiesToSet.forEach(({ name, value, options }) => {
supabaseResponse.cookies.set(name, value, options)
})
},
},
}
)
// 세션 갱신 (반드시 호출)
const { data: { user } } = await supabase.auth.getUser()
// 보호 라우트: 비로그인 시 /login으로 리디렉션
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
return supabaseResponse
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}
⚠️ 주의: middleware.ts에서 반드시 supabase.auth.getUser()를 호출해야 세션이 갱신됩니다. 이 줄을 빼면 로그인 상태가 유지되지 않는 버그가 발생합니다.
4. Database CRUD 구현 — Server Actions로 데이터 다루기
의외로 많은 개발자들이 Next.js에서 Supabase 데이터를 "클라이언트 컴포넌트에서 직접" 호출하는 실수를 합니다. 그렇게 하면 Anon Key가 브라우저에 노출되고, RLS(Row Level Security)가 없으면 전체 데이터가 탈취될 위험이 있습니다. 2026년 현재 가장 권장되는 패턴은 Server Actions를 통해 모든 DB 쓰기 작업을 서버에서 처리하는 방식입니다. GitHub의 한 오픈소스 분석에 따르면, Next.js + Supabase 레포지터리의 73%가 Server Actions 패턴으로 전환 중이라는 통계도 있습니다.
아래는 할 일(Todo) 앱을 예시로 한 완전한 CRUD Server Actions 구현입니다. 이 패턴을 익히면 어떤 도메인에도 바로 적용할 수 있습니다.
// app/actions/todo.ts — Server Actions로 CRUD 완전 구현
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
// [CREATE] 새 할 일 추가
export async function createTodo(formData: FormData) {
const supabase = await createClient()
// 현재 로그인 사용자 확인
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) throw new Error('인증이 필요합니다.')
const title = formData.get('title') as string
if (!title?.trim()) throw new Error('제목을 입력해주세요.')
const { error } = await supabase
.from('todos')
.insert({ title: title.trim(), user_id: user.id, is_done: false })
if (error) throw new Error(error.message)
revalidatePath('/dashboard') // 캐시 갱신
}
// [READ] 내 할 일 목록 조회
export async function getTodos() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return []
const { data, error } = await supabase
.from('todos')
.select('*')
.eq('user_id', user.id)
.order('created_at', { ascending: false })
if (error) throw new Error(error.message)
return data ?? []
}
// [UPDATE] 완료 상태 토글
export async function toggleTodo(id: string, isDone: boolean) {
const supabase = await createClient()
const { error } = await supabase
.from('todos')
.update({ is_done: !isDone })
.eq('id', id)
if (error) throw new Error(error.message)
revalidatePath('/dashboard')
}
// [DELETE] 할 일 삭제
export async function deleteTodo(id: string) {
const supabase = await createClient()
const { error } = await supabase
.from('todos')
.delete()
.eq('id', id)
if (error) throw new Error(error.message)
revalidatePath('/dashboard')
}
💡 실전 팁: revalidatePath()를 잊으면 데이터가 바뀌어도 화면이 갱신되지 않습니다. 모든 CUD(Create/Update/Delete) 액션 마지막에 반드시 추가하세요.
5. 실시간 기능 + RLS 보안 설정 비교 정리
Supabase Realtime은 PostgreSQL의 WAL(Write-Ahead Logging)을 기반으로 DB 변경사항을 WebSocket으로 실시간 구독할 수 있게 해줍니다. 채팅, 협업 툴, 실시간 대시보드에 활용하기 좋습니다. 그런데 Realtime을 활성화하기 전에 반드시 RLS 정책을 설정해야 합니다. RLS 없이 Realtime을 켜면 다른 사용자의 데이터 변경까지 구독될 수 있어 보안 사고로 이어집니다. 이 중에서 가장 중요한 것은 "RLS는 선택이 아닌 필수"라는 점입니다.
| 항목 | RLS 미설정 | RLS 설정 완료 |
|---|---|---|
| 데이터 접근 | 전체 테이블 조회 가능 | 본인 데이터만 접근 |
| Realtime 구독 | 모든 사용자 이벤트 수신 | 본인 관련 이벤트만 수신 |
| 보안 수준 | ⚠️ 심각한 취약점 | ✅ 엔터프라이즈 수준 |
| 설정 난이도 | 없음 | SQL 정책 2~3줄 |
| Supabase 권고 | 프로덕션 절대 비권장 | 모든 테이블에 필수 적용 |
-- RLS 정책 설정 (Supabase SQL Editor에서 실행)
-- 1. todos 테이블 RLS 활성화
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- 2. 본인 데이터만 SELECT 허용
CREATE POLICY "users can read own todos"
ON todos FOR SELECT
USING (auth.uid() = user_id);
-- 3. 본인 데이터만 INSERT 허용
CREATE POLICY "users can insert own todos"
ON todos FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- 4. 본인 데이터만 UPDATE/DELETE 허용
CREATE POLICY "users can update own todos"
ON todos FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "users can delete own todos"
ON todos FOR DELETE
USING (auth.uid() = user_id);
6. Vercel 배포 체크리스트 — 놓치면 안 되는 주의사항
코딩을 다 했는데 배포에서 막히는 경우가 생각보다 많습니다. Vercel에 Next.js + Supabase 앱을 올릴 때는 로컬 환경과 프로덕션 환경의 차이를 꼭 이해해야 합니다. 특히 환경변수 누락, Redirect URL 불일치, Edge Runtime 제한이 3대 원인입니다. 아래 체크리스트를 배포 전 반드시 확인하세요. 다음 FAQ 섹션에서 자주 헷갈리는 배포 관련 질문들도 정리했습니다.
⚠️ 주의: Service Role Key는 절대로 NEXT_PUBLIC_ 접두사로 설정하면 안 됩니다. 이 키는 RLS를 우회하므로 클라이언트에 노출되는 순간 전체 DB가 위험합니다. 서버 전용 환경변수(SUPABASE_SERVICE_ROLE_KEY)로만 사용하세요.
7. 자주 묻는 질문 (FAQ)
네, 가능합니다. Supabase 무료 플랜은 500MB DB, 5GB 스토리지, 월 200만 Auth 요청을 제공합니다. 단, 프로젝트가 7일 이상 비활성화되면 일시 정지될 수 있어 사이드 프로젝트보다는 MVP 검증 용도로 활용하는 것이 좋습니다. 트래픽이 생기면 Pro 플랜($25/월) 전환을 권장합니다. 세팅 방법은 2번 섹션을 참고하세요.
Pages Router는 App Router와 클라이언트 초기화 방식이 다릅니다. Pages Router에서는 getServerSideProps 안에서 쿠키를 수동으로 전달해야 하고, Server Actions를 사용할 수 없습니다. 신규 프로젝트라면 App Router 사용을 강력히 권장하며, 설정 방법은 2번 섹션에서 확인하세요.
가장 흔한 원인은 middleware.ts가 누락되었거나, supabase.auth.getUser() 호출을 빠뜨린 경우입니다. 미들웨어에서 세션 갱신이 이루어지지 않으면 쿠키가 만료되어 로그아웃됩니다. 3번 섹션의 middleware.ts 코드를 그대로 적용하면 해결됩니다.
RLS 자체의 성능 오버헤드는 매우 작습니다. 다만 정책이 복잡하거나 서브쿼리를 많이 사용하면 쿼리 플랜이 복잡해질 수 있습니다. user_id 컬럼에 인덱스를 추가하면 대부분의 성능 문제가 해결됩니다. 자세한 RLS 설정은 5번 섹션을 참고하세요.
99%는 Supabase의 Redirect URL과 Google/GitHub OAuth 설정에 프로덕션 도메인이 등록되지 않아서 발생합니다. Supabase 대시보드 → Authentication → URL Configuration에 https://your-app.vercel.app/auth/callback을 추가하고, Google Cloud Console의 승인된 리디렉션 URI도 동일하게 업데이트하세요. 배포 전 체크리스트는 6번 섹션에서 확인할 수 있습니다. 더 궁금한 점은 댓글로 남겨주세요!
8. 마무리 요약
✅ Supabase + Next.js 풀스택 구축 핵심 정리
Supabase와 Next.js 14 App Router의 조합은 2026년 현재 가장 생산적인 풀스택 개발 스택 중 하나입니다. 서버 클라이언트와 브라우저 클라이언트를 분리해 초기화하고, middleware.ts에서 세션을 자동 갱신하는 구조가 이 스택의 핵심입니다. 모든 DB 쓰기 작업은 Server Actions로 처리해 보안을 강화하고, RLS는 선택이 아닌 필수로 모든 테이블에 적용해야 합니다. Vercel 배포 시에는 환경변수와 OAuth Redirect URL 설정을 반드시 프로덕션 도메인에 맞게 업데이트해야 소셜 로그인이 정상 동작합니다.
지금 당장 할 수 있는 첫 번째 행동은, Supabase 대시보드에서 새 프로젝트를 생성하고, npx create-next-app@latest로 Next.js 앱을 만드는 것입니다. 환경변수 2개만 연결하면 30분 안에 인증이 동작하는 앱을 만들 수 있습니다. 여러분은 어떤 프로젝트에 이 스택을 적용해보고 싶으신가요? 아이디어나 겪으신 트러블슈팅 경험이 있다면 댓글로 공유해주세요! 다음 포스팅에서는 Supabase Edge Functions로 AI API 연동하기를 다룰 예정이니 기대해 주세요.
댓글
댓글 쓰기