Google OAuth 로그인 플로우를 처리하기 위한 커스텀 훅 useGoogleOAuth를 구현했습니다.

Google 로그인 버튼 클릭 시 OAuth 인증 페이지로 리디렉션하고, 로그인 성공 후 서버로 access token을 전송하여 사용자 인증을 처리합니다.

서버 응답에서 accessToken은 응답 헤더(Authorization)로 받아, 이를 localStorage에 저장한 후 적절한 경로로 리디렉션 처리합니다.

useGoogleAuthMutaion

const postOAuth = async ({ code }: OAuthRequest): Promise<OAuthResponse> => {
  const res = await clientInstance.post(APIPath.postOAuth, { code });

  const authorizationHeader = res.headers['authorization'];
  const accessToken = authorizationHeader.replace('Bearer ', '');

  if (!accessToken) {
    throw new Error('Authorization header is missing in the response');
  }

  return {
    accessToken,
    type: res.data.type,
    profileImage: res.data.profileImage,
    name: res.data.name,
  };
};

export function useGoogleOAuthMutation() {
  return useMutation<OAuthResponse, AxiosError, OAuthRequest>({
    mutationFn: postOAuth,
  });
}

useGoogleAuth

const googleClientId = import.meta.env.VITE_GOOGLE_AUTH_CLIENT_ID;
const googleRedirectUri = import.meta.env.VITE_GOOGLE_AUTH_REDIRECT_URI;
const googleAuthUri = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleClientId}&redirect_uri=${googleRedirectUri}&response_type=code&scope=email%20profile`;

const getAccessTokenFromUrl = () => {
  const searchParams = new URLSearchParams(window.location.search);
  return searchParams.get('code');
};

export function useGoogleOAuth() {
  const [isLoading, setIsLoading] = useState(false);
  const navigate = useNavigate();
  const { mutate } = useGoogleOAuthMutation();

  const redirectToGoogleLogin = useCallback(() => {
    window.location.href = googleAuthUri;
  }, []);

  const handleLogin = useCallback(() => {
    const token = getAccessTokenFromUrl();

    if (token) {
      setIsLoading(true);
      mutate(
        { code: token },
        {
          onSuccess: (data: OAuthResponse) => {
            const { accessToken, type, profileImage, name } = data;
            localStorage.setItem('token', accessToken);
            localStorage.setItem('user', JSON.stringify({ profileImage, name, type }));

            if (type === 'first') {
              navigate(ROUTE_PATH.AUTH.SIGN_UP);
            } else {
              navigate(ROUTE_PATH.HOME);
            }

            window.location.reload();
          },
          onError: (error) => {
            console.error('Error during login:', error);
            alert('로그인에 실패했습니다. 다시 시도해주세요.');
            setIsLoading(false);
          },
        },
      );
    }
  }, [mutate, navigate]);

  useEffect(() => {
    handleLogin();
  }, [handleLogin]);

  return { isLoading, redirectToGoogleLogin };
}

Usage

버튼 클릭시 구글 로그인 팝업으로 이동하기 위해 redirectToGoogleLogin 함수를 호출합니다.

import { useGoogleOAuth } from '@/apis/auth/mutations/useGoogleOAuth';

export function SignInButton() {
  const { redirectToGoogleLogin } = useGoogleOAuth();

  return (
    <Button theme="outlined" onClick={redirectToGoogleLogin}>
      <Flex alignItems="center" gap={FLEX_GAP_CONFIG}>
        <Icon.Social.Google />
        <Typo size="14px" color="gray" element="span" style={BUTTON_STYLE}>
          Sign up with Google
        </Typo>
      </Flex>
    </Button>
  );
}

이후 사용자가 구글 아이디를 선택하면 LoadingPage 로 이동하게 되며 자동으로 서버에 구글 서버에서 받은 accessToken을 전송합니다.

import { Spinner, Typo } from '@/components/common';
import { useGoogleOAuth } from '@/apis/auth/mutations/useGoogleOAuth';

export default function LoadingPage() {
  const { isLoading } = useGoogleOAuth();

  return (
    <div>
      {isLoading ? (
        <Spinner />
      ) : (
        <Typo element="h1" size="18px" bold color="blue">
          로그인 처리 중 오류가 발생했습니다.
        </Typo>
      )}
    </div>
  );
}