Supabase OAuth 인증 트러블슈팅

장현빈·2025년 4월 14일

구현 목표

Supabase를 활용해 Google 또는 Kakao 소셜 로그인을 구현.

  • 사용자가 로그인 후 redirect 되면,
  • DB (users 테이블)에 사용자 정보가 없을 경우 자동 생성,
  • 이후 닉네임 존재 여부에 따라 (닉네임 등록 or 랜딩)페이지로 redirect

🐛 문제 1: 서버 컴포넌트에서 getUser() 사용

시도

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

결과

  • 로그인 직후 서버 콜백에서 user = null
  • redirect(/auth/auth-code-error)
  • 디버깅 콘솔들도 전부 안찍힘
  • 콜백 페이지에 접근 하긴 함

원인

  • OAuth 로그인 직후에는 브라우저에만 세션이 존재하고,
  • 서버에는 쿠키가 전달되지 않음
  • 콜백 페이지에 접근 하긴 함 (= getUser()자체가 안먹힘)

처음에 우연히 작동함: 같은 브라우저 세션에서 이전에 로그인된 세션이 살아있어 getUser()가 작동해서 성공한 줄 알았으나. 로그아웃 이후 다시 테스트 해보니 실패


🐛 문제 2: getSession()으로 대체 시도

시도

const {
  data: { session },
} = await supabase.auth.getSession();

결과

  • session = null, session.user = undefined

원인

  • 역시 서버에선 쿠키 기반으로 세션을 읽기 때문에 OAuth 직후에는 실패

    옳은 방법이였으나 서버컴포넌트라는 것을 망각: 여기서부터 삽질,,
    -쿠키는 브라우저가 세션 저장을 끝내기 전까지는 생성되지 않기 때문.
    -Supabase는 로그인 직후 세션을 브라우저에서 저장해야 하며, 서버는 이 쿠키를 받지 못함


🐛 문제 3: exchangeCodeForSession() 직접 사용

시도

const code = new URLSearchParams(window.location.search).get("code");
await supabase.auth.exchangeCodeForSession(code);

결과

400 Bad Request: both auth code and code_verifier should be non-empty

원인

  • Supabase는 PKCE 방식으로 OAuth 처리함
  • code_verifier는 로그인 시 자동 저장되므로, 직접 호출하면 실패함

🐛 문제 4: getSessionFromUrl({ storeSession: true }) 시도

시도

await supabase.auth.getSessionFromUrl({ storeSession: true });

결과

  • 타입 오류 (getSessionFromUrl 없음), 또는 undefined

원인

  • @supabase/ssr의 createBrowserClient() 사용 중이었음
  • supabase에서 타입 자체를 받아올 수 없었음(추후 원인 공부)

추가시도

  • @supabase/supabase-js의 createClient()로 변경
  • 타입 재시작 해봤으나 결과는 같았음

🐛 문제 5:useEffect에 직접 async 사용

useEffect(async () => {
  const { data: { session }, error } = await supabase.auth.getSession();
  ...
}, []);

원인

  • React는 useEffect()에 직접 async 함수를 허용하지 않음
  • ESLint 또는 런타임에서 경고/예외 발생
  • 로그인 흐름이 비동기적으로 처리되지 않음 (아무 동작 안 함)

이때도 코드 자체는 맞았지만, “비동기(async) 처리 위치”가 틀려서 또 삽질

✅ 최종 성공 코드 (getSession 기반, 클라이언트 컴포넌트 + useEffect 내부 async)

useEffect(() => {
  const handleCallback = async () => {
    const { data: { session }, error } = await supabase.auth.getSession();

    if (error || !session?.user) {
      router.replace("/auth/auth-code-error");
      return;
    }

    const user = session.user;

    const { data: existingUser, error: fetchError } = await supabase
      .from("users")
      .select("nickname")
      .eq("id", user.id)
      .maybeSingle();

    if (fetchError) {
      router.replace("/auth/auth-code-error");
      return;
    }

    if (!existingUser) {
      const { error: insertError } = await supabase.from("users").insert({
        id: user.id,
        nickname: "",
      });

      if (insertError) {
        router.replace("/auth/auth-code-error");
        return;
      }

      router.replace("/auth/nickname");
      return;
    }

    router.replace(existingUser.nickname ? "/" : "/auth/nickname");
  };

  handleCallback(); 
}, [router, supabase]);

🔍 성공 원인

  • use client!!!!
  • 로그인 완료 후 Supabase가 내부적으로 세션 쿠키를 저장함
  • 이후 콜백 페이지 진입 시 getSession()에서 세션이 정상적으로 읽힘
  • 클라이언트 컴포넌트에서 실행되기 때문에 쿠키가 포함됨
  • useEffect 내부 async ( => useMutation()으로 개선 예정)

최종 결과

  • Google, Kakao 모두 OAuth 로그인 후 세션 저장 완료
  • users 테이블에 유저 존재 여부 판단 후 없다면 (user_id)자동 삽입
  • 닉네임 존재 시 랜딩페이지(/), 없으면 닉네임 등록(/auth/nickname)으로 리디렉션
  • 전 과정 클라이언트 컴포넌트 하나로 통합 처리 성공

정리

getUser() : 서버에서 세션 쿠키 없으면 실패할 수 있음 (불안정)
getSession() : 클라이언트에선 쿠키 기반으로 안정적으로 작동
getSessionFromUrl() : 로그인 직후 세션 저장용이지만 현재 구조에서는 불필요
Supabase 클라이언트 : @supabase/supabase-jscreateClient() 사용해야 함
클라이언트 처리 : useEffect 안에서 비동기 함수 실행해야 함 (async component X)


이 고비를 넘기며 단순 로그인 구현이 아니라 Supabase의 인증 구조, 서버/클라이언트 세션 처리 방식, OAuth 플로우까지 깊이 있게 체득할 수 있는 과정이었습니다.

profile
안녕하세요

0개의 댓글