매일 반복되는 영상 업로드, 코드 한 번으로 자동화해 보세요.
들어가며
저는 매일 저녁 ETF 등락률 TOP 7을 유튜브 쇼츠로 올리려고 생각중입니다.
처음에는 영상을 만들고, 유튜브 스튜디오에 직접 들어가서 업로드하고, 예약 시간을 설정하는 과정을 매일 반복했습니다.
하루 이틀은 괜찮았지만, 며칠이 지나자 이 단순 반복 작업이 꽤 번거롭게 느껴졌습니다.
"어차피 코랩에서 영상을 만드는데, 업로드까지 자동으로 안 될까?"
찾아보니 YouTube Data API v3를 사용하면 가능했습니다.
이 글에서는 Google Colab 환경에서 영상을 제작하고, 저녁 9시 예약 공개까지 자동화한 전체 과정을 공유합니다.
이번 코딩은 클로드 AI의 도움을 받았습니다.
전체 흐름
셀 1~2 (환경 설정)
↓
셀 3 (날짜 + 종목 데이터 입력)
↓
셀 4 (영상 자동 제작 — moviepy)
↓
셀 5 (유튜브 자동 업로드 + 9시 예약)
오늘 글에서는 셀 5 (유튜브 자동 업로드) 부분을 중심으로 설명합니다.
1단계 — Google Cloud Console 설정
YouTube Data API를 사용하려면 Google Cloud에서 프로젝트를 만들고 API를 활성화해야 합니다.
1-1. 프로젝트 생성
👉 https://console.cloud.google.com 접속
상단 프로젝트 선택 → 새 프로젝트 클릭
프로젝트 이름: etf-youtube-upload 입력 → 만들기
1-2. YouTube Data API v3 활성화
API 및 서비스 → 라이브러리 → YouTube Data API v3 검색 → 사용 설정
⚠️ 이 단계를 빠뜨리면 나중에 업로드 시 아래 에러가 납니다.
ResumableUploadError: YouTube Data API v3 has not been used in project ... before or it is disabled.에러 메시지 안에 있는 링크를 클릭하면 바로 활성화 페이지로 이동할 수 있습니다.
1-3. OAuth 동의 화면 설정
API 및 서비스 → OAuth 동의 화면
- 유형: 외부 선택 → 만들기
- 앱 이름: ETF Shorts Uploader
- 사용자 지원 이메일: 본인 이메일 선택
- 저장 후 계속 (나머지 단계도 동일)
- 테스트 사용자 탭 → + ADD USERS → 본인 구글 계정 추가
⚠️ 테스트 사용자 등록을 빠뜨리면 로그인 시 아래 에러가 납니다.
액세스 차단됨: ETF Shorts Uploader은(는) Google 인증 절차를 완료하지 않았습니다. 403 오류: access_denied
1-4. OAuth 클라이언트 ID 발급
API 및 서비스 → 사용자 인증 정보 → + 사용자 인증 정보 만들기 → OAuth 클라이언트 ID
- 애플리케이션 유형: 데스크톱 앱 선택
- 이름: etf-uploader → 만들기
생성 후 목록에서 ⬇️ JSON 다운로드 클릭
다운로드된 파일명을 client_secrets.json으로 변경 후
구글 드라이브 /유튜브쇼츠/ 폴더에 업로드합니다.
2단계 — 셀 5 코드 작성
# =============================================
# 셀 5. 📤 유튜브 업로드 + 9시 예약 + 알림
# =============================================
!pip install -q google-auth google-auth-oauthlib google-api-python-client
import os, pickle, smtplib
from datetime import datetime, timezone, timedelta
from email.mime.text import MIMEText
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
# ── 기본 설정 ──────────────────────────────────
CLIENT_SECRETS = "/content/drive/MyDrive/유튜브쇼츠/client_secrets.json"
TOKEN_PATH = "/content/drive/MyDrive/유튜브쇼츠/youtube_token.pickle"
SCOPES = ["https://www.googleapis.com/auth/youtube.upload"]
VIDEO_PATH = f"{PROJECT_ROOT}/유튜브쇼츠/etf_shorts_{DATE.replace(' ', '')}.mp4"
# ── ✏️ 알림 설정 ───────────────────────────────
GMAIL_SENDER = "발신gmail주소@gmail.com"
GMAIL_PASSWORD = "앱비밀번호16자리"
GMAIL_RECEIVER = "수신주소@gmail.com"
KAKAO_TOKEN = "카카오토큰입력"
USE_EMAIL = False
USE_KAKAO = False
# ── 영상 정보 ──────────────────────────────────
VIDEO_TITLE = f"📊 {DATE} ETF 상승·하락률 TOP 7 | 레버리지·인버스 제외"
VIDEO_DESCRIPTION = f"""{DATE} ETF 등락률 TOP 7 요약
📈 상승률 TOP 7
{chr(10).join([f"{i+1}. {n} {r}" for i, (n, r) in enumerate(gainers_data)])}
📉 하락률 TOP 7
{chr(10).join([f"{i+1}. {n} {r}" for i, (n, r) in enumerate(losers_data)])}
※ 레버리지/인버스 종목 제외
#ETF #주식 #쇼츠 #ETF상승 #ETF하락
"""
VIDEO_TAGS = ["ETF", "주식", "쇼츠", "ETF상승률", "ETF하락률", "재테크"]
VIDEO_CATEGORY = "25"
# ── 저녁 9시 예약 시간 계산 (KST) ──────────────
KST = timezone(timedelta(hours=9))
now_kst = datetime.now(KST)
publish = now_kst.replace(hour=21, minute=0, second=0, microsecond=0)
if now_kst >= publish:
publish += timedelta(days=1)
publish_at = publish.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
print(f"⏰ 예약 공개 시간: {publish.strftime('%Y-%m-%d %H:%M')} KST")
# ── 인증 ───────────────────────────────────────
def get_authenticated_service():
creds = None
if os.path.exists(TOKEN_PATH):
with open(TOKEN_PATH, 'rb') as f:
creds = pickle.load(f)
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
print("🔄 토큰 갱신 완료")
elif not creds or not creds.valid:
flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRETS, SCOPES)
flow.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
auth_url, _ = flow.authorization_url(prompt='consent', access_type='offline')
print("=" * 60)
print("🔐 아래 URL을 브라우저에서 열어 로그인해 주세요:\n")
print(auth_url)
print("=" * 60)
auth_code = input("인증 코드: ").strip()
flow.fetch_token(code=auth_code)
creds = flow.credentials
print("✅ 인증 완료!")
with open(TOKEN_PATH, 'wb') as f:
pickle.dump(creds, f)
return build("youtube", "v3", credentials=creds)
# ── 업로드 ─────────────────────────────────────
def upload_video(youtube):
print(f"📤 업로드 시작: {VIDEO_PATH}")
body = {
"snippet": {
"title": VIDEO_TITLE,
"description": VIDEO_DESCRIPTION,
"tags": VIDEO_TAGS,
"categoryId": VIDEO_CATEGORY,
},
"status": {
"privacyStatus": "private",
"publishAt": publish_at,
"selfDeclaredMadeForKids": False,
}
}
media = MediaFileUpload(VIDEO_PATH, mimetype="video/mp4", resumable=True)
request = youtube.videos().insert(part="snippet,status", body=body, media_body=media)
response = None
while response is None:
status, response = request.next_chunk()
if status:
print(f" 업로드 중... {int(status.progress() * 100)}%", end='\r')
video_id = response['id']
video_url = f"https://www.youtube.com/shorts/{video_id}"
print(f"\n✅ 업로드 완료!")
print(f" 📎 영상 ID : {video_id}")
print(f" 🔗 URL : {video_url}")
print(f" ⏰ 공개 예약 : {publish.strftime('%Y-%m-%d %H:%M')} KST")
return video_id, video_url
# ── 이메일 알림 ────────────────────────────────
def send_email(video_id, video_url):
try:
msg = MIMEText(f"✅ {DATE} ETF 쇼츠 업로드 완료!\n\n🔗 {video_url}\n⏰ 공개 예약: {publish.strftime('%Y-%m-%d %H:%M')} KST")
msg['Subject'] = f"[ETF 쇼츠] {DATE} 업로드 완료"
msg['From'] = GMAIL_SENDER
msg['To'] = GMAIL_RECEIVER
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
smtp.login(GMAIL_SENDER, GMAIL_PASSWORD)
smtp.send_message(msg)
print("📧 이메일 알림 전송 완료!")
except Exception as e:
print(f"⚠️ 이메일 전송 실패: {e}")
# ── 카카오톡 알림 ──────────────────────────────
def send_kakao(video_id, video_url):
try:
import urllib.request, urllib.parse
msg = f"✅ {DATE} ETF 쇼츠 업로드 완료!\n🔗 {video_url}\n⏰ 공개 예약: {publish.strftime('%Y-%m-%d %H:%M')} KST"
req = urllib.request.Request(
"https://kapi.kakao.com/v2/api/talk/memo/default/send",
data=urllib.parse.urlencode({
"template_object": f'{{"object_type":"text","text":"{msg}","link":{{"web_url":"{video_url}"}}}}'
}).encode(),
headers={"Authorization": f"Bearer {KAKAO_TOKEN}", "Content-Type": "application/x-www-form-urlencoded"}
)
urllib.request.urlopen(req)
print("💬 카카오톡 알림 전송 완료!")
except Exception as e:
print(f"⚠️ 카카오톡 전송 실패: {e}")
# ── 실행 ───────────────────────────────────────
if not os.path.exists(VIDEO_PATH):
print(f"❌ 영상 파일 없음 — 셀 4를 먼저 실행해 주세요")
elif not os.path.exists(CLIENT_SECRETS):
print(f"❌ client_secrets.json 없음")
else:
youtube = get_authenticated_service()
video_id, video_url = upload_video(youtube)
if USE_EMAIL:
send_email(video_id, video_url)
if USE_KAKAO:
send_kakao(video_id, video_url)
3단계 — 코랩 환경의 인증 특이사항
일반적인 Python 환경에서는 flow.run_local_server()를 사용하면 브라우저가 자동으로 열립니다.
하지만 Google Colab은 서버 환경이라 브라우저를 직접 열 수 없어 아래 에러가 납니다.
Error: could not locate runnable browser
이 문제는 redirect_uri를 urn:ietf:wg:oauth:2.0:oob로 설정하면 해결됩니다.
이 방식은 브라우저 없이 URL → 코드 복사 → 붙여넣기로 인증을 처리합니다.
flow.redirect_uri = 'urn:ietf:wg:oauth:2.0:oob'
auth_url, _ = flow.authorization_url(prompt='consent', access_type='offline')
인증은 최초 1회만 필요합니다.
이후에는 youtube_token.pickle 파일이 자동으로 저장되어 재인증 없이 바로 업로드됩니다.
4단계 — 저녁 9시 예약 공개 설정
유튜브 API에서 예약 공개는 publishAt 필드로 설정합니다.
주의할 점은 시간대를 반드시 UTC로 변환해서 전달해야 한다는 것입니다.
KST = timezone(timedelta(hours=9))
now_kst = datetime.now(KST)
publish = now_kst.replace(hour=21, minute=0, second=0, microsecond=0)
# 이미 9시가 지났으면 다음날로 자동 설정
if now_kst >= publish:
publish += timedelta(days=1)
# UTC로 변환
publish_at = publish.astimezone(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
그리고 예약 공개를 사용하려면 반드시 privacyStatus를 "private"으로 설정해야 합니다.
"status": {
"privacyStatus": "private", # ← 반드시 private
"publishAt": publish_at, # ← 9시에 자동 공개
}
자주 발생하는 에러 정리
에러 원인 해결 방법
| could not locate runnable browser | 코랩에서 브라우저 실행 불가 | redirect_uri = 'urn:ietf:wg:oauth:2.0:oob' 설정 |
| access_denied 403 | 테스트 사용자 미등록 | OAuth 동의 화면 → 테스트 사용자에 본인 계정 추가 |
| accessNotConfigured | YouTube API 미활성화 | Google Cloud에서 YouTube Data API v3 사용 설정 |
| redirect_uri multiple values | fetch_token에 중복 전달 | fetch_token(code=auth_code) — redirect_uri 제거 |
| client_secrets.json 없음 | 파일 경로 불일치 | 코랩에서 find 명령으로 실제 경로 확인 |
마무리
처음에는 단순히 "업로드 자동화"를 상상만 했는데, 생각보다 많은 시행착오가 있었습니다.
특히 코랩 환경에서의 OAuth 인증 방식과 예약 공개 시 UTC 변환은 공식 문서에도 잘 나와 있지 않아서 꽤 시간이 걸렸습니다.
이 글이 비슷한 자동화를 고민하는 분들께 도움이 되길 바랍니다.
코딩 초보자 이지만 다음 글에서는 썸네일 자동 생성과 업로드 완료 후 카카오톡 알림 기능도 다룰 예정입니다.
'인공지능 (AI) 따라잡기' 카테고리의 다른 글
| Playwright 입문 1일차 – 브라우저 자동화로 클릭까지 해보기 (초보자 완벽 가이드) (0) | 2026.04.01 |
|---|---|
| 파이선으로 유투브 쇼츠 영상 제작하기 (0) | 2026.03.28 |
| 삼성전자·SK하이닉스, 급여 비교 — 5년치 공시 데이터로 본 연봉 추이 (0) | 2026.03.19 |
| 삼성물산, '홈 AI 로봇' 실증 착수… 시니어 돌봄의 미래인가? (5) | 2025.09.04 |
| 시니어 개발자들이 AI 코딩에 더 열광하는 진짜 이유? (1) | 2025.09.03 |