flowoodz.
← blog
10분 읽기글 flowoodz

유튜브 API 할당량 초과: search.list를 아끼고 하루 10,000유닛으로 트렌드 서비스를 굴린 방법

유튜브 API 할당량 초과를 피하려면 100유닛짜리 search.list를 아끼고 1유닛 배치 조회와 DB 풀로 바꾸면 됩니다. Trending Finder를 하루 10,000유닛 안에서 운영하며 정한 원칙을 정리했습니다.

목차
  1. search.list는 한 번에 100 유닛
  2. 트렌딩 차트는 사실상 공짜다
  3. API를 치는 시점과 사용자가 보는 시점을 분리
  4. 사용자에게 API 키를 받지 않기로 한 결정
  5. 하루 10,000유닛에서 지킨 원칙

Trending Finder를 만들면서 가장 먼저 부딪힌 벽은 알고리즘도 UI도 아니고 쿼터였습니다. YouTube Data API는 기본적으로 하루 10,000 유닛을 줍니다. 넉넉해 보이지만, 검색 한 번에 100씩 깎인다는 걸 알고 나면 이야기가 달라집니다. 이 글은 그 10,000 안에서 트렌드 수집 서비스를 굴리기 위해 했던 선택들의 기록입니다. TrendScore 자체가 궁금하면 이전 글이 먼저입니다.

search.list는 한 번에 100 유닛#

YouTube Data API 할당량 비용 비교. 하루 1만 유닛 기준으로 search.list는 100유닛이라 100회면 소진되고, videos.list와 channels.list는 1유닛으로 최대 50개를 한 번에 조회할 수 있습니다

YouTube Data API의 쿼터는 호출 횟수가 아니라 메서드별 비용으로 계산됩니다. 공식 문서 기준으로:

메서드 비용 하는 일
search.list 100 키워드 검색
videos.list 1 영상 상세 조회 (최대 50개 한 번에)
channels.list 1 채널 정보 조회 (최대 50개 한 번에)

같은 "영상 목록 얻기"인데 비용이 100배 차이 납니다. 그래서 설계 원칙이 하나로 정리됐습니다. 검색은 최대한 아끼고, ID를 알면 무조건 배치 조회.

Trending Finder의 수집(discover)은 KR·US 두 리전을 돕니다. 키워드 시드 하나를 두 리전에서 검색하면:

시드 1개 비용
= 2 리전 × (search.list 100 + videos.list 1 + channels.list 1)
≈ 204 유닛
 
10,000 / 204 ≈ 하루 49번의 시드-리전 검색이 한계

시드 열 개를 등록한 사용자 다섯 명이 각자 discover를 돌리면 그날 쿼터는 끝입니다. 이 계산이 이후 모든 결정의 출발점이었습니다.

트렌딩 차트는 사실상 공짜다#

다행히 비싼 건 검색뿐입니다. videos.list에 chart=mostPopular를 주면 리전별 인기 영상 50개가 1 유닛에 옵니다. 카테고리 필터를 걸어도 마찬가지입니다.

그래서 수집의 뼈대는 트렌딩 차트 순회로 잡았습니다. 리전 2개 × (전체 1 + 카테고리 15) = videos.list 32번, 32 유닛. 검색 시드 하나의 6분의 1 비용으로 수천 개 영상 풀의 기반이 만들어집니다.

검색을 쓸 때도 search.list의 응답은 snippet뿐이라 조회수·좋아요·영상 길이가 없습니다. 어차피 videos.list를 한 번 더 쳐야 하는데, 이때 ID를 50개씩 묶어 보냅니다. 채널 구독자 수도 같은 방식입니다.

// videos.list, channels.list 모두 50개 단위 배치
async function getVideosByIds(ids) {
  const out = [];
  for (let i = 0; i < ids.length; i += 50) {
    const d = await get('videos', {
      part: 'snippet,statistics,contentDetails,status',
      id: ids.slice(i, i + 50).join(','),
    });
    // ... 채널 정보도 dedupe 후 50개씩 조회
  }
  return out;
}

영상 500개의 통계를 갱신해도 videos.list 10번, 10 유닛입니다. ID만 확보되면 그 뒤는 거의 공짜라는 감각이 생기면, "검색으로 ID를 얻는 순간"만 아끼면 된다는 결론이 나옵니다.

API를 치는 시점과 사용자가 보는 시점을 분리#

가장 큰 절약은 코드가 아니라 구조에서 나왔습니다. 사용자가 목록을 새로고침할 때마다 API를 치면 어떤 최적화도 소용없습니다. 그래서:

  • discover가 수집한 영상은 전부 Postgres에 upsert: 이 DB 풀이 곧 캐시입니다
  • 사용자 화면의 정렬·필터·TrendScore 계산은 전부 DB 데이터로만 수행
  • 그 위에 사용자별 인메모리 캐시 60초 TTL을 한 겹 더. 새로고침 연타에도 DB 조회조차 안 나감
  • discover 직후에만 캐시 무효화

사용자가 하루 종일 화면을 들여다봐도 YouTube API 호출은 0입니다. API가 불리는 건 discover가 돌 때뿐이고, discover는 서버가 통제합니다. 30일 지난 영상은 풀에서 purge해서 갱신 대상 자체를 줄입니다.

여기에 가드를 몇 개 더 얹었습니다.

  • 풀 상한(기본 3,000개): 풀이 가득 차면 비싼 시드 검색을 아예 건너뜁니다. 트렌딩 차트(32 유닛)만 돕니다
  • 시드 dedupe: cron으로 도는 discover는 전 사용자의 시드를 모아 중복 제거 후 검색합니다. 열 명이 "AI 자동화"를 등록해도 검색은 한 번
  • 실패 격리: 카테고리 하나가 쿼터 초과로 실패해도 에러만 기록하고 다음 호출을 계속합니다. 어느 리전의 어느 카테고리가 건너뛰어졌는지 결과에 남겨서, 쿼터가 언제쯤 바닥나는지 추적할 수 있게 했습니다. 한 번의 에러가 그날 수집 전체를 날리지 않는 게 우선입니다

채널 메타데이터도 같은 원칙입니다. 화면에서 채널 뷰를 만들 때 개설일 같은 정보가 비어 있는 채널만 골라 channels.list를 칩니다. 이미 받아둔 채널을 매번 다시 조회할 이유가 없습니다.

사용자에게 API 키를 받지 않기로 한 결정#

비슷한 도구들이 흔히 쓰는 방식이 있습니다. "당신의 YouTube API 키를 입력하세요." 서비스 입장에선 쿼터 문제가 통째로 사라지니 유혹적입니다. 저도 고민했고, 안 하기로 했습니다.

  • Google Cloud 콘솔에서 프로젝트 만들고 API 활성화하고 키를 발급받는 과정 자체가 타깃 사용자(크리에이터)에게는 진입 장벽입니다. 그 관문에서 대부분 이탈합니다
  • 남의 키를 서버에 보관하는 순간 유출 리스크와 관리 책임이 생깁니다
  • 무엇보다, 위의 구조가 되면 키를 나눠 받을 이유가 없습니다. 수집은 공유 풀 한 번이면 되니까요

그래서 로그인은 Google OAuth로만 받고, YouTube API 키는 서버에 하나만 둡니다. 사용자 수가 늘어도 API 비용은 사용자 수가 아니라 시드 종류 수에 비례합니다. 이게 이 구조의 핵심입니다.

한 가지 예외는 뒀습니다. 사용자가 화면에서 직접 키워드를 검색하는 원샷 discover인데, 이건 클릭 한 번에 200 유닛짜리 행동이라 로그인 사용자에게만 열고 결과를 즉시 공유 풀에 넣습니다. 한 명의 검색이 모두의 데이터가 되는 쪽으로.

하루 10,000유닛에서 지킨 원칙#

  • 쿼터는 호출 수가 아니라 메서드 비용: search.list 100 vs videos.list 1, 설계 전에 이 표부터 봐야 합니다
  • 검색은 ID 발견용으로만: 상세·통계·채널 정보는 전부 50개 배치 조회로
  • 트렌딩 차트는 1 유닛: 검색 없이 풀의 뼈대를 만들 수 있는 가장 싼 재료
  • API 시점과 조회 시점 분리: DB 풀 + 짧은 TTL 캐시면 사용자 트래픽과 쿼터가 무관해집니다
  • 키를 나눠 받는 대신 검색을 공유: 시드 dedupe가 되는 순간 사용자별 키가 필요 없어집니다

10,000 유닛은 부족한 숫자가 아니었습니다. search.list를 아무 데서나 부르지만 않는다면.