웹앱에 로그인 기능을 넣을 때 가장 먼저 고려하는 게 소셜 로그인이다. 직접 회원가입 시스템을 만들면 비밀번호 해싱, 이메일 인증, 비밀번호 찾기 등 신경 쓸 게 한두 가지가 아니다. Google OAuth를 쓰면 이런 고민을 한 번에 해결할 수 있다.

실제로 사이드 프로젝트에 Google 로그인을 붙여본 경험을 바탕으로 정리한다. 생각보다 삽질 포인트가 몇 개 있어서, 순서대로 따라하면 빠지지 않게 적어봤다.

Google Cloud Console 설정

프로젝트 생성

  1. Google Cloud Console에 접속
  2. 상단의 프로젝트 선택 → 새 프로젝트 만들기
  3. 프로젝트 이름을 적당히 짓고 만들기

OAuth 동의 화면 설정

이게 빠뜨리기 쉬운 부분인데, OAuth 동의 화면을 먼저 설정해야 클라이언트 ID를 만들 수 있다.

  1. 왼쪽 메뉴 → APIs & Services → OAuth consent screen
  2. User Type은 External 선택 (내부용이 아니면)
  3. 앱 이름, 사용자 지원 이메일, 개발자 연락처 입력
  4. 스코프(Scopes)에서 email, profile, openid 추가
  5. 테스트 사용자에 본인 이메일 추가 (앱이 검증되기 전까지는 테스트 사용자만 로그인 가능)

클라이언트 ID 생성

  1. APIs & Services → Credentials → Create Credentials → OAuth client ID
  2. 애플리케이션 유형: 웹 애플리케이션
  3. 승인된 JavaScript 출처: http://localhost:3000 (개발용)
  4. 승인된 리디렉션 URI: http://localhost:3000/auth/callback
  5. 생성하면 Client ID와 Client Secret이 나옴 → 잘 보관

여기서 삽질 포인트: 리디렉션 URI가 정확히 일치해야 한다. 끝에 슬래시 하나 차이로도 에러가 난다. http://localhost:3000/auth/callbackhttp://localhost:3000/auth/callback/은 다르다.

프론트엔드 구현

Google에서 제공하는 새로운 Sign In with Google 라이브러리를 쓰는 게 가장 간단하다:

<script src="https://accounts.google.com/gsi/client" async defer></script>

<div id="g_id_onload"
  data-client_id="YOUR_CLIENT_ID.apps.googleusercontent.com"
  data-callback="handleCredentialResponse">
</div>
<div class="g_id_signin" data-type="standard"></div>

콜백 함수:

function handleCredentialResponse(response) {
  // response.credential에 JWT 토큰이 들어있다
  const token = response.credential;
  
  // 이 토큰을 백엔드로 보내서 검증
  fetch('/auth/google', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token }),
  })
    .then(res => res.json())
    .then(data => {
      if (data.success) {
        // 로그인 성공 처리
        localStorage.setItem('session', data.sessionToken);
        window.location.href = '/dashboard';
      }
    });
}

백엔드 검증 (Node.js)

프론트에서 받은 JWT 토큰을 백엔드에서 검증해야 한다. 절대 프론트에서 받은 토큰을 그대로 신뢰하면 안 된다.

import { OAuth2Client } from 'google-auth-library';

const client = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);

async function verifyGoogleToken(token) {
  const ticket = await client.verifyIdToken({
    idToken: token,
    audience: process.env.GOOGLE_CLIENT_ID,
  });
  
  const payload = ticket.getPayload();
  
  return {
    googleId: payload.sub,
    email: payload.email,
    name: payload.name,
    picture: payload.picture,
    emailVerified: payload.email_verified,
  };
}

Express 라우트 예시:

app.post('/auth/google', async (req, res) => {
  try {
    const { token } = req.body;
    const userInfo = await verifyGoogleToken(token);
    
    // DB에서 사용자 찾기 또는 생성
    let user = await db.findUserByGoogleId(userInfo.googleId);
    if (!user) {
      user = await db.createUser({
        googleId: userInfo.googleId,
        email: userInfo.email,
        name: userInfo.name,
        avatar: userInfo.picture,
      });
    }
    
    // 세션 토큰 생성
    const sessionToken = generateSessionToken(user.id);
    
    res.json({ success: true, sessionToken });
  } catch (error) {
    res.status(401).json({ success: false, error: 'Invalid token' });
  }
});

토큰 관리

Access Token vs Refresh Token

간단한 프로필 정보만 필요하면 ID Token으로 충분하다. 하지만 Google Calendar이나 Gmail 같은 API를 호출해야 한다면 Access Token이 필요하고, 이 토큰은 1시간 후 만료된다. 그래서 Refresh Token을 받아서 저장해둬야 한다.

Refresh Token을 받으려면 Authorization Code Flow를 써야 한다:

const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
  `client_id=${CLIENT_ID}` +
  `&redirect_uri=${REDIRECT_URI}` +
  `&response_type=code` +
  `&scope=openid email profile` +
  `&access_type=offline` +  // 이게 있어야 refresh token을 줌
  `&prompt=consent`;        // 매번 동의 화면을 보여줌

access_type=offline을 빼먹으면 refresh token을 안 준다. 그리고 prompt=consent를 넣어야 재로그인 시에도 refresh token을 새로 발급받을 수 있다. 이거 때문에 한참 헤맸다.

세션 관리

JWT를 세션 토큰으로 쓰는 방법도 있지만, 나는 서버사이드 세션을 선호한다. JWT는 토큰을 발급한 후 무효화하기가 어렵기 때문이다. 사용자가 로그아웃해도 토큰이 만료되기 전까지는 유효하다는 게 찜찜하다.

import crypto from 'crypto';

function generateSessionToken(userId) {
  const token = crypto.randomBytes(32).toString('hex');
  // Redis나 DB에 저장
  sessionStore.set(token, { userId, createdAt: Date.now() });
  return token;
}

프로덕션 배포 시 주의사항

  1. 리디렉션 URI 업데이트: localhost 대신 실제 도메인으로 변경. Cloud Console에서 추가해줘야 함
  2. HTTPS 필수: 프로덕션에서는 반드시 HTTPS를 써야 한다. Google이 HTTP 리디렉션 URI를 localhost 외에는 허용하지 않음
  3. OAuth 동의 화면 검증: 테스트 모드에서 벗어나려면 Google에 앱 검증을 요청해야 한다. 민감한 스코프를 쓰면 보안 심사도 필요
  4. Client Secret 관리: 절대 프론트엔드 코드에 넣지 말 것. 환경 변수로 관리

정리

Google OAuth 연동 자체는 그리 어렵지 않다. 하지만 리디렉션 URI 불일치, refresh token 미발급, 동의 화면 미설정 같은 소소한 함정들이 있어서 처음 해보면 시간을 잡아먹을 수 있다. 이 글에 정리한 순서대로 하면 큰 문제 없이 연동할 수 있을 거다.

개인적으로는 소셜 로그인만으로 인증을 처리하는 걸 선호한다. 비밀번호 관리에서 자유로워지는 것만으로도 보안 리스크가 크게 줄어든다. Google OAuth에 익숙해지면 GitHub, Kakao 같은 다른 OAuth 프로바이더를 추가하는 것도 비슷한 패턴이라 금방 할 수 있다.