파이썬으로 유튜브 자동 업로드를 만들고 첫 OAuth 로그인을 시도하면 높은 확률로 이 화면을 만난다 — "액세스 차단됨: (앱 이름)에서 Google 인증 절차를 완료하지 않았습니다. 오류 403: access_denied". 결론부터: 앱이 잘못된 게 아니라 내 계정을 테스트 사용자로 등록 안 한 것이다. 1분이면 풀린다. 이 글은 그 해결부터, 영상+자막 자동 업로드 성공, 그리고 그 뒤에 숨어 있는 함정 두 개(비공개 잠금, 7일 토큰 만료)까지의 기록이다.
바쁜 사람을 위한 3줄 요약
- 증상: OAuth 첫 로그인에서 "액세스 차단됨 — 오류 403: access_denied".
- 원인: OAuth 동의 화면이 "테스트" 게시 상태일 때는 테스트 사용자로 등록된 계정만 로그인할 수 있는데, 본인 계정을 등록 안 했기 때문.
- 해결: Google Cloud 콘솔 → 대상(Audience) → 테스트 사용자에 본인 Gmail 추가. 이후 "확인되지 않은 앱" 경고는 고급 → 계속으로 통과.
배경
자동매매 시스템의 하루 결과를 쇼츠 영상으로 만드는 파이프라인을 운영 중인데, 마지막 단계(유튜브 업로드)만 수동이었다. 렌더된 mp4와 자막(srt)을 YouTube Data API v3로 자동 업로드하는 upload_youtube.py를 만들어 파이프라인 끝에 붙이는 게 목표였다.
- 사용 API:
videos.insert(영상 업로드),captions.insert(자막 등록) - 인증: OAuth 2.0 데스크톱 앱 클라이언트 (
google-auth-oauthlib) - 필요 스코프(scope — 앱에 부여할 권한 범위):
youtube.upload+youtube.force-ssl(자막 등록에 필요)
1. 증상: 첫 로그인에서 403
스크립트를 처음 실행하면 브라우저가 열리고 구글 로그인 창이 뜬다. 계정을 선택하는 순간:
액세스 차단됨: (프로젝트명)에서 Google 인증 절차를 완료하지 않았습니다.
오류 403: access_denied
여기서 헷갈리는 게, 에러 문구가 "Google 인증 절차(verification)를 완료하지 않았습니다"라서 구글의 앱 심사를 받아야만 쓸 수 있는 것처럼 읽힌다. 아니다. 개인용 자동화는 심사 없이 쓸 수 있다. 진짜 원인은 딴 데 있다.
2. 원인: 테스트 모드 + 테스트 사용자 미등록
Google Cloud에서 OAuth 클라이언트를 만들면 동의 화면(consent screen)의 게시 상태가 기본적으로 "테스트(Testing)"다. 테스트 상태의 앱은 심사가 필요 없는 대신, 테스트 사용자 목록에 등록된 계정만 로그인을 허용한다.
즉 구글의 로그인 판정 흐름에서 내가 어디서 걸렸는지 그림으로 보면:
[스크립트 실행 → 브라우저에서 구글 로그인 시도]
│
앱 게시 상태가 "테스트"인가? (새 프로젝트의 기본값 = 테스트)
│ 예
테스트 사용자 명단에 내 계정이 있는가?
│ │
│ 예 │ 아니오
▼ ▼
"확인되지 않은 앱" 경고 ❌ 오류 403: access_denied
→ 고급 → 계속 → 성공 (지금 이 상황)
앱 코드, 스코프, 클라이언트 시크릿 전부 문제없다. 명단에 내 이름이 없었을 뿐이다.
3. 해결: 테스트 사용자에 본인 계정 추가 (1분)
- Google Cloud 콘솔 → 해당 프로젝트 선택
- 좌측 메뉴 API 및 서비스 → OAuth 동의 화면 (최근 콘솔에서는 "Google 인증 플랫폼 → 대상(Audience)" 메뉴)
- 테스트 사용자(Test users) 섹션 → + ADD USERS → 업로드에 쓸 본인 Gmail 주소 입력 → 저장
이게 끝이다. 스크립트를 다시 실행하면 로그인이 진행된다.
3-1. 그다음 나오는 "확인되지 않은 앱" 경고는 정상이다
테스트 사용자로 등록해도 로그인 중간에 무서운 화면이 한 번 더 나온다:
Google에서 확인하지 않은 앱
이 앱은 Google의 확인 절차를 거치지 않았습니다...
이건 에러가 아니라 경고다. 내가 만든 앱에 내가 로그인하는 것이므로 그대로 통과하면 된다: 좌측 하단 고급 클릭 → (앱 이름)(안전하지 않음)으로 이동 → 권한 계속/허용.
4. 업로드 코드 핵심부
인증이 풀리면 나머지는 API 호출이다. 실제 운영 중인 upload_youtube.py에서 핵심부를 발췌했다.
# upload_youtube.py 핵심부
import json, os
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
SCOPES = ["https://www.googleapis.com/auth/youtube.upload",
"https://www.googleapis.com/auth/youtube.force-ssl"] # force-ssl: 자막 등록용
PRIVACY = os.getenv("YT_PRIVACY", "private") # 공개 상태 스위치 (아래 함정 1 참고)
# 인증: token.json 있으면 재사용/갱신, 없으면 최초 1회만 브라우저 로그인
creds = (Credentials.from_authorized_user_file("token.json", SCOPES)
if os.path.exists("token.json") else None)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request()) # 무인 갱신
else:
flow = InstalledAppFlow.from_client_secrets_file("client_secret.json", SCOPES)
creds = flow.run_local_server(port=0) # ← 여기서 403이 났던 것
open("token.json", "w", encoding="utf-8").write(creds.to_json())
yt = build("youtube", "v3", credentials=creds)
# 1) 영상 업로드 (진행률 찍히는 재개형 업로드)
meta = json.load(open("upload_meta.json", encoding="utf-8"))
body = {
"snippet": {"title": meta["title"][:100], # 제목 글자수 제한 선제 컷
"description": meta["description"][:4900],
"tags": meta.get("tags", []),
"categoryId": "27", # 교육
"defaultLanguage": "ko", "defaultAudioLanguage": "ko-KR"},
"status": {"privacyStatus": PRIVACY, "selfDeclaredMadeForKids": False},
}
media = MediaFileUpload("daily_short.mp4", chunksize=4*1024*1024,
resumable=True, mimetype="video/mp4")
req = yt.videos().insert(part="snippet,status", body=body, media_body=media)
resp = None
while resp is None:
status, resp = req.next_chunk()
if status:
print(f" … {int(status.progress()*100)}%")
vid = resp["id"]
print(f"✓ 영상 업로드 완료 → https://youtube.com/shorts/{vid}")
# 2) 자막 등록 — 실패해도 영상 업로드는 유지 (방어적 분리)
try:
yt.captions().insert(
part="snippet",
body={"snippet": {"videoId": vid, "language": "ko", "name": "한국어"}},
media_body=MediaFileUpload("subs.srt", mimetype="application/octet-stream"),
).execute()
print("✓ 자막(한국어) 등록 완료")
except Exception as e:
print(f"⚠ 자막 등록 실패({e}) — 스튜디오에서 수동 등록 가능")
설계 포인트 세 가지.
- 공개 상태를 환경변수로 스위치화 — 기본은
private. 심사(함정 1) 통과 전에는 어차피 공개가 잠기므로 private 고정으로 운영하고, 통과하는 날setx YT_PRIVACY public한 줄로 완전 자동 발행으로 전환되게 해뒀다. 코드 수정 없이 운영 정책만 바꾸는 구조다. - 자막 등록을 try/except로 분리 — 자막이 실패해도 영상 업로드는 살린다. 파이프라인의 마지막 단계일수록 "부분 실패 허용" 설계가 중요하다. 자막 하나 때문에 그날 영상 전체가 안 올라가는 건 손해가 더 크다.
- 제목/설명 길이 선제 컷 (
[:100],[:4900]) — 유튜브 API는 글자수 제한을 넘기면 업로드 요청 자체를 거부한다. 자동 생성 텍스트는 길이가 튈 수 있으므로 클라이언트에서 미리 자른다.
토큰은 token.json으로 저장돼 이후 실행은 무인이다(단, 아래 함정 2 참고).
5. 검증: 성공 로그
파이프라인 끝에 붙여 실행하면, 콘솔에 이 두 줄이 찍히는 것으로 끝난다.
✓ 영상 업로드 완료 → https://youtube.com/shorts/(영상ID)
✓ 자막(한국어) 등록 완료
유튜브 스튜디오에서 확인하면 제목·설명·태그·자막까지 스크립트가 넣은 값 그대로 들어가 있다. 이로써 "장 마감 → 렌더 → 업로드"가 자동으로 이어진다. (공개 전환만 심사 통과 전까지 수동인데, 그 이유가 아래 함정 1이다.)
6. 뒤에 숨은 함정 두 개 (403보다 이게 진짜다)
함정 1 — 미심사 프로젝트의 업로드는 '비공개 잠금'이 걸릴 수 있다. YouTube Data API 정책상, API 심사(audit)를 통과하지 않은 프로젝트가 videos.insert로 올린 영상은 공개(public)로 설정해도 비공개(private)로 잠길 수 있다. "코드는 성공했는데 영상이 공개가 안 된다"면 코드 버그가 아니라 이 정책이다. 해법은 둘 중 하나 — (a) API 업로드는 private로 하고 공개 전환은 스튜디오에서 수동으로, (b) YouTube API Services의 심사(audit) 신청을 통과시키기. 나는 일단 (a)로 운영하되, 코드에 공개 상태 스위치(YT_PRIVACY)를 심어둬서 심사를 통과하는 날 환경변수 한 줄로 (b)의 완전 자동 발행으로 넘어가게 해뒀다.
함정 2 — 테스트 모드의 리프레시 토큰은 7일이면 만료된다. 게시 상태가 "테스트"인 앱의 리프레시 토큰(재로그인 없이 인증을 자동 갱신해주는 토큰)은 발급 후 7일이 지나면 무효가 된다. 즉 일주일에 한 번은 브라우저 재인증을 하게 된다. 매일 도는 무인 파이프라인이라면 이게 은근히 성가시다. 벗어나려면 앱을 "프로덕션"으로 게시해야 하는데, youtube.upload 같은 민감 스코프는 심사가 얽히므로, 개인용 자동화라면 "주 1회 재인증을 운영 루틴에 넣는" 현실적 타협도 방법이다.
요약
| 단계 | 증상/작업 | 정체 |
|---|---|---|
| 첫 로그인 | 403 access_denied | 테스트 사용자 미등록 → 대상(Audience)에 본인 계정 추가 |
| 재로그인 | "확인되지 않은 앱" 경고 | 정상. 고급 → 계속으로 통과 |
| 업로드 | videos.insert + captions.insert | private 업로드 + YT_PRIVACY 스위치 운영 |
| 운영 함정 1 | 공개 설정이 비공개로 잠김 | 미심사 프로젝트 정책. 코드 버그 아님 |
| 운영 함정 2 | 일주일 뒤 인증 실패 | 테스트 모드 토큰 7일 만료. 재인증 루틴화 |
403의 교훈은 단순하다 — 에러 문구가 가리키는 곳(앱 심사)과 실제 원인(테스트 사용자 명단)이 다를 수 있다. 그리고 진짜 운영 리스크는 첫 에러가 아니라, 성공한 뒤에 조용히 걸리는 정책들(비공개 잠금, 토큰 만료)이다.
이 파이프라인이 만드는 실매매 기록 영상은 유튜브에서 → https://www.youtube.com/@quant-one-ai
📖 3년 기록을 전자책으로 정리 중입니다 — 목차·샘플 공개: (https://oneai.tistory.com/6)
One AI : AI 자동매매 시스템 만들기
이 채널은 AI와 규칙 기반 시스템으로 운영하는 퀀트매매, 자동매매의 실전 기록 채널입니다. 매매 신호, 실행, 결과, 복기 과정을 영상으로 정리합니다. 특정 종목 추천이 아닌 개인 매매 기록
www.youtube.com