티스토리 뷰
구글 로그인 · Gmail 발신, 다음에 또 헤매지 않기 위한 설정 노트
사이드 프로젝트마다 반복하는 일이 있다. 구글로 로그인 붙이기, Gmail로 인증 메일·뉴스레터 보내기.
할 때마다 GCP 콘솔 UI가 바뀌어 있고, OAuth Playground에서 unauthorized_client를 한 번씩 맞는다.
이 글은 그 과정을 처음부터 다시 따라 할 수 있게 정리한 메모다.
특정 프레임워크 문서가 아니라, 새 레포를 열었을 때 이 글만 보고 설정을 끝내는 것이 목표다.
Google Cloud Console UI는 자주 바뀐다. 예전 OAuth 동의 화면은 지금 Google Auth Platform으로 보이며, 메뉴가 개요 / 브랜딩 / 대상 / 클라이언트 / 데이터 액세스로 나뉜다. 아래는 이 최신 메뉴 기준이다.
목차
- 먼저 큰 그림
- 구글 로그인과 Gmail 발신은 다른 일
- GCP 프로젝트부터
- Google Auth Platform 공통 설정
- 구글 로그인 붙이기
- Gmail로 메일 보내기
- 환경변수 한곳에 모으기
- 검증 체크리스트
- 자주 나는 오류
- 새 프로젝트용 빠른 체크리스트
1. 먼저 큰 그림
왜 OAuth인가
서버가 사용자 Gmail로 메일을 보내거나, “구글로 로그인”을 하려면 Google 비밀번호를 앱에 넣으면 안 된다.
대신 OAuth2로 “이 앱이 이 권한을 가졌다”는 증명을 받는다.
등장인물은 셋이다.
| 역할 | 예시 |
|---|---|
| 사용자 | you@gmail.com |
| 앱 | 내 서비스 (Client ID / Secret) |
| 인가 서버 |
핵심 규칙 하나만 기억하면 된다.
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 프로젝트부터
- Google Cloud Console 접속
- 발신·로그인에 쓸 Google 계정으로 로그인
- 상단에서 새 프로젝트 생성 (또는 기존 프로젝트 선택)
- 이후 작업은 항상 그 프로젝트가 선택된 상태에서
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 클라이언트 만들기
- 클라이언트 → 클라이언트 만들기
- 유형: 웹 애플리케이션
- 이름 예:
myapp-google-login - 승인된 리디렉션 URI에 앱 콜백 등록:
# 로컬
http://localhost:3000/api/auth/google/callback
# 배포
https://your-domain.com/api/auth/google/callback
- 값을 환경변수에 저장:
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이 로그인용 클라이언트와 같은 쌍이라는 점이다.
배포할 때
- 호스팅(Vercel 등)에
GOOGLE_CLIENT_*,BASE_URL등록 - GCP 클라이언트에 배포 도메인 콜백 URI 추가
- 로컬 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. 발신용 클라이언트를 따로 만든다
로그인용과 별도로 만든다.
- 클라이언트 → 클라이언트 만들기
- 유형: 웹 애플리케이션
- 이름 예:
myapp-gmail-sender - 승인된 리디렉션 URI:
https://developers.google.com/oauthplayground
여기서 Playground URL의 의미를 헷갈리기 쉽다.
Playground로 메일을 보내는 것이 아니다.
동의 후 authorization code를 받아올 도착지로 그 주소를 빌려 쓰는 것이다.
코드의redirectUri와 Console 등록값이 완전히 같아야 한다.
- 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으로.
실무적으로는 작은 스크립트를 하나 두는 편이 안전하다.
- 내
GMAIL_CLIENT_ID/SECRET과 redirect URI로 동의 URL을 만든다.access_type=offlineprompt=consent(refresh token을 다시 받기 위해)- scope:
https://mail.google.com/
- 브라우저에서 발신에 쓸 Gmail로 로그인·동의한다.
- “확인되지 않은 앱”이면 고급 → 이동.
- 리다이렉트된 주소에서
code=값만 복사한다.- 예:
https://developers.google.com/oauthplayground/?code=4/0A...&scope=... code=뒤부터 다음&앞까지
- 예:
- Playground의 Exchange 버튼은 누르지 않는다.
- 내 서버/스크립트로
code + client_id + client_secret을 Google/token에 보낸다. - 응답의
refresh_token을.env에 저장한다.
refresh_token이 안 오면:
- Google 계정 → 연결된 앱에서 앱 권한 삭제
- 동의부터 다시 (
prompt=consent+access_type=offline)
6-6. 굳이 Playground UI로 교환할 때
- Playground 우측 톱니바퀴
- Use your own OAuth credentials 체크
- 내
GMAIL_CLIENT_ID/SECRET입력 - 그 다음에 Authorize → Exchange
요청 로그에 여전히 407408718192...가 보이면, 아직 본인 자격증명이 적용되지 않은 상태다.
6-7. 발송 시 런타임
refresh_token + client_id/secret
→ Google이 access_token 발급
→ Nodemailer(Gmail OAuth2) 또는 Gmail API로 전송
검증은 단순하다.
- refresh → access 토큰 교환이 성공하는지
- 테스트 주소로 실제 메일이 도착하는지
로그인용 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는 내 클라이언트로만 교환
이 다섯 줄이면 대부분 다시 헤매지 않는다.
'Develog' 카테고리의 다른 글
| 1인창업가 배포 체크리스트 - 배포환경,도메인,GA,SEO,마케팅 (0) | 2025.12.15 |
|---|---|
| ChatGPT Apps SDK 퀵스타트 오류해결 Quick Start Error Fix (0) | 2025.11.24 |
| 1인창업가를 위한 수익화 결제 모듈 총정리! RevenueCat, PG사, 토스페이먼츠 등 (0) | 2025.11.21 |
| 검색엔진 최적화(SEO)를 위한 자동 인덱싱 설정(Google Search Console, Indexing API, IndexNow API) (0) | 2025.07.15 |
| [FastMCP] MCP제작-날씨알리미(1) : 프로젝트 소개 (0) | 2025.04.22 |
- Total
- Today
- Yesterday
- 랩다이아 가드링
- 1인창업가 배포
- 치지직 댓글
- GA
- API
- 코딩
- modelcontextprotocol
- 학점은행제
- 개발팀 회고
- ai에이전트
- 아프리카TV 채팅
- SEO
- 미래트렌드
- 아프리카tv 댓글
- 치지직 채팅
- 개발자
- 민준주얼리 웨딩밴드
- 유투브 댓글
- 스트리밍 채팅 API
- 수집
- 구스피크
- 2025트렌드
- MCP
- 챗지피티 MCP
- aiagent
- ai
- it비즈니스
- 스트리밍 댓글 API
- 유투브 채팅
- goosepeak
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
