| 필드 | 값 |
|---|---|
| Application name | 앱 이름 (예: My App) |
| Homepage URL | http://localhost:3000 (개발) 또는 실제 도메인 |
| Authorization callback URL | https://<project-ref>.supabase.co/auth/v1/callback |
중요: Authorization callback URL은 Supabase 프로젝트의 URL을 사용해야된다!
생성 후 표시되는 Client ID와 Client Secret을 복사해둔다.
Supabase 대시보드의 Authentication → URL Configuration에서 다음을 확인합니다:
http://localhost:3000 (개발) 또는 실제 도메인http://localhost:3000/auth/callback 추가npm install @supabase/supabase-js @supabase/ssr
.env.local 파일을 생성하고 다음 내용을 추가한다.
나는 railway로 서버 호스팅을 하고 있어서, railway 환경변수에도 해당 값을 추가했다.
NEXT_PUBLIC_SUPABASE_URL=https://<project-ref>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key>
Supabase 대시보드의 Settings → API에서 확인할 수 있다.
src/lib/supabase/server.ts:
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_ANON_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/client.ts:
import { createBrowserClient } from '@supabase/ssr';
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
}
GitHub 인증 후 리다이렉트를 처리하는 라우트를 구현한다.
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');
let next = searchParams.get('next') ?? '/';
// Open Redirect 공격 방지
if (!next.startsWith('/')) {
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) {
.();
} {
.();
}
}
}
.();
}
next 파라미터가 /로 시작하는지 검증x-forwarded-host 헤더를 사용하여 프로덕션 환경에서 올바른 도메인으로 리다이렉트src/components/GitHubLoginButton.tsx:
'use client';
import { createClient } from '@/lib/supabase/client';
export function GitHubLoginButton() {
const handleLogin = async () => {
const supabase = createClient();
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
};
return (
<button
onClick={handleLogin}
className="flex items-center gap-2 px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800"
>
<GitHubIcon />
GitHub으로 로그인
</button>
);
}
function GitHubIcon() {
return (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
);
}
특정 라우트를 인증된 사용자만 접근하도록 보호한다.
src/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 }) => request.cookies.set(name, value));
supabaseResponse = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options),
);
},
},
},
);
const {
data: { user },
} = await supabase.auth.getUser();
// 보호된 라우트 체크
if (!user && request...()) {
url = request..();
url. = ;
.(url);
}
supabaseResponse;
}
config = {
: [],
};
'use client';
import { createClient } from '@/lib/supabase/client';
import { useRouter } from 'next/navigation';
export function LogoutButton() {
const router = useRouter();
const handleLogout = async () => {
const supabase = createClient();
await supabase.auth.signOut();
router.push('/');
router.refresh();
};
return (
<button onClick={handleLogout}>
로그아웃
</button>
);
}
import { createClient } from '@/lib/supabase/server';
export default async function ProfilePage() {
const supabase = await createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
return <div>로그인이 필요합니다.</div>;
}
return (
<div>
<p>이메일: {user.email}</p>
<p>GitHub 사용자명: {user.user_metadata.user_name}</p>
<img src={user.user_metadata.avatar_url} alt="프로필" />
</div>
);
}
'use client';
import { createClient } from '@/lib/supabase/client';
import { useEffect, useState } from 'react';
import type { User } from '@supabase/supabase-js';
export function UserProfile() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
const supabase = createClient();
supabase.auth.getUser().then(({ data: { user } }) => {
setUser(user);
});
}, []);
if (!user) return null;
return <div>{user.email}</div>;
}
Supabase 대시보드의 Authentication → URL Configuration에서 Redirect URL이 올바르게 설정되어 있는지 확인하자.
로드밸런서나 프록시 뒤에서 실행 중이라면 x-forwarded-host 헤더를 처리하는 로직이 있는지 확인하자.
이제 Next.js 애플리케이션에서 Supabase와 GitHub OAuth를 사용한 인증이 완성되었다. 필요에 따라 Google, Kakao 등 다른 OAuth 제공자도 동일한 방식으로 추가할 수 있다!