supabase의 Auth Providers를 활용해서 소셜 로그인을 구현한 과정을 기록
Supabase에서 제공하는 Kakao Social Login 문서
https://supabase.com/docs/guides/auth/social-login/auth-kakao?queryGroups=language&language=js&queryGroups=environment&environment=server&queryGroups=framework&framework=nextjs
이전 글에서 카카오 개발자 세팅하는 과정을 소개했지만 supabase의 문서에서도 친절하게 안내해주고 있기에 한 번 읽어보는 것을 추천
차이점이라면 Redirect URI에 supabase가 처리해주는 url(https://<project-ref>.supabase.co/auth/v1/callback)을 추가해준다. 그리고 Kakao Client ID와 Kakao Client Secret
를 supabase 프로젝트에 연결해주는 것.


(Next.js를 완전히 이해하고 진행하는 것이 아니라 틀린 점이 있을 수도 있다..)
supabase에서 제공하는 signInWithOAuth()를 실행한다. 이 때 provider와 redirectTo를 파라미터로 전달하는데, provider 종류를 확인하고 redirect URL에서 교환한 코드로 유저 정보를 받아 세션에 정보를 저장한다.
redirect URL에서 코드를 교환하는 로직을 supabase 문서에서 알려주기 때문에 공식 문서를 잘 따라간다면 크게 문제가 없다.
supabase 패키지 설치
https://supabase.com/docs/guides/auth/quickstarts/nextjs
Server-side 세팅
https://supabase.com/docs/guides/auth/server-side/nextjs
.env.localNEXT_PUBLIC_SUPABASE_URL=<your_supabase_project_url>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>
client와 server에서 동작하는 각각의 client 만들기
Supabase는 브라우저에서 동작하는 Client Component client와 서버에서 동작하는 Server Component Client 두 가지 client를 사용한다.
즉, 클라이언트 단에서 서버 client 함수인 createServerClient()를 호출하면 에러
2-1 utils/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function browserClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
2-2 utils.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 {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}
middleware
서버 단에서는 쿠키를 사용할 수 없기 때문에 middleware를 통해서 브라우저의 쿠키에서 auth token을 가져오고 저장한다.
3-1 middlware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'
export async function middleware(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
],
}
3-2 utils/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_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// Do not run code between createServerClient and
// supabase.auth.getUser(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
// IMPORTANT: DO NOT REMOVE auth.getUser()
const {
data: { user },
} = await supabase.auth.getUser()
// route protect 로직
return supabaseResponse
}
로그인 페이지에서는 client component client를 생성
app/login/page.tsx
'use client';
import style from '@/app/(anon)/login/page.module.scss';
import { browserClient } from '@/utils/supabase/client';
import Image from 'next/image';
import { useEffect, useState } from 'react';
const Login = () => {
const signInWithKakao = async () => {
const supabase = browserClient();
await supabase.auth.signInWithOAuth({
provider: 'kakao',
options: {
redirectTo: `http:localhost:3000/auth/callback`,
},
});
};
return (
<div>
<button
className={style.loginButton}
type='button'
onClick={signInWithKakao}
>
<Image
src={'/kakao_login_medium_wide.png'}
alt='카카오 로그인 이미지'
width={300}
height={45}
/>
</button>
</div>
);
};
export default Login;
route handler에서는 server component client를 생성
/app/auth/callback/route.ts
'use server';
import { NextResponse } from 'next/server';
import { createClient } from '@/utils/supabase/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`);
}
로그인 버튼을 클릭하면 redirectTo 주소로 코드를 query parameter에 담아 전달되고, app/auth/callback의 route가 실행되면서 코드를 교환해 유저 정보를 세션에 저장한다.
중간에 오류를 몇 번 마주쳤는데, route를 살펴보면 코드가 없거나 에러가 있으면 ${origin}/auth/auth-code-error 주소로 이동되어서 어디서 오류가 났는지 확인하기 어렵다. 다음 편에서는 오류를 확인할 수 있는 코드를 추가한 내용을 알아보자.