본문 바로가기
React 관련/프로젝트

[React] 구글 로그인 기능 구현(with firebase 소셜 로그인 인증)

by ash9river 2024. 8. 7.

React Firebase Google Authentification

리액트로 구현할 때, 파이어베이스를 이용하면 구글과 같은 소셜 로그인 기능을 쉽게 구현할 수 있다고 해서 적용해보았다.

 

구현은 쉬우나, 정보가 별로 없어서 글로 작성해보았다. 이번 글은 구글 로그인을 다루고, 다음 글에서 카카오톡 로그인을 다루겠다.

 

1. 파이어베이스 프로젝트 생성

가장 처음에 할 일은 파이어베이스 콘솔로 가서 파이어베이스 프로젝트를 생성한다.

 

다음과 같은 이미지에서 프로젝트 만들기를 클릭한다.

 

 

프로젝트 이름을 입력하고, 구글 애널리틱스 설정을 사용할거냐 묻는데, 나는 연습 프로젝트이기에 사용을 안한다고 체크했다.

 

프로젝트 만들기를 클릭했으면 다음과 같이 홈이 나온다.

 

여기서 빌드>Authentification을 들어간다.

 

시작하기 버튼을 누르면 다음과 같이 여러 가지 제공업체가 나오는데, 추가 제공업체 목록에 있는 업체들은 기본으로 사용 가능하나, 카카오톡 소셜 로그인은 OpenID connect를 써야해서 업그레이드를 해야한다.

 

 

그러나 우리는 구글 로그인만 다룰 것이라서, 먼저 추가 제공업체에서 Google을 선택한다.

 

 

구글을 선택하면 다음과 같이 나오는데, 사용설정을 on한다.

 

 

사용설정을 on하면 다음과 같이 나오는데, 프로젝트 지원 이메일은 가볍게 파이어베이스를 가입한 이메일을 입력하고 저장을 누르면 된다.

 

 

저장을 하면 다음과 같이 구글 인증이 추가가 된다.

 

이제 리액트에 파이어베이스를 설정해보자.

 

2. 리액트에 파이어베이스 설정

 

 

파이어베이스 상단의 프로젝트 개요 옆의 설정(톱니바퀴 모양)을 클릭해서 프로젝트 설정으로 들어갈 수 있다.

 

우리는 리액트로 할 예정이라, </> 버튼을 눌러서 설정을 시작하자.

 

앱 닉네임을 등록하고, 호스팅은 설정하지 않아도 된다.

 

 

다음과 같이 파이어베이스 설정을 추가하라 하는데, 지금 복붙하지 않아도, 나중에 프로젝트 설정와서 다시 볼 수 있다.

 

3. 리액트 파이어베이스 설정

 

먼저 리액트 프로젝트를 간단하게 빌드하자.

 

vite 혹은 cra로 만들 수 있는데, 나는 그냥 cra로 만들어서 포트번호가 3000이다. vite로 빌드한 사람은 기본 포트번호가 5173인걸 염두해두자.

 

 

그리고 firebase를 npm 또는 yarn으로 설치한다. 난 yarn으로 프로젝트를 진행했다.

 

npm install firebase
yarn add firebase

 

 

일단 폴더와 파일을 내 취향껏분할하였다.

 

 

폴더구조는 다음과 같고, FirebaseConfig 파일은 이하의 코드이다.

 

const fbConfig = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_APP_ID,
};

export default fbConfig;

 

파이어 베이스의 키값은 유출되면 안되므로, .env 파일을 활용해서 관리한다.

 

fbInstance의 코드는 다음과 같다.

 

// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import fbConfig from "../config/FirebaseConfig";
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries

// Your web app's Firebase configuration
const firebaseConfig = fbConfig;

// Initialize Firebase
const app = initializeApp(firebaseConfig);
export const authService = getAuth();

 

기존의 config만 다른 파일에서 export한 것이다. 파일분할의 이유는 가독성말고는 딱히 없다.

 

 

4. 로그인을 위한 전역 변수 설정

 

로그인을 위한 전역 변수 라이브러리로 zustand를 선택하였다. 

다른 전역 변수 라이브러리인 recoil이나 redux는 사용해본 경험이 있으나, zustand는 사용해본 적이 없으므로 선택해보았다.

 

import { create } from "zustand";
import fbConfig from "../config/FirebaseConfig";

interface ZustandAuthStoreInterface {
  isLoggedIn: boolean;
  username: string | null;
  id_token: string | null;
  accessToken: string | null;
  photoURL: string | null;
  setIsLoggedIn: (isLoggedIn: boolean) => void;
  setUsername: (username: string | null) => void;
  setIdToken: (id_token: string | null) => void;
  setAccessToken: (accessToken: string | null) => void;
  setPhotoURL: (photoURL: string | null) => void;
}

const sessionData = window.sessionStorage.getItem(
  `firebase:authUser:${fbConfig.apiKey}:[DEFAULT]`
);
let displayName: string | null = null;
let userImage: string | null = null;
let loggedinState = false;
if (sessionData) {
  const userData = JSON.parse(sessionData);
  loggedinState = true;
  displayName = userData.displayName;
  userImage = userData.photoURL;
}

const useZustandAuthStore = create<ZustandAuthStoreInterface>((set) => ({
  isLoggedIn: loggedinState,
  username: displayName,
  id_token: "",
  accessToken: "",
  photoURL: userImage,
  setIsLoggedIn: (isLoggedIn) => set(() => ({ isLoggedIn })),
  setUsername: (username) => set(() => ({ username })),
  setIdToken: (id_token) => set(() => ({ id_token })),
  setAccessToken: (accessToken) => set(() => ({ accessToken })),
  setPhotoURL: (photoURL) => set(() => ({ photoURL })),
}));

export default useZustandAuthStore;

 

세션 로그인을 사용할 것이기 때문에, 먼저 window.sessionStorage.getItem()을 통해서 세션 로그인 상태인지 확인한다.

 

만약 세션 로그인이 된 상태이면, 개발자 도구의 세션 저장소에서 확인할 수 있는데, 보통 firebase:authUser:파이어베이스앱키:[DEFAULT]의 형태를 가진다.

 

 

사실 아이디 토큰과 액세스 토큰이랑 유저 정보는 분리하는 편이 좋지만 이 글에서 설명을 위해 한 파일에 몰아넣었다.

 

이제 리액트의 컴포넌트를 확인해보면서, 알아가보자. 

 

5. 로그인 상태에 따른 홈 화면 분기 컴포넌트

 

import React, { useEffect, useLayoutEffect, useState } from "react";
import logo from "./logo.svg";
import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
import "./App.css";
import AuthForm from "./components/AuthForm";
import { authService } from "./firebase/fbInstance";
import Logout from "./components/Logout";
import useZustandAuthStore from "./store/ZustandAuthStore";
import KakaoLogin from "./components/KaKaoLogin";

function LoginHome() {
  const isLoggedIn = useZustandAuthStore((state) => state.isLoggedIn);
  const username = useZustandAuthStore((state) => state.username);
  const photoURL = useZustandAuthStore((state) => state.photoURL);
  const setPhotoURL = useZustandAuthStore((state) => state.setPhotoURL);
  const setIsLoggedIn = useZustandAuthStore((state) => state.setIsLoggedIn);
  const setUsername = useZustandAuthStore((state) => state.setUsername);
  authService.onAuthStateChanged((user) => {
    if (user) {
      if (!isLoggedIn) setIsLoggedIn(true);
      if (!user.displayName) setUsername(user.displayName || "");
      if (!photoURL) setPhotoURL(user.photoURL);
    } else {
      setIsLoggedIn(false);
      setUsername(null);
      setPhotoURL(null);
    }
  });

  return (
    <>
      {!isLoggedIn && <AuthForm />}
      {isLoggedIn && <p>welcome! {username}</p>}
      {isLoggedIn && photoURL && <img src={photoURL} alt="alt" />}
      {isLoggedIn && <Logout />}

      {!isLoggedIn && <KakaoLogin />}
    </>
  );
}

export default LoginHome;

 

먼저 최상단 컴포넌트인 LoginHome 컴포넌트이다.

 

authService.onAuthStateChange가 콜백함수를 통해 실행되면 로그인 변경 상태가 감지된다.

 

사실 검색을 하다보면 useEffect나 useLayoutEffect의 콜백함수 안에 .onAuthStateChange 함수가 있는 경우가 많은데, onAuthStateChange 자체도 비동기 콜백함수이기 때문에, useEffect 또는 useLayoutEffect에 있을 필요가 없다.

 

로그인 상태 때문에 더럽게 되었지만, 사실 전역변수로 따로 user 객체를 만들면 이렇게 하나하나씩 쓸 필요가 없다. 그렇지만 데이터를 닉네임과 사진만 쓸 것이기 때문에 현재 로그인 상태를 나타내는 변수와 닉네임, 그리고 이미지만 따로 설정했다.

 

카카오 로그인은 다음 게시글에서 확인하고, 구글 로그인 컴포넌트를 보기 직전에 AuthForm 컴포넌트를 보자. 

 

import { useEffect, useRef } from "react";
import { createUserWithEmailAndPassword } from "firebase/auth";
import { authService } from "../firebase/fbInstance";
import useZustandAuthStore from "../store/ZustandAuthStore";
import GoogleLogin from "./GoogleLogin";
import EnrollmentForm from "./EnrollmentForm";

function AuthForm() {
  const username = useZustandAuthStore((state) => state.username);
  useEffect(() => {
    console.log(username);
  }, [username]);

  return (
    <div>
      <EnrollmentForm />
      <div>
        <GoogleLogin />
        <button name="Github">Github 로그인</button>
      </div>
    </div>
  );
}

export default AuthForm;

 

안지운 기타 잡 import 있긴한데 무시하고, 그냥 간단한 로그인 컴포넌트다. Github 로그인은 딱히 구현하지 않았다.

 

구글 로그인만 볼 것이므로 구글 로그인 컴포넌트를 확인해보자.

 

 

6. 구글 로그인 컴포넌트

 

firebase의 로그인을 유지할려면 3 가지 옵션이 있는데, 공식 문서에서는 다음과 같이 설명한다.

 

 

나는 로그인 옵션을 세션으로 설정할 것이다.

 

firebase.auth().setPersistance 함수를 통해서 옵션을 설정할 수 있는데, firebase/auth에서 browserSessionPersistence를 import하여 사용함으로써 쉽게 옵션을 세션으로 설정할 수 있다.

 

소셜 로그인에는 2 가지 방법이 있다.

 

1. redirect를 이용한 로그인

2. popup을 이용한 로그인

 

1번인 리다이렉션을 이용해서 구글 로그인을 할 수도 있지만, 2번인 팝업을 통해여 구글 로그인을 구현하겠다.

 

다행스럽게도, 파이어베이스에서 구글 로그인 팝업 기능은 signInWithPopup을 통해 구현가능하다.

 

signInWithPopup을 실행하고, 그 콜백함수를 통해서 user 데이터를 활용할 수 있다.

 

 

그러나, 파이어베이스를 이용한, 팝업 방식의 구글 로그인은 Cross-Origin-Opener-Policy라는 COOP 오류가 발생하는데, firebase와 구글의 문제라 고칠 수가 없다... 하지만 로그인 흐름 자체는 문제가 없기 때문에, 만약 제품에 이용하고 싶으면 리다이렉션 방식을 이용하자.

 

import {
  browserSessionPersistence,
  GoogleAuthProvider,
  setPersistence,
  signInWithPopup,
} from "firebase/auth";
import { authService } from "../firebase/fbInstance";
import useZustandAuthStore from "../store/ZustandAuthStore";

function GoogleLogin() {
  const setUsername = useZustandAuthStore((state) => state.setUsername);

  async function handleGoogleLogin() {
    const provider = new GoogleAuthProvider();
    console.log(provider);

    setPersistence(authService, browserSessionPersistence).then(() => {
      signInWithPopup(authService, provider)
        .then((data) => {
          setUsername(data.user.displayName);
          console.log(data);
        })
        .catch((err) => {
          console.log(err);
        });
    });
  }

  return (
    <button type="button" onClick={handleGoogleLogin}>
      구글 로그인
    </button>
  );
}

export default GoogleLogin;

 

구글 로그인 컴포넌트는 다음과 같고 이제 마지막으로 로그아웃 기능을 보자.

 

7. 로그아웃 컴포넌트

 

로그아웃 컴포넌트는 상당히 간단하다.

 

import { useNavigate } from "react-router-dom";
import { authService } from "../firebase/fbInstance";

function Logout() {
  //const navigate = useNavigate();
  function onLogOutClik() {
    authService.signOut();
    //navigate("../");
  }
  return (
    <button type="button" onClick={onLogOutClik}>
      로그아웃
    </button>
  );
}

export default Logout;

 

firebase.auth의 signOut 함수를 통해서 간단히 로그아웃을 할 수 있다.

 

만약 라우트 보호가 필요하면, useNavigation을 통해서 페이지를 이동시키자.

 

8. 결과

 

결과 움짤

 

구글 로그인 기능은 간단히 구현되었고, 새로고침을 해도 세션유지가 되어서 화면깜빡임 없이 로그인 상태를 유지할 수 있었다.

 

파이어베이스 콘솔로 들어가면 다음과 같이 사용자가 제대로 등록되었음을 알 수 있다.

 

 

팝업화면도 녹화할려면 프로그램을 깔아야해서 귀찮아서 폰으로 영상 찍었다...

 

다음은 카카오 소셜 로그인입니다.

혹시 궁금한 점 있으시면 댓글로 물어봐주세요.