React에서 useGoogleLogin과 useGoogleLogout을 활용한 OAuth 2.0 통합 심화 가이드

React 애플리케이션에 Google 인증 기능을 추가할 때, react-google-login 라이브러리의 Hook API인 useGoogleLogin과 useGoogleLogout은 컴포넌트 기반 접근법보다 더 유연하고 간결한 코드를 작성할 수 있게 해줍니다. 이 문서에서는 해당 Hook들의 초기 설정부터 고급 시나리오까지의 구현 방법을 상세히 다루며, 코드 구조와 변수 명명을 변경하여 실제 프로젝트 적용 시 참고할 수 있는 최적화된 패턴을 제시합니다.

Hook 기반 인증의 장점

전통적인 <GoogleLogin /> 컴포넌트를 사용하는 대신 Hook을 활용하면 다음과 같은 이점을 얻을 수 있습니다:

  • 상태 관리 분리: 인증 로직을 UI 렌더링과 분리하여 관심사 분리 원칙(Separation of Concerns)을 준수합니다.
  • 재사용성 향상: 커스텀 버튼이나 특정 조건부 렌더링 등 다양한 UI 요소에 쉽게 통합할 수 있습니다.
  • 성능 개선: 필요할 때만 스크립트를 로드하고 상태를 업데이트하므로 불필요한 리렌더링을 최소화합니다.

환경 설정 및 기본 구현

먼저 패키지를 설치하고 Google Cloud Console에서 OAuth 클라이언트 ID를 발급받아야 합니다.

npm install react-google-login --save

가장 기본적인 로그인 버튼 구현 예제입니다. 여기서는 authConfig 객체를 통해 옵션을 명확하게 구분하여 가독성을 높였습니다.

import React from 'react';
import { useGoogleLogin } from 'react-google-login';

// 환경 변수 또는 상수로 관리하는 것이 좋습니다.
const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID;

function SimpleAuthButton() {
  const authOptions = {
    clientId: GOOGLE_CLIENT_ID,
    onSuccess: (response) => {
      console.log('인증 완료:', response);
      // 여기서 백엔드 API 호출이나 상태 업데이트 수행
    },
    onFailure: (error) => {
      console.error('인증 오류 발생:', error);
    }
  };

  // signIn 함수는 클릭 이벤트 핸들러로 직접 연결됩니다.
  const { signIn } = useGoogleLogin(authOptions);

  return (
    <button 
      className="btn-google-auth" 
      onClick={signIn}
    >
      Google 계정으로 계속하기
    </button>
  );
}

useGoogleLogin 파라미터 심층 분석

Hook의 동작을 세밀하게 제어하기 위해 주요 구성 요소를 이해해야 합니다.

설정 키 타입 설명
clientId string (필수) Google Developer Console에서 생성한 OAuth 클라이언트 ID.
onSuccess function (필수) 사용자가 성공적으로 인증되었을 때 실행되는 콜백. 응답 객체에는 토큰 및 사용자 프로필 정보가 포함됩니다.
onFailure function (필수) 인증 과정 중 오류가 발생했을 때 실행되는 콜백.
scope string 요청할 권한 범위. 기본값은 'profile email'이며, 캘린더나 드라이브 접근 등이 필요하면 추가해야 합니다.
uxMode string 'popup'(기본값) 또는 'redirect'. 모바일 웹뷰 등에서 팝업이 차단될 경우 redirect 모드를 사용합니다.
autoLoad boolean 컴포넌트가 마운트될 때 자동으로 로그인 창을 띄울지 여부.
isSignedIn boolean 이미 로그인된 사용자를 감지하여 즉시 onSuccess를 트리거할지 여부.

고급 시나리오: 오프라인 액세스 및 리다이렉트 모드

서버 사이드에서 Refresh Token을 사용하여 장기적인 접근 권한을 유지해야 하는 경우, responseType을 'code'로 설정하고 서버로 권한 코드를 전송하는 방식을 사용합니다.

const handleCodeExchange = async (code) => {
  // 백엔드 엔드포인트로 권한 코드(code)를 보내 Access/Refresh Token을 교환
  try {
    const res = await fetch('/api/auth/google/exchange', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ code })
    });
    if (!res.ok) throw new Error('Token exchange failed');
    const data = await res.json();
    // 세션 쿠키 설정 또는 로컬 스토리지 저장
  } catch (err) {
    console.error('Server-side token exchange error:', err);
  }
};

const advancedLoginConfig = {
  clientId: GOOGLE_CLIENT_ID,
  scope: 'https://www.googleapis.com/auth/calendar.readonly',
  responseType: 'code', // Authorization Code Flow 사용
  uxMode: 'redirect',   // 리다이렉트 방식 사용
  redirectUri: `${window.location.origin}/callback`, // 반드시 Google Console에 등록되어야 함
  prompt: 'consent',    // 항상 동의 화면 표시 (Refresh Token 확보용)
  accessType: 'offline', // 오프라인 액세스 요청
  onSuccess: (response) => {
    if (response.code) {
      handleCodeExchange(response.code);
    }
  },
  onFailure: (err) => console.warn('OAuth flow failed', err),
  onAutoLoadFinished: (isLoggedIn) => {
    console.log(`자동 로드 완료. 로그인 상태: ${isLoggedIn}`);
  }
};

const { signIn, loaded } = useGoogleLogin(advancedLoginConfig);

로그아웃 처리: useGoogleLogout

단순히 프론트엔드의 상태를 제거하는 것뿐만 아니라, Google 세션을 완전히 종료하고 브라우저 캐시 데이터를 정리해야 할 때 useGoogleLogout을 사용합니다.

import { useGoogleLogout } from 'react-google-login';

function SessionTerminator() {
  const logoutConfig = {
    clientId: GOOGLE_CLIENT_ID,
    onLogoutSuccess: () => {
      // 1. 로컬 저장소 데이터 삭제
      localStorage.removeItem('userProfile');
      sessionStorage.clear();
      
      // 2. Redux/MobX 등 전역 상태 초기화 (dispatch 등)
      // store.dispatch(resetUserState());
      
      // 3. 메인 페이지로 리다이렉트
      window.location.href = '/home';
    },
    onFailure: () => {
      alert('세션 종료에 실패했습니다. 다시 시도해주세요.');
    }
  };

  const { signOut } = useGoogleLogout(logoutConfig);

  return (
    <button onClick={signOut} className="btn-logout">
      로그아웃
    </button>
  );
}

실무 적용: 완전한 인증 컨테이너 컴포넌트

로그인과 로그아웃 로직을 하나의 컴포넌트로 캡슐화하고, 사용자 정보 유무에 따라 UI를 분기하는 패턴입니다. 이는 재사용 가능한 인증 위젯으로 활용될 수 있습니다.

import React, { useState, useEffect } from 'react';
import { useGoogleLogin, useGoogleLogout } from 'react-google-login';

const CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID;

export default function AuthContainer() {
  // 로컬 스토리지에서 이전 세션 복원
  const [currentUser, setCurrentUser] = useState(() => {
    const stored = localStorage.getItem('google_user_data');
    return stored ? JSON.parse(stored) : null;
  });

  // 로그인 Hook 설정
  const loginResponse = useGoogleLogin({
    clientId: CLIENT_ID,
    onSuccess: (resp) => {
      const profile = {
        id: resp.googleId,
        displayName: resp.profileObj.name,
        email: resp.profileObj.email,
        avatarUrl: resp.profileObj.imageUrl,
        accessToken: resp.accessToken,
        expiresAt: Date.now() + (resp.expiresIn * 1000)
      };
      
      setCurrentUser(profile);
      localStorage.setItem('google_user_data', JSON.stringify(profile));
    },
    onFailure: (err) => {
      console.error('Login failed:', err);
      // 에러 유형별 처리 가능
    }
  });

  // 로그아웃 Hook 설정
  const logoutResponse = useGoogleLogout({
    clientId: CLIENT_ID,
    onLogoutSuccess: () => {
      setCurrentUser(null);
      localStorage.removeItem('google_user_data');
    }
  });

  // 토큰 만료 확인을 위한 효과 (선택 사항)
  useEffect(() => {
    if (currentUser && currentUser.expiresAt < Date.now()) {
      // 토큰 만료 시 자동 로그아웃 또는 갱신 로직
      logoutResponse.signOut();
    }
  }, [currentUser]);

  if (currentUser) {
    return (
      <div className="logged-in-view">
        <img src={currentUser.avatarUrl} alt={currentUser.displayName} />
        <p>환영합니다, {currentUser.displayName}</p>
        <button onClick={logoutResponse.signOut}>세션 종료</button>
      </div>
    );
  }

  return (
    <div className="logged-out-view">
      <button 
        onClick={loginResponse.signIn}
        disabled={!loginResponse.loaded}
      >
        {loginResponse.loaded ? "Google 로그인" : "준비 중..."}
      </button>
    </div>
  );
}

디버깅 및 트러블슈팅

구현 과정에서 자주 발생하는 문제와 그 해결책은 다음과 같습니다.

  • Popup Blocked 오류: 일부 브라우저는 사용자 제스처(클릭) 없이 열리는 팝업을 차단합니다. signIn 함수를 버튼의 onClick 속성에 직접 바인딩하여 사용자 인터랙션 내에서 실행되도록 보장하십시오.
  • CORS 오류: redirect_uri가 Google Cloud Console의 "Authorized redirect URIs" 목록에 정확히 일치하지 않으면 인증이 실패합니다. 프로토콜(http/https)과 포트 번호까지 완벽하게 매칭되어야 합니다.
  • Third-party Cookies 비활성화: Chrome 등의 브라우저에서 서드파티 쿠키를 제한하는 정책을 시행 중일 수 있습니다. 이 경우 uxMode: 'redirect'를 사용하여 서버 사이드 리다이렉트 방식으로 전환하는 것이 안정적입니다.
  • TypeScript 타입 정의: 공식 라이브러리에는 index.d.ts가 포함되어 있어 IDE에서 자동 완성과 타입 체크를 지원하지만, 복잡한 커스텀 필드가 필요한 경우 자체 인터페이스를 확장하여 사용할 수 있습니다.

프로젝트 구조 참고

라이브러리 내부 소스는 다음과 같이 모듈화되어 있어 필요 시 Fork하여 수정하거나 기여할 수 있습니다.

src/
├── hooks/
│   ├── use-google-login.js    # 메인 로그인 로직 및 스크립트 로드 관리
│   └── use-google-logout.js   # 세션 해제 및 스크립트 제거 로직
├── components/
│   ├── google-login.js        # 레거시 컴포넌트 호환용 래퍼
│   └── google-logout.js       # 레거시 컴포넌트 호환용 래퍼
└── utils/
    ├── load-script.js         # Google GSI 스크립트 동적 삽입 유틸리티
    └── remove-script.js       # 메모리 누수 방지를 위한 스크립트 정리

태그: React oauth2 Google Login Hooks Frontend Authentication

9월 30일 06:20에 게시됨