[React+Typescript] 구글 로그인

토깽이·2023년 3월 31일
2

구글 로그인 라이브러리인 Google Sign-in platform library가 2023년 3월 이후로 deprecated 되고 Google Identity Services library로 변경됩니다.

react-google-login 라이브러리를 사용중이었는데, 이를 삭제하고 새로운 라이브러리로 마이그레이션 하였습니다.

GCP에서 Client ID를 발급 받는 부분은 생략하였습니다. (구글링하면 잘 설명해놓은 글이 많습니다!)

Google Login

  1. d.ts 파일에 window.google 타입을 선언 (For Typescript)
  2. 유저 데이터가 JWT 형태로 리턴되도록 변경되었기 때문에 decode가 필요함

index.html

<!DOCTYPE html>
<html translate="no"  class="notranslate">
  <head>
  ...
   <!-- Google Sign-in -->
    <script src="https://accounts.google.com/gsi/client" async defer></script>
  </head>
</html>

GoogleLogin.tsx

import React, { useCallback, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import jwt_decode from 'jwt-decode';

interface Props {
  handleGoogleLoginSuccess: (data: any) => void;
}

const GoogleLogin = ({ handleGoogleLoginSuccess }: Props) => {
  const googleSignInButton = useRef<HTMLDivElement>(null);
  const [scriptLoaded, setScriptLoaded] = useState(false);

  const handleGoogleSignIn = useCallback(
    (res: CredentialResponse) => {
      if (!res.clientId || !res.credential) return;
      const userObject = jwt_decode(res.credential);
      const { email, name } = userObject as ICredential;
      
      LoginIn(email, name); // your sign-in
    
      setScriptLoaded(false);
    },
    [handleGoogleLoginSuccess],
  );

  function onClickGooglelogin() {
    let element: HTMLElement = document.querySelector(
      '[aria-labelledby="button-label"]',
    ) as HTMLElement;
    element.click();
  }

  useEffect(() => {
    if (
      typeof window === 'undefined' ||
      !window.google ||
      !googleSignInButton.current
    ) {
      return;
    }

    const { google } = window;

    const initializeGoogle = () => {
      if (!google || scriptLoaded || googleClientId === undefined) return;

      setScriptLoaded(true);
      google.accounts.id.initialize({
        client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID,
        callback: handleGoogleSignIn,
      });
      const parent = document.getElementById('google-login-api');
      if (parent) {
        google.accounts.id.renderButton(parent, {
          type: 'icon',
        });
      }
    };

    const script = document.createElement('script');
    script.src = 'https://accounts.google.com/gsi/client';
    script.onload = initializeGoogle;
    script.async = true;
    script.id = 'google-client-script';
    document.querySelector('body')?.appendChild(script);

    return () => {
      window.google?.accounts.id.cancel();
      document.getElementById('google-client-script')?.remove();
    };
  }, [handleGoogleSignIn, scriptLoaded]);

  return (
    <>
      <GoogleButton id="google-login-api" ref={googleSignInButton} />
      <GoogleButtonImage
        src={'asset/images/logo_google.png'}
        alt="google login"
        onClick={onClickGooglelogin}
      />
    </>
  );
};

const GoogleButton = styled.div`
  display: none;
`;

const GoogleButtonImage = styled.img`
  height: 48px;
  width: 48px;
  border-radius: 50%;
  cursor: pointer;
`;

export default GoogleLogin;

google.d.ts

구글에서 타입을 제공하진 않지만 만들어주신 분들이 있었습니다. reference를 참고하여 필요한 것을 추가하였습니다.

interface IdConfiguration {
  client_id: string;
  auto_select?: boolean;
  callback: (handleCredentialResponse: CredentialResponse) => void;
  login_uri?: string;
  native_callback?: (...args: any[]) => void;
  cancel_on_tap_outside?: boolean;
  prompt_parent_id?: string;
  nonce?: string;
  context?: string;
  state_cookie_domain?: string;
  ux_mode?: 'popup' | 'redirect';
  allowed_parent_origin?: string | string[];
  intermediate_iframe_close_callback?: (...args: any[]) => void;
}

interface ICredential {
  iss: string; // The JWT's issuer
  nbf: number;
  aud: string; // Your server's client ID
  sub: string; // The unique ID of the user's Google Account
  hd: string; // If present, the host domain of the user's GSuite email address
  email: string; // The user's email address
  email_verified: boolean; // true, if Google has verified the email address
  azp: string;
  name: string; // If present, a URL to user's profile picture
  picture: string;
  given_name: string;
  family_name: string;
  iat: number; // Unix timestamp of the assertion's creation time
  exp: number; // Unix timestamp of the assertion's expiration time
  jti: string;
}

interface CredentialResponse {
  credential?: string;
  select_by?:
    | 'auto'
    | 'user'
    | 'user_1tap'
    | 'user_2tap'
    | 'btn'
    | 'btn_confirm'
    | 'brn_add_session'
    | 'btn_confirm_add_session';
  clientId?: string;
}

interface GsiButtonConfiguration {
  type: 'standard' | 'icon';
  theme?: 'outline' | 'filled_blue' | 'filled_black';
  size?: 'large' | 'medium' | 'small';
  text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signup_with';
  shape?: 'rectangular' | 'pill' | 'circle' | 'square';
  logo_alignment?: 'left' | 'center';
  width?: string;
  local?: string;
}

interface PromptMomentNotification {
  isDisplayMoment: () => boolean;
  isDisplayed: () => boolean;
  isNotDisplayed: () => boolean;
  getNotDisplayedReason: () =>
    | 'browser_not_supported'
    | 'invalid_client'
    | 'missing_client_id'
    | 'opt_out_or_no_session'
    | 'secure_http_required'
    | 'suppressed_by_user'
    | 'unregistered_origin'
    | 'unknown_reason';
  isSkippedMoment: () => boolean;
  getSkippedReason: () =>
    | 'auto_cancel'
    | 'user_cancel'
    | 'tap_outside'
    | 'issuing_failed';
  isDismissedMoment: () => boolean;
  getDismissedReason: () =>
    | 'credential_returned'
    | 'cancel_called'
    | 'flow_restarted';
  getMomentType: () => 'display' | 'skipped' | 'dismissed';
}

interface RevocationResponse {
  successful: boolean;
  error: string;
}

interface Credential {
  id: string;
  password: string;
}

interface Google {
  accounts: {
    id: {
      initialize: (input: IdConfiguration) => void;
      prompt: (
        momentListener?: (res: PromptMomentNotification) => void,
      ) => void;
      renderButton: (
        parent: HTMLElement,
        options: GsiButtonConfiguration,
      ) => void;
      disableAutoSelect: () => void;
      storeCredential: (credentials: Credential, callback: () => void) => void;
      cancel: () => void;
      onGoogleLibraryLoad: () => void;
      revoke: (
        hint: string,
        callback: (done: RevocationResponse) => void,
      ) => void;
    };
  };
}

interface Window {
  google?: Google;
}

Reference

https://www.dolthub.com/blog/2022-05-04-google-signin-migration/

https://devjeong.com/react/react-1/

https://www.youtube.com/watch?v=V2ypG7gCcGA

https://dev.to/oloriasabi/google-oauth2-using-the-new-google-identity-services-sdk-for-react-using-jwt-decode-2ioo

틀린 부분이 있다면 알려주시면 감사합니다!

0개의 댓글