티스토리 뷰

728x90

구글 로그인 · Gmail 발신, 다음에 또 헤매지 않기 위한 설정 노트

사이드 프로젝트마다 반복하는 일이 있다. 구글로 로그인 붙이기, Gmail로 인증 메일·뉴스레터 보내기.
할 때마다 GCP 콘솔 UI가 바뀌어 있고, OAuth Playground에서 unauthorized_client를 한 번씩 맞는다.

이 글은 그 과정을 처음부터 다시 따라 할 수 있게 정리한 메모다.
특정 프레임워크 문서가 아니라, 새 레포를 열었을 때 이 글만 보고 설정을 끝내는 것이 목표다.

Google Cloud Console UI는 자주 바뀐다. 예전 OAuth 동의 화면은 지금 Google Auth Platform으로 보이며, 메뉴가 개요 / 브랜딩 / 대상 / 클라이언트 / 데이터 액세스로 나뉜다. 아래는 이 최신 메뉴 기준이다.


목차

  1. 먼저 큰 그림
  2. 구글 로그인과 Gmail 발신은 다른 일
  3. GCP 프로젝트부터
  4. Google Auth Platform 공통 설정
  5. 구글 로그인 붙이기
  6. Gmail로 메일 보내기
  7. 환경변수 한곳에 모으기
  8. 검증 체크리스트
  9. 자주 나는 오류
  10. 새 프로젝트용 빠른 체크리스트

1. 먼저 큰 그림

왜 OAuth인가

서버가 사용자 Gmail로 메일을 보내거나, “구글로 로그인”을 하려면 Google 비밀번호를 앱에 넣으면 안 된다.
대신 OAuth2로 “이 앱이 이 권한을 가졌다”는 증명을 받는다.

등장인물은 셋이다.

역할 예시
사용자 you@gmail.com
내 서비스 (Client ID / Secret)
Google 인가 서버

핵심 규칙 하나만 기억하면 된다.

authorization code와 refresh token은 항상 특정 Client ID에 묶인다.
A 클라이언트로 받은 code를 B 클라이언트로 교환하면 무조건 실패한다.

나중에 Playground에서 헤매는 이유의 대부분이 이 한 줄이다.

토큰 세 종류

토큰 수명 역할
authorization code 수 분, 1회용 동의 직후 받는 교환권
access token 짧음 (보통 ~1시간) API 호출·SMTP 발송용
refresh token access token을 다시 뽑는 장기 허가증

비유하면 이렇다.

  • Client ID / Secret → 앱의 사업자등록증
  • 동의 화면 → 사용자가 권한에 서명
  • code → 일회용 교환권
  • refresh token → 장기 회원카드
  • access token → 당일 입장권

최초 1회 vs 매일

[최초 1회 — 권한 받기]
브라우저 동의 → code → client_id/secret으로 교환 → refresh_token 저장

[매일 — 권한 사용]
refresh_token → access_token → 메일 발송

구글 로그인은 “지금 이 사람이 누구인지”만 알면 되므로, 서버에 refresh token을 오래 안 두는 구현도 많다.
Gmail 발신(뉴스레터, 인증 메일 배치)은 사용자 브라우저 없이 서버가 보내야 하므로 refresh token을 환경변수에 보관하는 패턴이 일반적이다.


2. 구글 로그인과 Gmail 발신은 다른 일

같은 GCP 프로젝트를 써도 된다. 다만 OAuth 클라이언트는 용도별로 나누는 것을 강력히 권장한다.
하나를 공유하면 redirect URI·스코프·토큰이 섞여 invalid_grant로 이어지기 쉽다.

구글 로그인 Gmail 발신
목적 사용자 식별 + 세션 서버가 메일 발송
대표 env GOOGLE_CLIENT_ID / SECRET GMAIL_USER / CLIENT_ID / SECRET / REFRESH_TOKEN
Redirect URI 앱 콜백
예: https://your.app/api/auth/google/callback 토큰 발급용
예: https://developers.google.com/oauthplayground
Scope openid, email, profile https://mail.google.com/
refresh token 필수는 아님 필수 (배치 발송)

Redirect URI는 코드에 넣은 값과 Console에 등록한 값이 한 글자도 같아야 한다.
문서에 /api/auth/callback이라고 적혀 있어도, 실제 코드가 /api/auth/google/callback이면 Console도 그쪽으로 맞춰야 한다.


3. GCP 프로젝트부터

  1. Google Cloud Console 접속
  2. 발신·로그인에 쓸 Google 계정으로 로그인
  3. 상단에서 새 프로젝트 생성 (또는 기존 프로젝트 선택)
  4. 이후 작업은 항상 그 프로젝트가 선택된 상태에서

API 사용 설정

용도 켤 API
구글 로그인 보통 OAuth만으로 충분
Gmail 발신 Gmail API 필수

경로: API 및 서비스 → 라이브러리 → Gmail API → 사용 설정


4. Google Auth Platform 공통 설정

진입 경로 예:

  • 예전: API 및 서비스 → OAuth 동의 화면
  • 현재: Google Auth Platform (또는 동의 화면 진입 후 같은 UI)

메뉴가 헷갈릴 때

왼쪽 메뉴 예전 이름 여기서 할 일
개요 상태 확인
브랜딩 앱 정보 앱 이름, 지원 이메일
대상 게시 상태 / 테스트 사용자 Testing ↔ Production
클라이언트 사용자 인증 정보 Client ID / Secret 발급
데이터 액세스 범위(Scopes) 스코프 추가

즉:

  • 스코프데이터 액세스
  • 게시 상태대상
  • CLIENT_ID 발급클라이언트

브랜딩

  • 앱 이름
  • 사용자 지원 이메일 / 개발자 연락처
  • 로고·홈페이지는 비워도 됨 → 저장

데이터 액세스 (Scopes)

구글 로그인

openid
email
profile

앱이 동의 URL에서 요청하는 scope와 Console에 등록한 scope를 맞춘다.

Gmail 발신

https://mail.google.com/

최소 권한만 원하면:

https://www.googleapis.com/auth/gmail.send

앱 코드가 요청하는 스코프와 Console 등록이 어긋나면 동의·토큰 단계에서 깨진다.

대상 (Publishing status)

상태 의미 refresh token
Testing 테스트 사용자만 동의 가능 약 7일 후 만료
In production 게시됨 장기 유지 (배치용 권장)

Testing을 유지할 때만 테스트 사용자에 본인 Gmail을 추가한다.
Production이면 테스트 사용자 추가는 필수는 아니다.

배치로 메일을 보내는 서비스라면, Testing 상태로 두면 일주일 뒤 invalid_grant를 다시 만난다. Production으로 두는 편이 낫다.

“확인되지 않은 앱” 경고

프로덕션 게시Google 검증(Verification) 은 별개다.

  • Production으로 바꿔도 mail.google.com 같은 제한/민감 스코프는 검증 전까지 “확인되지 않은 앱” 경고가 뜬다.
  • 본인만 쓰는 개인·내부 프로젝트면 검증 없이 진행해도 된다.
  • 동의 화면에서 고급 → (앱 이름)으로 이동 후 허용하면 된다.

경고가 보인다고 설정이 잘못된 것은 아니다.
반대로, Production이라고 해서 그 경고가 바로 사라지는 것도 아니다.


5. 구글 로그인 붙이기

OAuth 클라이언트 만들기

  1. 클라이언트 → 클라이언트 만들기
  2. 유형: 웹 애플리케이션
  3. 이름 예: myapp-google-login
  4. 승인된 리디렉션 URI에 앱 콜백 등록:
# 로컬
http://localhost:3000/api/auth/google/callback

# 배포
https://your-domain.com/api/auth/google/callback
  1. 값을 환경변수에 저장:
GOOGLE_CLIENT_ID=....apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=GOCSPX-...
NEXT_PUBLIC_BASE_URL=http://localhost:3000

배포 시 NEXT_PUBLIC_BASE_URL을 실제 도메인으로 바꾼다.
코드에서 redirect URI를 ${BASE_URL}/api/auth/google/callback처럼 조합한다면, BASE_URL과 Console URI가 함께 맞아야 한다.

앱에서 일어나는 일

1) /api/auth/google
   - state 쿠키 저장 (CSRF 방지)
   - Google 동의 URL로 리다이렉트
   - scope: openid email profile

2) 사용자가 Google에서 로그인·동의

3) /api/auth/google/callback?code=...&state=...
   - state 검증
   - code → token 교환 (GOOGLE_CLIENT_*)
   - userinfo로 email/profile 조회
   - DB 유저 생성/조회 + 세션(JWT 등) 발급
   - 앱으로 리다이렉트

Supabase Auth를 쓰지 않고 Google OAuth + 자체 JWT로 구현해도 흐름은 같다.
중요한 건 콜백에서 쓰는 Client ID/Secret이 로그인용 클라이언트와 같은 쌍이라는 점이다.

배포할 때

  1. 호스팅(Vercel 등)에 GOOGLE_CLIENT_*, BASE_URL 등록
  2. GCP 클라이언트에 배포 도메인 콜백 URI 추가
  3. 로컬 URI만 있는 채 배포하면 redirect_uri_mismatch

6. Gmail로 메일 보내기

목표: 서버·크론이 GMAIL_USER 명의로 메일을 보낸다.

GMAIL_USER=you@gmail.com
GMAIL_CLIENT_ID=...
GMAIL_CLIENT_SECRET=...
GMAIL_REFRESH_TOKEN=...

6-1. Gmail API 켜기

라이브러리 → Gmail API → 사용 설정

6-2. 발신용 클라이언트를 따로 만든다

로그인용과 별도로 만든다.

  1. 클라이언트 → 클라이언트 만들기
  2. 유형: 웹 애플리케이션
  3. 이름 예: myapp-gmail-sender
  4. 승인된 리디렉션 URI:
https://developers.google.com/oauthplayground

여기서 Playground URL의 의미를 헷갈리기 쉽다.

Playground로 메일을 보내는 것이 아니다.
동의 후 authorization code를 받아올 도착지로 그 주소를 빌려 쓰는 것이다.
코드의 redirectUri와 Console 등록값이 완전히 같아야 한다.

  1. Client ID / Secret을 GMAIL_CLIENT_*에 넣는다.

6-3. 데이터 액세스에 Gmail 스코프 추가

https://mail.google.com/

6-4. 대상은 Production 권장

Testing이면 refresh token이 약 7일 후 만료된다.
뉴스레터·크론 발송이면 Production이 안전하다.

6-5. Refresh Token 발급 — 가장 헤매는 구간

OAuth Playground의 Exchange 버튼을 기본 설정으로 누르면 실패하기 쉽다.
Playground는 자체 기본 Client ID를 쓰기 때문이다.

client_id=407408718192.apps.googleusercontent.com

이건 Google Playground 기본 클라이언트다. 내 GCP 클라이언트가 아니다.
그 상태로 교환하면 unauthorized_client가 나는 것이 정상이다.

권장 원칙:

동의는 브라우저에서, 토큰 교환은 반드시 내 Client ID / Secret으로.

실무적으로는 작은 스크립트를 하나 두는 편이 안전하다.

  1. GMAIL_CLIENT_ID/SECRET과 redirect URI로 동의 URL을 만든다.
    • access_type=offline
    • prompt=consent (refresh token을 다시 받기 위해)
    • scope: https://mail.google.com/
  2. 브라우저에서 발신에 쓸 Gmail로 로그인·동의한다.
  3. “확인되지 않은 앱”이면 고급 → 이동.
  4. 리다이렉트된 주소에서 code= 값만 복사한다.
    • 예: https://developers.google.com/oauthplayground/?code=4/0A...&scope=...
    • code= 뒤부터 다음 & 앞까지
  5. Playground의 Exchange 버튼은 누르지 않는다.
  6. 내 서버/스크립트로 code + client_id + client_secret을 Google /token에 보낸다.
  7. 응답의 refresh_token.env에 저장한다.

refresh_token이 안 오면:

  1. Google 계정 → 연결된 앱에서 앱 권한 삭제
  2. 동의부터 다시 (prompt=consent + access_type=offline)

6-6. 굳이 Playground UI로 교환할 때

  1. Playground 우측 톱니바퀴
  2. Use your own OAuth credentials 체크
  3. GMAIL_CLIENT_ID / SECRET 입력
  4. 그 다음에 Authorize → Exchange

요청 로그에 여전히 407408718192...가 보이면, 아직 본인 자격증명이 적용되지 않은 상태다.

6-7. 발송 시 런타임

refresh_token + client_id/secret
  → Google이 access_token 발급
  → Nodemailer(Gmail OAuth2) 또는 Gmail API로 전송

검증은 단순하다.

  1. refresh → access 토큰 교환이 성공하는지
  2. 테스트 주소로 실제 메일이 도착하는지

로그인용 GOOGLE_*로 발신을 시도하면 안 된다.
발신은 GMAIL_*그 클라이언트로 발급한 refresh token이어야 한다.


7. 환경변수 한곳에 모으기

# 구글 로그인
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
NEXT_PUBLIC_BASE_URL=http://localhost:3000

# Gmail 발신
GMAIL_USER=
GMAIL_CLIENT_ID=
GMAIL_CLIENT_SECRET=
GMAIL_REFRESH_TOKEN=
환경 위치
로컬 .env / .env.local
웹 배포 Vercel Environment Variables 등
크론 발송 GitHub Actions Secrets 등

발송 배치에 최소로 넣을 것:

GMAIL_USER
GMAIL_CLIENT_ID
GMAIL_CLIENT_SECRET
GMAIL_REFRESH_TOKEN

8. 검증 체크리스트

구글 로그인

  • 로그인용 웹 클라이언트 생성
  • Redirect URI = 앱 콜백과 일치 (로컬 + 배포)
  • GOOGLE_CLIENT_*, BASE_URL 설정
  • 동의 → 콜백 → 세션 생성까지 확인
  • 배포 도메인 콜백을 Console에 추가했는지 확인

Gmail 발신

  • Gmail API 사용 설정
  • 발신용 웹 클라이언트 별도 생성
  • Redirect URI = https://developers.google.com/oauthplayground
  • 데이터 액세스에 Gmail 스코프
  • Audience = Production (배치용 권장)
  • GMAIL_* 4개 설정
  • refresh token을 GMAIL 클라이언트로 발급했는지 확인
  • 토큰 교환 성공
  • 테스트 메일 수신 확인

9. 자주 나는 오류

증상 원인 해결
unauthorized_client Playground 기본 client로 교환 내 CLIENT로 교환
invalid_grant refresh가 다른 클라이언트용이거나 폐기됨 Gmail 클라이언트로 재발급
invalid_grant (만료) Testing 7일 만료 Production 전환 후 재발급
redirect_uri_mismatch Console URI ≠ 코드 URI 철자까지 동일하게
access_denied Testing인데 테스트 사용자 없음 Audience에 이메일 추가 또는 Production
확인되지 않은 앱 미검증 + 민감/제한 스코프 정상. 고급 → 이동
로그인은 되는데 메일 실패 GOOGLE_*GMAIL_* 혼동 발신은 GMAIL_*
refresh_token 없음 이미 동의한 계정 앱 권한 삭제 후 재동의

Playground 실패 로그 읽는 법

요청에 이런 client_id가 보이면:

client_id=407408718192.apps.googleusercontent.com

내 앱이 아니다. Exchange를 그 상태로 하면 실패하는 것이 맞다.


10. 새 프로젝트용 빠른 체크리스트

다음에 새 레포를 열면 이것만 따라가면 된다.

[ ] GCP 프로젝트 생성/선택
[ ] (발신 시) Gmail API Enable
[ ] Auth Platform
    [ ] 브랜딩
    [ ] 데이터 액세스: login scopes + (필요 시) gmail scope
    [ ] 대상: Production (배치/장기 토큰이면)
[ ] 클라이언트 A — 로그인
    [ ] redirect = https://<app>/api/auth/.../callback
    [ ] env: GOOGLE_CLIENT_ID/SECRET, BASE_URL
[ ] 클라이언트 B — Gmail
    [ ] redirect = https://developers.google.com/oauthplayground
    [ ] env: GMAIL_USER, GMAIL_CLIENT_ID/SECRET
[ ] refresh token 발급
    [ ] 동의 URL → code 복사 → 내 CLIENT로 교환
    [ ] Playground 기본 Exchange는 쓰지 않기
[ ] GMAIL_REFRESH_TOKEN 저장
[ ] 토큰 교환 확인 → 테스트 메일 → 로그인 E2E

한 줄 요약

동의는 브라우저에서, 토큰 교환은 반드시 같은 Client ID / Secret으로.
Playground는 code를 받아오는 주소일 수 있지만, 기본 Exchange 버튼을 누르면 클라이언트가 바뀐다.


다음에 또 GCP를 열었을 때, 메뉴 이름이 조금 바뀌어 있어도 이 글의 매핑표만 보면 된다.

  • 스코프 → 데이터 액세스
  • 게시 상태 → 대상
  • 신분증 발급 → 클라이언트
  • 로그인과 발신은 클라이언트 분리
  • refresh는 내 클라이언트로만 교환

이 다섯 줄이면 대부분 다시 헤매지 않는다.

728x90