본문 바로가기

TIL

TIL - 소셜 로그인 기능 추가 도중 트러블 슈팅

발단 

이번 프로젝트에서는 사용자가 Google 및 카카오 계정 등을 사용해 로그인할 수 있도록 소셜 로그인 기능을 추가하려고 했습니다. 하지만 여러 가지 문제를 겪으며 해결 과정을 통해 많은 것을 배울 수 있었습니다.

문제 

소셜 로그인 버튼을 클릭할 때 리디렉션이 되면서 supabase에 저장되지 않는 오류가 발생했습니다. 

 

원인 분석

처음에는 문제가 Google API 콘솔의 리디렉션 URI 설정에 있다고 생각했습니다. 이후, 리디렉션이 발생하는 것을 확인하고 로그인 폼 제출 방지 문제라고 생각해서 테스트해 보았지만, 문제가 지속되었습니다.

 로그를 통해 문제를 찾으려고 했지만, 소셜 로그인 시 페이지가 이동하면서 로그를 확인하기가 어려웠습니다. 이 과정에서 소셜 로그인 페이지로 이동하는 것을 보고 연결은 제대로 되었음을 확인했습니다.

 

해결 방법

  1. 첫 번째 시도: Google API 콘솔의 리디렉션 URI를 다시 설정했습니다. 그러나 여전히 문제가 해결되지 않았습니다.
  2. 두 번째 시도: 기존에는 zustand와 Tanstack query로 인증을 관리했는데, onSuccess가 비동기 처리되는 동안 리디렉션이 발생해 실행되지 않는 문제를 의심했습니다. onSuccess를 제거하고 직접 API 호출로 변경해보았지만 실패했습니다.
  3. 세 번째 시도: 리디렉션을 제거하고 다시 시도해보니 URL에서 error=server_error&error_code=500&error_description=Database+error+saving+new+user 오류 메시지를 확인했습니다. 그리고 개발자 도구의 네트워크 탭에 요청이 가지 않는 것을 통해 데이터베이스 문제임을 인지했습니다.
  4. 네 번째 시도: Supabase의 auth에 있는 users 테이블에서 정책을 확인했지만, Supabase에서 자동으로 보안 관리됨을 확인했습니다. 마지막으로, 기존에 로그인 시 자동으로 nickname을 퍼블릭 테이블에 할당해주는 트리거 함수가 문제임을 의심했습니다.
  5. 트리거 함수 수정: 트리거 함수를 삭제하고, 아래와 같은 새로운 트리거 함수를 생성했습니다

기존 트리거 함수

//기존 트리거 함수
CREATE OR REPLACE FUNCTION public.handle_new_user_custom()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.users (id, email, nickname)
  VALUES (NEW.id, NEW.email, NEW.raw_user_meta_data->>'nickname');
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 새로운 트리거 생성
CREATE TRIGGER on_auth_user_created_custom
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user_custom();

 

수정한 트리거

-- 사용자의 이름을 사용하는 트리거
CREATE OR REPLACE FUNCTION public.handle_new_user_custom()
RETURNS TRIGGER AS $$
DECLARE
    nickname_value TEXT;
    base_nickname TEXT;
    suffix INT := 1;
BEGIN
    -- 먼저 display name을 확인
    nickname_value := NEW.raw_user_meta_data->>'name';
    
    -- display name이 없으면 nickname을 확인
    IF nickname_value IS NULL THEN
        nickname_value := NEW.raw_user_meta_data->>'nickname';
    END IF;
    
    -- 둘 다 없으면 이메일의 @ 앞부분을 사용
    IF nickname_value IS NULL THEN
        nickname_value := split_part(NEW.email, '@', 1);
    END IF;

    base_nickname := nickname_value;
    
    -- nickname의 중복을 확인하고 중복이 없을 때까지 suffix를 붙여 새로운 nickname 생성
    WHILE EXISTS (SELECT 1 FROM public.users WHERE nickname = nickname_value) LOOP
        nickname_value := base_nickname || '_' || suffix;
        suffix := suffix + 1;
    END LOOP;

    INSERT INTO public.users (id, email, nickname)
    VALUES (
        NEW.id, 
        NEW.email, 
        nickname_value
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 트리거 재생성
DROP TRIGGER IF EXISTS on_auth_user_created_custom ON auth.users;
CREATE TRIGGER on_auth_user_created_custom
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user_custom();
CREATE OR REPLACE FUNCTION public.handle_new_user_custom()
RETURNS TRIGGER AS $$
DECLARE
    nickname_value TEXT;
    random_number INT;
BEGIN
    -- 무작위 4자리 숫자 생성 (1000에서 9999 사이)
    random_number := floor(random() * 9000 + 1000)::int;
    
    -- "익명_" 뒤에 무작위 숫자를 붙여 nickname 생성
    nickname_value := '익명_' || random_number::text;
    
    -- nickname의 중복을 확인하고 중복이 없을 때까지 새로운 무작위 숫자로 nickname 생성
    WHILE EXISTS (SELECT 1 FROM public.users WHERE nickname = nickname_value) LOOP
        random_number := floor(random() * 9000 + 1000)::int;
        nickname_value := '익명_' || random_number::text;
    END LOOP;

    INSERT INTO public.users (id, email, nickname)
    VALUES (
        NEW.id, 
        NEW.email, 
        nickname_value
    );
    RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- 트리거 재생성 (이 부분은 변경되지 않았습니다)
DROP TRIGGER IF EXISTS on_auth_user_created_custom ON auth.users;
CREATE TRIGGER on_auth_user_created_custom
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user_custom();



추가 시도 (Additional Attempts)

이와 함께 zustand를 이용해 로그인 상태 관리를 하기 위해서 리디렉션을 설정해서 아래와 같은 코드를 사용했습니다.

interface CustomUser {
  email: string;
  id: string;
  nickname: string;
  profile_picture: string | null;
}

const mapUserToCustomUser = (user: User): CustomUser => {
  return {
    email: user.email || "",
    id: user.id,
    nickname: user.user_metadata?.nickname || "",
    profile_picture: user.user_metadata?.avatar_url || null,
  };
};

const AuthCallback = () => {
  const setUser = useAuthStore((state) => state.setUser);
  const navigate = useNavigate();

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

      if (error) {
        console.error("Error fetching user:", error);
        navigate("/login"); // 로그인 실패 시 로그인 페이지로 리디렉션
        return;
      }

      if (user) {
        const customUser = mapUserToCustomUser(user);
        setUser(customUser);
        navigate("/");
      } else {
        navigate("/login");
      }
    };

    handleAuthChange();
  }, [setUser, navigate]);

  return <div>인증 처리 중...</div>;
};

export default AuthCallback;

 

 

{
  path: "/auth",
  element: <AuthPage />,
  children: [
    { path: "login", element: <LoginForm /> },
    { path: "signup", element: <SignUpForm /> },
    { path: "callback", element: <AuthCallback /> },
  ],
}

// 소셜 로그인
async signInWithOAuth(platform: Provider): Promise<{
  data: { provider: Provider; url: string } | null;
  error: Error | null;
}> {
  try {
    const { data, error } = await supabase.auth.signInWithOAuth({
      provider: platform,
      options: {
        redirectTo: `${window.location.origin}/auth/callback`,
        queryParams: {
          access_type: "offline",
          prompt: "consent",
        },
      },
    });
    if (error) throw error;
    return { data, error: null };
  } catch (error) {
    console.error("OAuth sign-in error:", error);
    return {
      data: null,
      error:
        error instanceof Error ? error : new Error("Unknown error occurred"),
    };
  }
}

 

 

결과

트리거 함수를 수정한 후, 소셜 로그인이 정상적으로 작동했고 퍼블릭 users 테이블에 nickname이 제대로 할당되는 것을 확인했습니다. 문제의 원인은 nickname 컬럼이 존재하지 않아 500 오류가 발생한 것이었습니다.

요약 및 느낀 점

이번 트러블 슈팅을 통해 API 콘솔 설정의 중요성과 로그를 통한 문제 분석의 한계를 경험했습니다. 특히, 개발자 도구를 활용한 네트워크 요청 확인이 문제 해결에 큰 도움이 되었습니다. 앞으로 비슷한 문제를 방지하기 위해 테이블 스키마와 트리거 함수를 더 신중히 검토할 것입니다. 이번 트러블 슈팅은 AI의 도움을 받아 생소한 SQL 트리거 문제를 해결했습니다. 디버깅 과정에서 정확한 원인을 파악하고 문제를 해결할 수 있었던 좋은 경험이었습니다.

 

 

DROP TRIGGER IF EXISTS on_auth_user_created_custom ON auth.users;