본문 바로가기
React TIL

[React] Day_71 데일리 정리

by 림졍 2024. 12. 30.
반응형

 

하.. 하얗게 불태웠다...

 

최종 전 마지막 프로젝트 드디어 끝-!

오늘은 조금 아쉬웠던 auth에 대해 정리해보고자 한다.

 

 

챌린지반 강의정리 - Supabase + Next.js 인증 및 데이터 관리 개요

나중에 써먹어보고 싶어서 적어보는 Auth에 대한 정리!

 

음.. 어렵군..

 

 

디렉토리 구조 및 환경 변수 설정

 

1. 환경 변수 설정

프로젝트 루트에 .env.local 파일 생성

NEXT_PUBLIC_SUPABASE_URL=<your_supabase_project_url>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>

 

 

2. 추천 디렉토리 구조

utils/
  supabase/
    client.ts   # 클라이언트용 Supabase 클라이언트
    server.ts   # 서버용 Supabase 클라이언트
    middleware.ts # 인증 상태 관리 미들웨어
app/
  login/
    page.tsx
  private/
    page.tsx
  auth/
    confirm/
      route.ts
middleware.ts

 

 

 Supabase 클라이언트 설정

 

1. 클라이언트용 Supabase 클라이언트 (utils/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!
  );
}

 

 

2. 서버용 Supabase 클라이언트 (utils/supabase/server.ts)

 

서버 컴포넌트, Route Handler에서 Supabase를 사용 시, createServerClient를 사용

cookies 객체를 통해 인증 세션을 관리하고, 요청마다 독립적인 클라이언트를 생성

import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createClient() {
  const cookieStore = cookies();
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => cookieStore.getAll(),
        setAll: (cookiesToSet) =>
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          ),
      },
    }
  );
}

 

 

cookies 객체 역할

  • 사용자가 로그인하면 인증 토큰이 생성
  • 이 토큰을 쿠키에 저장하고, 다음에 페이지를 열면 쿠키에 있는 토큰을 통해 로그인 상태를 유지
  • Supabase는 cookies 객체를 통해 쿠키 읽기/쓰기 방식을 이해하고, 인증 상태를 관리

 

- 로그인 및 회원가입 예제 (서버 컴포넌트)

"use server";

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { createClient } from '@/utils/supabase/server';

export async function login(formData: FormData) {
  const supabase = await createClient();
  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  };
  const { error } = await supabase.auth.signInWithPassword(data);
  if (error) redirect('/error');
  
  revalidatePath('/', 'layout');
  redirect('/');
}

export async function signup(formData: FormData) {
  const supabase = await createClient();
  const data = {
    email: formData.get('email') as string,
    password: formData.get('password') as string,
  };
  const { error } = await supabase.auth.signUp(data);
  if (error) redirect('/error');
  
  revalidatePath('/', 'layout');
  redirect('/');
}

 

 

- Route Handler에서 Supabase 사용 예시

import { createClient } from "@/utils/supabase/server";

export async function POST(request: Request) {
  const supabase = await createClient();
  const body = await request.json();
  const { data, error } = await supabase.from("todos").insert(body);
  
  if (error) {
    return new Response(JSON.stringify({ error: error.message }), { status: 400 });
  }
  return new Response(JSON.stringify({ data }), { status: 200 });
}

 

 

미들웨어 설정 (세션 갱신)

 

1. 미들웨어 로직 (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)
          );
        },
      },
    }
  );

  const { data: { user } } = await supabase.auth.getUser();
  if (!user && !request.nextUrl.pathname.startsWith("/login")) {
    const url = request.nextUrl.clone();
    url.pathname = "/login";
    return NextResponse.redirect(url);
  }
  if (user && request.nextUrl.pathname.startsWith("/login")) {
    return NextResponse.redirect('/');
  }
  return supabaseResponse;
}

 

 

- 미들웨어 적용 (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: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};

 

- 트리거를 사용한 데이터 동기화

CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.users (id, email)
  VALUES (NEW.id, NEW.email);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user();

 

 

TypeScript 타입 연동

supabase gen types typescript --project-id $PROJECT_REF > src/types/supabase.ts

 

import { Database } from "@/types/supabase";
type Todo = Database["public"]["Tables"]["todos"]["Row"];
const [todo, setTodo] = useState<Todo | null>(null);
728x90
반응형