next.js + supabase 데이터 연동

oweaj·2025년 2월 21일
post-thumbnail

Supabase에 대해서는 이전에 storage를 활용하여 간편하게 이미지 파일을 업로드하여 관리를 했었습니다. 이번에는 Supabase를 활용하는 강의를 들으면서 Next와 Supabase로 데이터를 연동하는 부분에 대해 알아가며 정리하는 포스팅입니다.


  • Supabase 란

Supabase는 PostgreSQL 데이터베이스를 기반으로 DB, Auth, Storage, RealTime 기능 등을 제공하는 오픈소스 BaaS(Backend as a Service) 플랫폼 입니다. 즉 서버를 직접 구축하거나 관리하지 않고도 백엔드 기능을 사용할 수 있는 서비스 입니다.

  • Firebase와 Supabase의 비교
    여러 BaaS 서비스가 있지만 Firebase와 Supabase의 차이를 비교하는 자료를 많이 볼 수 있을 정도로 두 서비스가 많이 사용됩니다. 그리고 Supabase 공식페이지에 들어가서 바로 보이는 텍스트를 보면 "Supabase는 오픈소스 Firebase 대안입니다." 라는 부분의 문구를 볼 수 있습니다. 그럼 아래와 같이 두 서비스의 특징을 보면서 비교해볼 수 있습니다.
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가 완성됩니다.


🖇️ Reference

profile
데굴데굴데굴데굴데굴

0개의 댓글