AI 블로그 미디어 파이프라인 — Blogger API 제약, Google Drive 저장소, ORB 차단 우회
개요: 자동 발행되는 블로그 시스템
이 블로그는 AI가 콘텐츠를 만들고 크론이 자동 발행하는 파이프라인으로 운영됩니다. 구조는 크게 두 갈래입니다:
- 자체 블로그 (Vercel + Supabase) — Next.js 기반. 게시글, 검색, 시리즈, SEO, 조회수까지 자체 DB로 관리
- Google Blogger (Blogspot) — Google 검색 노출과 안정성 때문에 병행. API v3로 발행
발행 흐름은 단순합니다: 크론 → Python 스크립트 → Blogger API 발행 + Supabase 동시 INSERT → Vercel ISR 60초 내 반영. 한 번의 발행으로 구글 블로그와 자체 블로그 양쪽에 같은 글이 올라갑니다.
Blogger API의 미디어 제약: 이미지도, 영상도 "직접 올릴 수 없다"
자동 발행을 하면서 가장 먼저 부딪힌 벽이 바로 미디어 업로드였습니다.
이미지
Blogger API v3는 게시물 content에 HTML 문자열을 받을 뿐, 이미지 파일을 업로드하는 엔드포인트가 없습니다. 게시물 본문에 들어가는 이미지는 반드시 이미 존재하는 URL이어야 합니다. 사람이 Blogger 에디터에서 이미지를 넣으면 Blogger 서버가 대신 호스팅해 주지만, API로는 그 경로가 막혀 있습니다.
영상
영상은 더 막막합니다. Blogger에 영상 파일을 올리는 API는 없고, 지원되는 방식은 YouTube 임베드뿐입니다. 매일 생성되는 짧은 클립을 전부 유튜브에 올리는 건 과하고, 그렇다고 외부 저장소 없이는 영상을 글에 넣을 방법이 없습니다.
결론: 미디어는 전부 "외부 저장소에 올린 뒤 URL만 삽입"하는 방식이 유일한 자동화 경로입니다. 그럼 어떤 저장소를 쓸까?
왜 Google Drive인가
| 후보 | 용량 | 서빙 | 판정 |
|---|---|---|---|
| Google Drive | 무료 15GB | lh3 CDN 무료 inline 서빙 | ✅ 선택 |
| Supabase Storage | 무료 1GB | public 버킷 CDN | ⚠️ 소규모용 (영상이 쌓이면 부족) |
| Cloudinary 등 | 무료 티어 제한 | CDN | ⚠️ 외부 서비스 추가 |
- 용량: 무료 15GB — 1MB 안팎의 짧은 영상 기준 수천 개 수용. Supabase Storage(1GB)는 영상이 쌓이면 금방 찹니다
- 이미지 무료 서빙: 드라이브 파일의
lh3.googleusercontent.com/d/{파일ID}URL은 CDN에서inline으로 바로 서빙돼, 별도 비용·설정 없이 블로그 이미지로 쓸 수 있습니다 - Google 생태계 단일화: 이미 Blogger OAuth 토큰을 쓰고 있으니 같은 GCP 프로젝트에서 Drive 토큰까지 한 번에 관리됩니다
- 공유 제어: "링크가 있는 모든 사용자"로 공유하면 로그인 없이 접근 가능
영상의 벽: 드라이브 직접 재생은 안 된다
이미지는 순조로웠지만 영상은 드라이브 URL을 그대로 쓰면 재생이 안 됐습니다. 원인을 쫓아가 봤습니다.
시도 1: export=download → "attachment" 헤더의 벽
드라이브 파일의 직접 URL은 drive.usercontent.google.com/download?id={FILE_ID} 형태입니다. 그런데 이 URL은 응답에 이런 헤더를 달고 옵니다:
Content-Disposition: attachment; filename="video.mp4"
attachment는 브라우저에게 "다운로드로 처리하라"는 신호라서, <video> 태그는 이 응답을 재생하지 않습니다. 이미지가 잘 나온 건 lh3.googleusercontent.com CDN이 inline으로 서빙하기 때문이었죠.
시도 2: export=view → curl은 되는데 브라우저는 403
export=view 파라미터를 붙이니 응답이 바뀝니다:
HTTP/2 206
Content-Type: video/mp4
Content-Disposition: inline
Content-Range: bytes 0-1023/991974
inline + HTTP 206 Range 지원 — 스트리밍에 필요한 모든 조건이 충족됐습니다. curl과 ffprobe로도 정상 파싱이 됐어요. 그런데 실제 브라우저의 <video> 태그는 여전히 재생 불가.
Chrome 개발자 도구에서 진짜 원인을 발견했습니다:
net::ERR_BLOCKED_BY_ORB
브라우저의 <video> 요청에는 Sec-Fetch-Dest: video라는 헤더가 붙는데, 구글 드라이브는 이 요청에 HTTP 403을 반환합니다. curl(서버 요청)에는 200/206을 주면서, 브라우저(영상 재생 요청)에는 403을 주는 것 — 드라이브 UI 밖에서의 직접 스트리밍을 원천 차단하는 정책이었습니다. Chrome은 이를 ORB(Opaque Response Blocking)로 차단합니다.
iframe 임베딩(drive.google.com/file/d/{ID}/preview)도 시도했지만, 파일 공유가 되어 있어도 비로그인 상태에선 로그인 페이지로 리다이렉트됩니다.
해결: Vercel 서버리스 프록시
핵심 통찰은 간단합니다. 403은 브라우저 요청에만 걸린다는 것. 그렇다면 서버(Node.js)에서 드라이브를 fetch한 뒤, 브라우저에게는 우리 도메인에서 오는 영상처럼 보이게 재서빙하면 모든 제약이 사라집니다.
Next.js App Router 기준, 단 50줄입니다:
// src/app/api/drive/route.ts
export async function GET(request: NextRequest) {
const id = request.nextUrl.searchParams.get('id')
const range = request.headers.get('range')
const upstream = await fetch(
`https://drive.usercontent.google.com/download?id=${id}&export=view`,
{ headers: { 'User-Agent': 'Mozilla/5.0', ...(range ? { Range: range } : {}) } }
)
const headers = new Headers()
headers.set('Content-Type', 'video/mp4')
headers.set('Content-Disposition', 'inline') // ★ attachment → inline
headers.set('Accept-Ranges', 'bytes') // ★ Range 지원 유지
headers.set('Cache-Control', 'public, max-age=86400')
if (upstream.headers.get('content-range'))
headers.set('Content-Range', upstream.headers.get('content-range')!)
return new Response(upstream.body, { status: upstream.status, headers })
}
포인트는 세 가지입니다:
- 서버에서 fetch → 드라이브의 브라우저 차단(403)을 우회
- same-origin으로 재서빙 → 브라우저는 자기 도메인의 mp4로 인식 (ORB 불필요)
- Range 헤더 그대로 전달 → 206 부분 응답, 시킹/스트리밍 정상
왜 Vercel 서버리스인가
- 서버 요청이라 403을 안 맞는다: 드라이브의 브라우저 차단은 브라우저 요청에만 적용됩니다. 서버(Node.js)에서 fetch하면 curl처럼 정상 응답. 이 우회는 "서버에 코드가 있어야" 성립하므로, 정적 호스팅이나 클라이언트 전용 구조로는 불가능합니다
- same-origin = 모든 브라우저 제약 회피: 브라우저는 영상을 "자기 도메인"에서 받는 것으로 인식합니다. ORB도 CORS도 개입할 여지가 없습니다
- 무료 티어로 충분: Vercel Hobby 플랜 서버리스 함수로 충분히 동작합니다. 함수 응답 한도(4.5MB)가 있지만, 블로그에 쓰는 짧은 클립(1MB 안팎)은 문제없습니다
- 배포 = git push: 프록시 라우트를 커밋하면 Vercel이 자동 배포합니다. 별도 서버 운영이 없습니다
사용법과 데모
이제 사용법은 단순합니다. 마크다운에 이렇게만 쓰면 됩니다:
<video controls width="100%">
<source src="https://devsnack-blog.vercel.app/api/drive?id={파일ID}" type="video/mp4"/>
</video>
아래 영상은 구글 드라이브에 있는 파일을 이 프록시로 재생하는 실제 데모입니다 (LTX-2.5로 생성한 5초 클립):
정리: 미디어 처리 표준
| 미디어 | 저장소 | 블로그에서 쓰는 URL |
|---|---|---|
| 이미지 | Google Drive | lh3.googleusercontent.com/d/{파일ID} |
| 영상 | Google Drive | devsnack-blog.vercel.app/api/drive?id={파일ID} (프록시) |
| 대용량 영상 | Supabase Storage | ...supabase.co/storage/v1/object/public/media/... |
주의할 점:
- 드라이브 파일은 "링크가 있는 모든 사용자"로 공유되어야 합니다 (anyoneWithLink / reader)
- Vercel Hobby 플랜 함수 응답 한도(4.5MB)가 있으므로, 영상은 4MB 미만이 안전합니다
"curl로 되는데 왜 브라우저는 안 되지?" — 이번 사례는 서버 요청과 브라우저 요청의 응답이 다를 수 있다는 점을 다시 한번 상기시켜 줬습니다. 디버깅은 항상 실제 브라우저에서 확인하는 습관이 중요합니다.