[Next.js] supabase 회원 탈퇴 기능 구현하기

정다롱·2024년 10월 28일

내일배움캠프 TIL

목록 보기
35/39

🖥️ supabase 회원 탈퇴 구현하기 (route handler)

supabase 회원 탈퇴를 위해서는 service role key를 사용하는 클라이언트를 생성하여 API요청을 해야하는데, 이 service role key는 노출이 되면 안되는 키라 서버에서 처리를 해야한다.

Next.js 는 간단한 백엔드 로직을 구현할 수 있도록 route handler 라는 기능을 제공하고 있으니, 이 방법을 사용해서 회원탈퇴를 구현했다.


🚩 Auth Client 생성하기

사용법에 대한 공식문서

먼저, supabase auth 스키마에 있는 user 정보를 변경하거나 삭제하기 위해서는 admin이라는 api 요청을 해야한다. 이 요청은 위에 적었듯이 서비스 롤 키로 사용이 가능하고, admin 요청을 위한 클라이언트를 따로 만들어야하기 때문에 이 과정을 첫번째로 실행해야 한다.

처음엔 어떻게 설정하라는 건지, 꼭 해야하는 건지 몰라서 헤멨는데 이 블로그에서 도움을 많이 받았다.

export function createAuthClient() {
  const cookieStore = cookies();
  // Create a server's supabase client with newly configured cookie,
  // which could be used to maintain user's session
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    // env.local 에다가 key 넣어 놓기
    process.env.SUPABASE_SERVICE_ROLE_KEY!,
    {
      auth: {
        autoRefreshToken: false,
        persistSession: false,
      },
      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.
          }
        },
      },
    },
  );
}

이미 createClient 를 많이 사용하고 있어서 false true를 넣진 못했고, 그냥 AuthClient 라고 따로 분리해서 만들었다. key 부분에 서비스 롤 키를 넣어주면 사용할 수 있다.


🚩 route handler 설정

회원 탈퇴 기능 구현에서 제일 중요한 부분이다!!

app/api/delete-user/route.ts 해당 경로에 파일을 만들었다.

그리고 사용할 땐

// 회원 탈퇴 라우트 핸들러 사용
export const deleteUser = async () => {
  const res = await fetch("/api/delete-user", {
    method: "DELETE",
  });
  const data = await res.json();

  if (res.ok) {
    console.log(data.message);
  } else {
    console.error(data.error);
  }
};

저렇게 경로와 메서드를 지정해주면 된다.

DELETE 요청이니까 라우트 핸들러 안에서도 DELETE 로직을 작성해주어야 한다.

export async function DELETE() {
  // 서비스 롤 키 넣어서 만든 Auth 클라이언트
  const supabase = createAuthClient();
  // 회원 탈퇴 요청시 user ID가 필요해서 gerUser 요청
  const {
    data: { user },
    error: userError,
  } = await supabase.auth.getUser();

  // user가 없거나 에러가 발생하면 에러 메세지를 반환한다
  if (userError || !user) {
    console.error("Authentication error:", userError);
    // 이게 아까 요청한 deleteUser 의 반환값으로 날아감
    return NextResponse.json(
      { message: "User not authenticated" },
      { status: 401 },
    );
  }

이 부분에서 헷갈렸던게, 어디서 getSession을 사용하라고 해서 getSession을 넣었는데

Using the user object as returned from supabase.auth.getSession() or from some supabase.auth.onAuthStateChange() events could be insecure! This value comes directly from the storage medium (usually cookies on the server) and many not be authentic. Use supabase.auth.getUser() instead which authenticates the data by contacting the Supabase Auth server.

이런 경고 문구가 떴다. 무슨 말인가 돌려보니

supabase.auth.getSession()이나 supabase.auth.onAuthStateChange() 이벤트에서 반환되는 user 객체를 그대로 사용하는 것은 보안에 취약할 수 있습니다. 대신 supabase.auth.getUser()를 사용하면, Supabase Auth 서버에 직접 데이터를 요청해 사용자 정보를 인증하게 되어 보안이 강화됩니다.

그래서 getUser로 바꿨다... 그런데 저번에 getUser, getSession 차이 찾아봤을 때도 서버에서는 getUser, 클라이언트에서는 getSession 사용을 권장한다고 나왔는데, 나는 어디서 getSession을 사용하라는 말을 본걸까...?

아무튼 메인적으로 요청이 되는 부분은 저 로직이고, 그 후에 에러나 완료 메세지를 위해 추가적으로 코드를 덧붙였다.

  try {
    console.log("Attempting to delete user:", user.id);
    const { error } = await supabase.auth.admin.deleteUser(user.id);

    if (error) {
      console.error("Error deleting user:", error);
      return NextResponse.json({ message: error.message }, { status: 500 });
    }

    return NextResponse.json(
      { message: "User deleted successfully" },
      { status: 200 },
    );
  } catch (err) {
    console.error("Unexpected error:", err);
    return NextResponse.json(
      { message: "An unexpected error occurred" },
      { status: 500 },
    );
  }
}

실제로 삭제가 요청되는 부분은 try catch로 작성했고, 오류 상황에 따라 다른 메세지를 출력하도록 했다. 오류 없이 작업이 끝나면 200 응답이 날아간다.


💥 DELETE http://localhost:3000/api/delete-user 500 (Internal Server Error)

로직도 잘 작성했고, 콘솔도 찍히는데 회원 탈퇴가 안되고 해당 오류가 반환됐다. 사실 이 오류를 보고나서 어디서 문제가 생기는 건지 확인하기 위해 위에서 에러 응답을 추가한건데

Error deleting user: AuthApiError: Database error deleting user

다시 시도했을 때 에러메세지가 이렇게 출력이 됐다. Error deleting user 부분 메세지인걸 보니 삭제 요청은 제대로 전달됐지만 삭제하는 과정에서 오류가 생긴 것으로 보인다.

여기저기 검색해보니, 외래키에 대한 얘기와 CASCADE 설정을 확인하라는 말이 보여서 supabase 내에서 auth 스키마 user와 연결된 테이블을 확인했다.

SELECT
    tc.table_schema, 
    tc.table_name, 
    kcu.column_name, 
    ccu.table_schema AS foreign_table_schema,
    ccu.table_name AS foreign_table_name, 
    ccu.column_name AS foreign_column_name 
FROM 
    information_schema.table_constraints AS tc 
JOIN 
    information_schema.key_column_usage AS kcu
ON 
    tc.constraint_name = kcu.constraint_name
JOIN 
    information_schema.constraint_column_usage AS ccu
ON 
    ccu.constraint_name = tc.constraint_name
WHERE 
    constraint_type = 'FOREIGN KEY' 
    AND ccu.table_name = 'users'
    AND ccu.table_schema = 'auth';

sql 에디터에 해당 코드를 실행시키면 어떤 테이블이 외래키로 연결되어 있는지 나오는데 해당 테이블에 가서 설정을 확인해보니 정말 CASCADE 적용이 안되어 있었다... 설정 하니까 바로 오류없이 회원 삭제가 진행되었다ㅎ;

지금껏 프로젝트에서 auth와 관련된 기능은 한 번도 해본 적이 없는데, 이번 기회에 회원 탈퇴를 구현해보면서 이것저것 알아가는 게 많아서 너무 좋았다!!!

0개의 댓글