Next.js에서 Supabase로 토큰 관리하기

고기호·2024년 10월 9일
1

만원 챌린지

목록 보기
1/3
post-custom-banner

초기 삽질 과정

처음에는 supabase client client를 이용해서 로그인과 회원가입을 구현을 했다. 하지만 미들웨어를 이용해서 새로고침 또는 페이지 이동시 로그인 상태가 아니라면 로그인 페이지로 이동하게 끔 구현을 하고 싶었는데, supabase client client의 경우 토큰을 스토리지에 랜덤한 문자열로 저장을 해서 토큰을 보낼 수가 없었다. 또한 개인적으로 스토리지로 토큰을 관리하는 것을 꺼리기도 했다.

그래서 생각해본 플로우가 브라우저에서 로그인 또는 회원가입을 하면 Next.js API Routes를 이용해 서버에서 supabase server client를 이용해 토큰을 받고 쿠키에 담아 클라이언트로 응답하는 방식이였다.

그런데 막상 구현해보니 supabase server client로 로그인 또는 회원가입을 하면 자동으로 쿠키에 토큰을 담아주는 것을 확인했다. 공식문서를 다시 확인해보니 PKCE 흐름이라는 것을 이용해 토큰을 주고 받고, 로그인 또는 회원가입 인증에 성공하면 supabase server client가 자동으로 세션정보를 쿠키에 담아주고, 브라우저에 응답을 보낼때 응답 헤더에 쿠키를 담아준다는 것을 알게 됐다.

결국 공식 문서에 나와있는 코드의 몇가지 부분을 고쳐서 사용하기만 하면 되는 것이였다...(역시 해답은 공식문서다.)

다이어그램으로 흐름을 짜보자

구현을 해보자

1. 패키지 설치

npm install @supabase/ssr @supabase/supabase-js

2. supabase 서버 클라이언트 설정

import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'

export function createClient() {
  const cookieStore = 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.
          }
        },
      },
    }
  )
}

3. middleware 설정

// middleware.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: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * Feel free to modify this pattern to include more paths.
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}
// 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_API_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)
                    );
                },
            },
        }
    );

    const {
        data: { user },
    } = await supabase.auth.getUser();

    if (
        !user &&
        !request.nextUrl.pathname.startsWith("/signIn") &&
        !request.nextUrl.pathname.startsWith("/signUp")
    ) {
        const url = request.nextUrl.clone();
        url.pathname = "/signIn";
        return NextResponse.redirect(url);
    }

    return supabaseResponse;
}

4. 로그인 페이지 구현

// signIn.tsx
"use client";

import { signIn } from "./actions";
import Button from "./Button";
import Input from "./Input";
import Label from "./Label";
import useSignInForm from "./SignInForm.hook";

export default function SignInForm() {
    const { value, handleChangeEmail, handleChangePassword } = useSignInForm();

    return (
        <form className="flex flex-col gap-6 p-6">
            <div className="flex flex-col gap-4">
                <Label htmlFor="email" text="이메일">
                    <Input
                        name="email"
                        type="email"
                        placeholder="이메일을 입력해주세요."
                        value={value.email}
                        onChange={handleChangeEmail}
                    />
                </Label>

                <Label htmlFor="password" text="비밀번호">
                    <Input
                        name="password"
                        type="password"
                        placeholder="비밀번호를 입력해주세요."
                        value={value.password}
                        onChange={handleChangePassword}
                    />
                </Label>
            </div>

            <Button text="로그인" formAction={signIn} />
        </form>
    );
}

5. server action으로 로직 구현

"use server";

import { createClient } from "@/supabase/server";
import { redirect } from "next/navigation";

export async function signIn(formData: FormData) {
    const supabase = createClient();

    const data = {
        email: formData.get("email") as string,
        password: formData.get("password") as string,
    };

    const response = await supabase.auth.signInWithPassword(data);

    if (response.error) {
        return console.log(response.error.message);
    }

    redirect("/home");
}

Reference

https://supabase.com/docs/guides/auth/server-side/advanced-guide
https://supabase.com/docs/guides/auth/server-side/nextjs

profile
웹 개발자 고기호입니다.
post-custom-banner

0개의 댓글