[Next.js + supabase] 카카오 로그인 구현 (2)

ss_kim·2025년 2월 2일

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 IDKakao Client Secret
를 supabase 프로젝트에 연결해주는 것.


들어가기 전

supabase와 Next.js는 어떻게 동작하는 걸까?

(Next.js를 완전히 이해하고 진행하는 것이 아니라 틀린 점이 있을 수도 있다..)

supabase에서 제공하는 signInWithOAuth()를 실행한다. 이 때 provider와 redirectTo를 파라미터로 전달하는데, provider 종류를 확인하고 redirect URL에서 교환한 코드로 유저 정보를 받아 세션에 정보를 저장한다.

redirect URL에서 코드를 교환하는 로직을 supabase 문서에서 알려주기 때문에 공식 문서를 잘 따라간다면 크게 문제가 없다.


구현

Next.js 환경에 supabase Auth 세팅하기

supabase 패키지 설치
https://supabase.com/docs/guides/auth/quickstarts/nextjs

Server-side 세팅
https://supabase.com/docs/guides/auth/server-side/nextjs

  1. .env.local
NEXT_PUBLIC_SUPABASE_URL=<your_supabase_project_url>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>
  1. 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.
          				}
        			},
      			},
    		}
    	)
    }
  2. 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
    }

로그인 페이지

  1. 로그인 페이지에서는 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;
  2. 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 주소로 이동되어서 어디서 오류가 났는지 확인하기 어렵다. 다음 편에서는 오류를 확인할 수 있는 코드를 추가한 내용을 알아보자.

profile
프론트엔드 개발자

0개의 댓글