본문 바로가기
✨FRONTEND/📍Next.js

구글 간편 로그인 구현하기

by 짱돌보리 2024. 9. 7.
728x90

🎉구글 간편 로그인 구현하기

이번 프로젝트에서 Google 소셜 로그인(OAuth 2.0)을 구현했다.

 

OAuth 인증 흐름을 기반으로 회원가입 / 로그인 기능을 처리했다.

구글 로그인은 기본적으로 인가 코드 → 토큰 발급 → 사용자 인증 → 서비스 로그인 순서로 진행된다.

✅ 구글 로그인 절차

1️⃣ 인가 코드 발급

사용자가 구글 로그인 버튼 클릭 → 구글 서버로 인증 요청 → 사용자 인증 및 동의 후 인가 코드(code)를 애플리케이션으로 리다이렉트

 

2️⃣ 토큰 발급
애플리케이션이 인가 코드로 구글 서버에 요청 → Access Token 발급

 

3️⃣ 사용자 정보 확인
발급받은 토큰으로 사용자 정보를 조회 → 회원 여부 확인

 

4️⃣ 서비스 로그인 처리
회원 확인 후 로그인 처리 → 서비스 화면으로 이동

위를 바탕으로 구글 소셜 로그인을 구현해보자.

✅ 구현 순서

1. 앱 등록하기

아래 링크로 들어가 Oauth 클라이언트 ID를 생성해준다.
https://console.cloud.google.com/welcome

2. 클라이언트 ID 저장하기

발급받은 Client ID는 .env 파일에 저장한다.

NEXT_PUBLIC_GOOGLE_CLIENT_ID=구글_CLIENT_ID
NEXT_PUBLIC_GOOGLE_SECRET=구글_SECRET_KEY

3. redirectUri 설정하기

OAuth 인증에서는 redirectUri 설정이 매우 중요하다.

로그인 인증이 끝나면 인가 코드가 redirectUri로 전달된다.

redirectUri 설정해야 하는 이유

OAuth 로그인에서는 로그인 완료 후 사용자를 다시 돌려보낼 URL이 필요하다.

 

예시) http://localhost:3000/oauth/callback

 

구글 로그인 인증이 완료되면 인가 코드가 포함된 상태로 이 URL로 리다이렉트된다.

4. 구글 시크릿 키 저장하기

Google OAuth에서는 Client Secret Key도 필요하다.

🤔구글 시크릿 키 설정해야 하는 이유

  • 시크릿 키는 액세스 토큰을 안전하게 발급받기 위해 필요한 중요한 요소다.
    시크릿 키 없이는 구글의 토큰 엔드포인트에서 액세스 토큰을 발급받을 수 없다.
  • 클라이언트 애플리케이션의 신원을 확인하는 데 사용된다.
    구글은 이 시크릿 키를 통해 요청이 실제로 등록된 애플리케이션에서 온 것인지 확인한다.

5. env 설정

NEXT_PUBLIC_GOOGLE_CLIENT_ID=위에서 설정했던 CLIENT KEY
NEXT_PUBLIC_GOOGLE_SECRET=구글 시크릿 키
NEXT_PUBLIC_GOOGLE_SIGNUP_REDIRECT_URI=http://localhost:3000/signup/oauth/google
NEXT_PUBLIC_GOOGLE_LOGIN_REDIRECT_URI=http://localhost:3000/login/oauth/google

6. URL 생성하기

위에서 설정했던 env를 통해 아래의 URL 을 설정해준다.

 

로그인 URL에는 prompt=consent 옵션을 추가했다.

이 옵션을 넣으면 로그인할 때마다 구글 로그인 창이 표시된다.

export const GOOGLE_SIGNUP_URL = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&scope=openid%20email&client_id=${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_GOOGLE_SIGNUP_REDIRECT_URI}`;
export const GOOGLE_LOGIN_URL = `https://accounts.google.com/o/oauth2/v2/auth?response_type=code&scope=openid%20email&client_id=${process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_GOOGLE_LOGIN_REDIRECT_URI}&prompt=consent`;

7. 간편 회원가입 구현

OAuth 인증이 완료되면 redirectUri로 인가 코드(code)가 전달된다.

 

이 코드를 이용해 Access Token을 발급받는다.

const response = await axios.post(
  'https://oauth2.googleapis.com/token',
  new URLSearchParams({
    code,
    client_id: GOOGLE_CLIENT_ID,
    client_secret: GOOGLE_SECRET,
    redirect_uri: GOOGLE_SIGNUP_REDIRECT_URI,
    grant_type: 'authorization_code',
  }),
  {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
  }
)

 

토큰 응답에서 id_token을 가져온다.

const googleAccessToken = tokenResponse.id_token

 

이 토큰을 서버 API로 전달해 회원가입을 진행한다.

const signUpData = {
  nickname: randomNickname(),
  redirectUri: GOOGLE_SIGNUP_REDIRECT_URI,
  token: googleAccessToken,
}

 

풀코드))

import Loading from '@/components/commons/Loading';
import { notify } from '@/components/commons/Toast';
import { SignUpUser } from '@/libs/api/oauth';
import {
  GOOGLE_CLIENT_ID,
  GOOGLE_SECRET,
  GOOGLE_SIGNUP_REDIRECT_URI,
} from '@/libs/constants/auth';
import { randomNickname } from '@/libs/utils/randomNickname';
import { SignInResponse, SignUpRequest } from '@trip.zip-api';
import axios from 'axios';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';

export default function Google() {
  const router = useRouter();
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const handleOAuthCallback = async () => {
      const { code } = router.query;

      if (code) {
        try {
          const getAccessToken = async (code: string) => {
            const response = await axios.post(
              'https://oauth2.googleapis.com/token',
              new URLSearchParams({
                code,
                client_id: GOOGLE_CLIENT_ID || '',
                client_secret: GOOGLE_SECRET || '',
                redirect_uri: GOOGLE_SIGNUP_REDIRECT_URI || '',
                grant_type: 'authorization_code',
              }),
              {
                headers: {
                  'Content-Type': 'application/x-www-form-urlencoded',
                },
              },
            );

            return response.data;
          };

          const tokenResponse = await getAccessToken(code as string);
          const googleAccessToken = tokenResponse.id_token;

          console.log(tokenResponse);
          console.log(googleAccessToken);

          const signUpData: SignUpRequest = {
            nickname: randomNickname(),
            redirectUri: GOOGLE_SIGNUP_REDIRECT_URI,
            token: googleAccessToken,
          };

          const signInResponse: SignInResponse = await SignUpUser(
            'google',
            signUpData,
          );

          console.log('회원가입 성공:', signInResponse);
          notify('success', '회원가입 성공!');

          router.push('/activities');
        } catch (error) {
          console.error('회원가입 오류:', error);
          if (axios.isAxiosError(error) && error.response) {
            notify('error', error.response.data.message);
            if (error.response.data.message === '이미 등록된 사용자입니다.')
              router.push('/login');
          } else {
            notify('error', '회원가입 중 알 수 없는 오류가 발생했습니다.');
          }
        } finally {
          setLoading(false);
        }
      }
    };

    handleOAuthCallback();
  }, [router.query]);

  return <div>{loading && <Loading />}</div>;
}

8. 간편 로그인 구현

로그인도 동일하게 인가 코드 → 토큰 발급 → 로그인 API 호출 순서로 진행된다.

const signInData = {
  redirectUri: GOOGLE_LOGIN_REDIRECT_URI,
  token: googleAccessToken,
}

 

로그인 성공 시 서버에서 받은 토큰을 쿠키에 저장한다.

setCookie('accessToken', signInResponse.accessToken)
setCookie('refreshToken', signInResponse.refreshToken)

 

그리고 로그인 상태를 표시하기 위해 소셜 로그인 여부 쿠키도 저장했다.

setCookie('isSocialUser', true)

 

풀코드))

import Loading from '@/components/commons/Loading';
import { notify } from '@/components/commons/Toast';
import { signInUser } from '@/libs/api/oauth';
import {
  GOOGLE_CLIENT_ID,
  GOOGLE_LOGIN_REDIRECT_URI,
  GOOGLE_SECRET,
} from '@/libs/constants/auth';
import { SignInRequest, SignInResponse } from '@trip.zip-api';
import axios from 'axios';
import { setCookie } from 'cookies-next';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';

export default function Google() {
  const router = useRouter();
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const handleOAuthCallback = async () => {
      const { code } = router.query;

      if (code) {
        try {
          const getAccessToken = async (code: string) => {
            const response = await axios.post(
              'https://oauth2.googleapis.com/token',
              new URLSearchParams({
                code,
                client_id: GOOGLE_CLIENT_ID || '',
                client_secret: GOOGLE_SECRET || '',
                redirect_uri: GOOGLE_LOGIN_REDIRECT_URI || '',
                grant_type: 'authorization_code',
              }),
              {
                headers: {
                  'Content-Type': 'application/x-www-form-urlencoded',
                },
              },
            );

            return response.data;
          };

          const tokenResponse = await getAccessToken(code as string);
          const googleAccessToken = tokenResponse.id_token;

          const signInData: SignInRequest = {
            redirectUri: GOOGLE_LOGIN_REDIRECT_URI,
            token: googleAccessToken,
          };

          const signInResponse: SignInResponse = await signInUser(
            'google',
            signInData,
          );
          console.log('로그인 성공:', signInResponse);
          notify('success', '로그인 성공!');

          setCookie('accessToken', signInResponse.accessToken, {
            path: '/',
            secure: true,
            sameSite: 'strict',
          });
          setCookie('refreshToken', signInResponse.refreshToken, {
            path: '/',
            secure: true,
            sameSite: 'strict',
          });
          setCookie('isSocialUser', true, {
            path: '/',
            secure: true,
            sameSite: 'strict',
          });

          router.push('/activities');
        } catch (error) {
          if (
            axios.isAxiosError(error) &&
            error.response &&
            error.response.status === 401
          ) {
            notify('warning', '계정이 없는 경우 회원가입을 진행해 주세요.');
            router.push('/signup');
          }
          console.error('로그인 오류:', error);
          notify('error', '로그인 중 알 수 없는 오류가 발생했습니다.');
        } finally {
          setLoading(false);
        }
      }
    };

    handleOAuthCallback();
  }, [router.query]);

  return <div>{loading && <Loading />}</div>;
}

구현 완!

참고

https://developers.google.com/identity/gsi/web/guides/overview?hl=ko&authuser=1

https://developers.google.com/identity/protocols/oauth2?authuser=1&hl=ko