Supabase에 대해서는 이전에 storage를 활용하여 간편하게 이미지 파일을 업로드하여 관리를 했었습니다. 이번에는 Supabase를 활용하는 강의를 들으면서 Next와 Supabase로 데이터를 연동하는 부분에 대해 알아가며 정리하는 포스팅입니다.
Supabase는 PostgreSQL 데이터베이스를 기반으로 DB, Auth, Storage, RealTime 기능 등을 제공하는 오픈소스 BaaS(Backend as a Service) 플랫폼 입니다. 즉 서버를 직접 구축하거나 관리하지 않고도 백엔드 기능을 사용할 수 있는 서비스 입니다.
| Firebase | Supabase |
|---|---|
| NoSQL 기반(문서형) | PostgreSQL 기반(관계형) |
| FireStore 보안 규칙 | RLS(Row Level Security) 정책 |
| Google Cloud Storage 기반 | Amazon S3 호환 Storage |
| Google 지원 및 통합 | 오픈 소스 |
| 읽기/쓰기 작업에 따라 비용 증가 | 상대적 저렴하고 유연한 가격 정책 |
Table 생성
먼저 Supabase에 로그인 후 프로젝트를 생성하고 Table Editor에서 Table Columns에서 필요한 데이터를 추가해주면 해당 테이블을 구성할 수 있습니다. 예시로 아래와 같이 todolist에 필요한 데이터를 구성하였습니다.

Table을 구성하면서 아래와 같이 행 수준 보안(RLS)에 대해 선택하는 부분이 나옵니다.
"RLS를 활성화하고 Postgres 정책을 작성하여 테이블에 대한 액세스를 제한합니다." 라는 문구와 함께 사용하는것을 권장하는 부분이 있습니다.

그래서 행 수준 보안 RLS(Row-Level Security)에 대해 알아보자면
데이터베이스 시스템에서 제공하는 보안 기능으로 데이터베이스의 각 행에 대한 접근을 제어하는 보안 기능입니다. 즉 특정 사용자 또는 해당 조건에 따라서 행의 접근 제어를 설정할 수 있습니다.
// todo insert
alter policy "Enable insert for authenticated users only"
on "public"."todos_with_rls"
to authenticated
with check (
true
);
위 예시로 todos_with_rls 테이블의 INSERT 작업을 인증된 사용자만 추가할 수 있도록 정책을 설정했습니다.
만약 사용자가 로그인 인증을 하지 않은 상태에서 INSERT 작업을 시도하면 WITH CHECK 조건을 만족하지 않으므로 Row-Level Security 정책을 위반했다는 에러 메시지가 아래와 같이 반환됩니다.

그래서 RLS(Row-Level Security) 정책은 데이터베이스 보안을 강화하고 사용자별 데이터 접근 및 조작을 제한하기 위해 권장됩니다.
그리고 Generating TypeScript Types를 활용하여 아래의 명령어를 실행하면 생성한 데이터베이스 테이블 스키마를 바탕으로 database.types.ts 파일에 자동으로 type을 생성할 수 있습니다.
npx supabase gen types typescript --project-id "$PROJECT_REF" --schema public > database.types.ts
OAuth
Google OAuth를 활용하여 로그인을 진행합니다.
Supabase 문서의 Login With Google을 참고하여 Google Cloud Platform 프로젝트 설정부터 Supabase Auth Providers의 Google의 클라이언트 id, pw, callback url을 설정할 수 있습니다.
클라이언트 id, pw, callback url 설정을 마치면 Google 로그인을 활성화 해주고 저장해줍니다.
const handleGoogleLogin = async () => {
await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: process.env.NEXT_PUBLIC_AUTH_REDIRECT_URL,
},
});
};
위 처럼 해당 provider와 redirect를 입력하고 signInWithOAuth로 로그인을 합니다.
그리고 콜백 엔드포인트에서 코드 교환을 통해서 사용자 세션을 쿠키에 저장할 수 있습니다.
이 코드 교환 부분을 살펴보면 로그인 후에 redirect url로 이동하면서 아래와 같이 code값도 파라미터로 함께 전달합니다.
http://localhost:3000/auth/callback?code=88faca49-836e-42b4-8b35-5c177143cbdc
그래서 Supabase의 Google Login 문서를 참고하면 아래와 같이 /auth/callback/route.ts 파일을 생성해서 인증 과정에서 받은 코드를 Supabase.auth.exchangeCodeForSession(code)에 넣고 호출하여 인증코드를 세션으로 반환 받습니다.
// auth/callback/route.ts
import { createServerSideClient } from "@/lib/supabase";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url)
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) {
const forwardedHost = request.headers.get('x-forwarded-host')
const isLocalEnv = process.env.NODE_ENV === 'development'
if (isLocalEnv) {
return NextResponse.redirect(`${origin}${next}`)
} else if (forwardedHost) {
return NextResponse.redirect(`https://${forwardedHost}${next}`)
} else {
return NextResponse.redirect(`${origin}${next}`)
}
}
}
return NextResponse.redirect(`${origin}/auth/auth-code-error`)
}
위처럼 코드 교환이 완료되면 구글 로그인이 이루어지고 메인 페이지로 라우팅 되며 세션 정보가 쿠키에 저장되는것을 확인할 수 있습니다.
여기서 로그인이 완료되면 supabase.auth.getUser()로 유저 정보(id, email, name, provider 등등)를 가져와서 로그인 상태를 알 수 있습니다.
데이터 가져오기
server action을 활용해서 데이터를 가져오려면 서버측에서 supabase 클라이언트를 생성해야합니다. 공식문서의 Next.js에 대한 서버 측 인증 설정을 참고하여 설정을 합니다.
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient(serverComponent = false) {
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) {
if (serverComponent) return;
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
},
},
}
)
}
아래와 같이 server action을 통해서 todos_with_rls 테이블에서 userId에 맞는 데이터만 가져오고 콘솔을 찍어보면 아래와 같이 테스트 todo list를 잘 가지고 온 것을 확인할 수 있습니다.
// user id 별로 데이터 가져오기
export const getTodosByUserId = async (userId: string) => {
const supabase = await createServerSideClient(true);
const result = await supabase
.from("todos_with_rls")
.select("*")
.is("deleted_at", null)
.eq("user_id", userId)
.order("id", { ascending: false });
return result.data;
};

테이블 데이터를 가져온 부분처럼 Todo 데이터 생성, 수정, 삭제등 action 함수를 추가 해주면 Next와 Supabase를 활용한 Todolist가 완성됩니다.