tudy.club
BlogResourcesAbout
EN

© 2024 tudy.club

← Back to Blog
Supabase로 유저 DB 구현하기
Ddev

Supabase로 유저 DB 구현하기

개미월드(gaemi.world)를 만들 때 DB는 일부러 안 붙였습니다. 주식 데이터는 전부 외부 API에서 가져오고 있었고, 유저 정보를 저장할 이유가 딱히 없었거든요. NextAuth로 Google 로그인만 달아두고 세션으로만 관리하면 충분했습니다.

2026-02-18•6분 읽기
#Claude#Supabase

Supabase로 유저 DB 구현하기

2026-02-18

개미월드(gaemi.world)를 만들 때 DB는 일부러 안 붙였습니다. 주식 데이터는 전부 외부 API에서 가져오고 있었고, 유저 정보를 저장할 이유가 딱히 없었거든요. NextAuth로 Google 로그인만 달아두고 세션으로만 관리하면 충분했습니다.

일단 두고 Watchlist로서의 기능을 하자! 가 1차 목표였는데요. 1차 목표 달성후 어쨌든 모두가 사용할 수 있는 구조고 로그인은 붙였다보니 기록을 확인하려했는데… 로그인은 되는데 누가 로그인했는지 아무데도 남아있지 않은 구조였습니다. 애널리틱스를 붙였지만 데이터를 신뢰하긴 애매하니까요.

그래서 본격적으로 DB를 붙이는 김에 인증 시스템을 통째로 갈아탔습니다. DB를 따로 세팅하기는 귀찮았고, Supabase를 쓰면 Auth와 DB가 하나로 묶여 있어서 유저가 로그인하면 자동으로 저장됩니다. 스키마 정의도 필요 없고, 대시보드에서 가입자 수를 바로 볼 수 있어요. 제가 원했던 방식이었습니다.

왜 Supabase?

  • 유저 DB가 자동으로 관리됨 (가입자 수 대시보드에서 바로 확인)
  • Auth + DB 올인원
  • Prisma 스키마 정의 없이 바로 시작 가능
  • 무료 티어로 충분

1. 패키지 교체

# Supabase 설치
npm install @supabase/supabase-js @supabase/ssr

# NextAuth + Prisma 제거
npm uninstall next-auth @auth/prisma-adapter @prisma/client prisma

2. Supabase 프로젝트 생성

supabase.com → New Project 생성 후 키 확인:

  • Settings → API Keys 에서:
    • sb_publishable_... (Publishable key) — 기존 anon key 대체
    • sb_secret_... (Secret key) — 기존 service_role 대체

클로드가 anon key를 입력하라고 할텐데, 2025년 11월부터 새 프로젝트는 레거시 anon/service_role 키가 없습니다. Publishable/Secret 키를 사용하면 돼요.

NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx
SUPABASE_SECRET_KEY=sb_secret_xxx

3. Supabase 클라이언트 설정 (3개 파일)

src/lib/supabase/client.ts — 브라우저용

import { createBrowserClient } from '@supabase/ssr';

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
  );
}

src/lib/supabase/server.ts — 서버 컴포넌트/API용

import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';

export async function createClient() {
  const cookieStore = await cookies();

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll();
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            );
          } catch {
            // Server Component에서 호출 시 무시
          }
        },
      },
    }
  );
}

src/lib/supabase/middleware.ts — 세션 갱신용

import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request });

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll();
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          );
          supabaseResponse = NextResponse.next({ request });
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          );
        },
      },
    }
  );

  await supabase.auth.getUser();
  return supabaseResponse;
}

4. useAuth 훅 만들기 (useSession 대체)

// src/hooks/useAuth.ts
'use client';

import { useEffect, useState } from 'react';
import { createClient } from '@/lib/supabase/client';
import type { User } from '@supabase/supabase-js';

export function useAuth() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const supabase = createClient();

  useEffect(() => {
    supabase.auth.getUser().then(({ data: { user } }) => {
      setUser(user);
      setLoading(false);
    });

    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setUser(session?.user ?? null);
        setLoading(false);
      }
    );

    return () => subscription.unsubscribe();
  }, []);

  const signInWithGoogle = () => {
    supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        redirectTo: `${window.location.origin}/auth/callback`,
      },
    });
  };

  const signOut = async () => {
    await supabase.auth.signOut();
    setUser(null);
  };

  return { user, loading, signInWithGoogle, signOut };
}

5. OAuth 콜백 라우트

// src/app/auth/callback/route.ts
import { NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';

export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url);
  const code = searchParams.get('code');

  if (code) {
    const supabase = await createClient();
    await supabase.auth.exchangeCodeForSession(code);
  }

  return NextResponse.redirect(origin);
}

6. 미들웨어 업데이트

// src/middleware.ts
import { NextRequest } from 'next/server';
import { updateSession } from '@/lib/supabase/middleware';

export async function middleware(request: NextRequest) {
  return await updateSession(request);
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};

7. 컴포넌트에서 사용법 변경

Before (NextAuth):

import { useSession, signOut } from 'next-auth/react';

const { data: session, status } = useSession();
const name = session?.user?.name;

After (Supabase):

import { useAuth } from '@/hooks/useAuth';

const { user, loading, signOut } = useAuth();
const name = user?.user_metadata?.full_name;

8. Google OAuth 연결 (수동)

  1. Supabase 대시보드 → Authentication → Providers → Google → Enable → Client ID/Secret 입력

    image.png

  2. Google Cloud Console → OAuth 승인된 리디렉션 URI에 추가:

    Google Cloud Platform
    Google Cloud Platform lets you build, deploy, and scale applications, websites, and services on the same infrastructure as Google.
    console.cloud.google.com

    https://your-project.supabase.co/auth/v1/callback

  3. Vercel 환경변수에 NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY 추가


결과

  • Supabase 대시보드 Authentication → Users 에서 가입자 수 바로 확인 가능
  • NextAuth의 SessionProvider 래핑 불필요
  • Prisma 스키마 정의 없이 유저 자동 저장
  • 패키지 4개 제거, 2개 추가 → 번들 사이즈 감소

결과적으로 NextAuth + Prisma 패키지 4개를 지우고 Supabase 2개를 넣었습니다. 번들도 가벼워졌고, 이제 Supabase 대시보드에서 가입자 목록과 수를 바로 확인할 수 있어요.

개미월드 (gaemi.world)

이 글에서 다룬 개미월드는 서학개미를 위한 기관투자자 포트폴리오 추적 서비스입니다. 워렌 버핏, 캐시 우드, 국민연금 같은 주요 투자자들이 SEC 13F 공시를 통해 어떤 종목을 사고팔았는지 한눈에 확인할 수 있어요. 투자자 간 포트폴리오 비교 기능도 지원하고 있어서, 기관들이 공통으로 담은 종목이나 반대로 엇갈린 포지션도 살펴볼 수 있습니다.

미국 주식 투자하시는 분들은 한번 들러보세요!

버핏·캐시우드·국민연금 포트폴리오 실시간 추적 | 개미월드
SEC 13F 공시 기반으로 워렌 버핏, 캐시 우드, 국민연금 등 유명 투자자의 보유 종목과 매매 내역을 추적하세요.
gaemi.world

목차

  • 왜 Supabase?
  • 1. 패키지 교체
  • 2. Supabase 프로젝트 생성
  • 3. Supabase 클라이언트 설정 (3개 파일)
  • 4. useAuth 훅 만들기 (useSession 대체)
  • 5. OAuth 콜백 라우트
  • 6. 미들웨어 업데이트
  • 7. 컴포넌트에서 사용법 변경
  • 8. Google OAuth 연결 (수동)
  • 결과
  • 개미월드 (gaemi.world)