
개미월드(gaemi.world)를 만들 때 DB는 일부러 안 붙였습니다. 주식 데이터는 전부 외부 API에서 가져오고 있었고, 유저 정보를 저장할 이유가 딱히 없었거든요. NextAuth로 Google 로그인만 달아두고 세션으로만 관리하면 충분했습니다.
개미월드(gaemi.world)를 만들 때 DB는 일부러 안 붙였습니다. 주식 데이터는 전부 외부 API에서 가져오고 있었고, 유저 정보를 저장할 이유가 딱히 없었거든요. NextAuth로 Google 로그인만 달아두고 세션으로만 관리하면 충분했습니다.
일단 두고 Watchlist로서의 기능을 하자! 가 1차 목표였는데요. 1차 목표 달성후 어쨌든 모두가 사용할 수 있는 구조고 로그인은 붙였다보니 기록을 확인하려했는데… 로그인은 되는데 누가 로그인했는지 아무데도 남아있지 않은 구조였습니다. 애널리틱스를 붙였지만 데이터를 신뢰하긴 애매하니까요.
그래서 본격적으로 DB를 붙이는 김에 인증 시스템을 통째로 갈아탔습니다. DB를 따로 세팅하기는 귀찮았고, Supabase를 쓰면 Auth와 DB가 하나로 묶여 있어서 유저가 로그인하면 자동으로 저장됩니다. 스키마 정의도 필요 없고, 대시보드에서 가입자 수를 바로 볼 수 있어요. 제가 원했던 방식이었습니다.
# Supabase 설치
npm install @supabase/supabase-js @supabase/ssr
# NextAuth + Prisma 제거
npm uninstall next-auth @auth/prisma-adapter @prisma/client prisma
supabase.com → New Project 생성 후 키 확인:
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
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;
}
// 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 };
}
// 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);
}
// 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)$).*)',
],
};
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;
Supabase 대시보드 → Authentication → Providers → Google → Enable → Client ID/Secret 입력

Google Cloud Console → OAuth 승인된 리디렉션 URI에 추가:
https://your-project.supabase.co/auth/v1/callback
Vercel 환경변수에 NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY 추가
결과적으로 NextAuth + Prisma 패키지 4개를 지우고 Supabase 2개를 넣었습니다. 번들도 가벼워졌고, 이제 Supabase 대시보드에서 가입자 목록과 수를 바로 확인할 수 있어요.
이 글에서 다룬 개미월드는 서학개미를 위한 기관투자자 포트폴리오 추적 서비스입니다. 워렌 버핏, 캐시 우드, 국민연금 같은 주요 투자자들이 SEC 13F 공시를 통해 어떤 종목을 사고팔았는지 한눈에 확인할 수 있어요. 투자자 간 포트폴리오 비교 기능도 지원하고 있어서, 기관들이 공통으로 담은 종목이나 반대로 엇갈린 포지션도 살펴볼 수 있습니다.
미국 주식 투자하시는 분들은 한번 들러보세요!