sociallinks.dev

블로그 / 개발자 가이드

핸들로 YouTube 채널 ID를 얻는 방법(그 반대도)

크리에이터는 YouTube 채널을 @handle로 공유하지만, YouTube Data API는 거의 모든 것을 채널 ID로 다룹니다. 핸들만 저장하면 이름이 바뀔 때 기록이 깨지고, ID만 저장하면 사용자가 알아보는 이름을 보여 줄 수 없습니다. 이 가이드는 API 호출 한 번으로 핸들을 채널 ID로 변환하는 방법, ID를 50개씩 핸들로 되돌리는 방법, 예전 방식의 /user/와 /c/ URL을 처리하는 방법, 그리고 기본 할당량 안에서 운영하는 방법을 알려 드립니다.

핸들, 채널 ID, 예전 방식의 이름

YouTube 채널에는 최대 네 가지 식별자로 접근할 수 있습니다. 그중 안정성이 보장되는 것은 하나뿐입니다.

식별자예시URL 형식참고
채널 IDUC_x5XG1OV2P6uZZ5FSM9Ttwyoutube.com/channel/UC...YouTube가 채널을 식별하는 ID. 대소문자를 구분합니다. 기본 키로 쓰세요.
핸들@GoogleDevelopersyoutube.com/@GoogleDevelopers고유하고 대소문자를 구분하지 않으며, 소유자가 YouTube Studio에서 바꿀 수 있습니다.
예전 사용자 이름GoogleDevelopersyoutube.com/user/GoogleDevelopers예전 사용자 이름 체계에서 온 것. API의 forUsername 필터로 변환할 수 있습니다.
예전 맞춤 URLGoogleDevelopersyoutube.com/c/GoogleDevelopers예전의 개인 맞춤 URL로, YouTube는 계속 작동한다고 밝힙니다. 이를 변환하는 API 필터는 없습니다.

채널 ID를 키로 저장하고, 핸들은 주기적으로 갱신하는 표시용 값으로 두세요. 이 가이드의 나머지는 사용자가 무엇을 주든 그 ID에 도달하는 방법에 관한 것입니다.

API 키 설정과 할당량 이해

  1. Step 1Google Cloud 프로젝트 만들기

    Google Cloud 콘솔에서 프로젝트를 만들거나 기존 프로젝트를 고르세요.

  2. Step 2YouTube Data API v3 사용 설정

    API 및 서비스를 열고 라이브러리에서 YouTube Data API v3를 찾아 프로젝트에서 사용 설정하세요.

  3. Step 3API 키 만들기

    사용자 인증 정보에서 API 키를 만드세요. 핸들이나 ID로 channels.list를 조회하는 것처럼 공개 채널 데이터에는 OAuth 없이 키만 있으면 됩니다. 키는 YouTube Data API로만 제한하세요.

  4. Step 4키를 코드에 넣지 않기

    YOUTUBE_API_KEY 같은 환경 변수에 넣고 실행 시점에 읽으세요. 아래 예시가 바로 그렇게 합니다.

  5. Step 5할당량 확인

    콘솔에서 해당 API의 할당량 페이지에 일일 할당량과 사용량이 나옵니다.

Google 문서에서 중요한 숫자는 다음과 같습니다.

따라서 기본 할당량으로 하루에 핸들을 약 10,000개 조회할 수 있고, ID 조회는 그 50배까지 가능합니다. 한 번의 호출로 같은 1단위에 채널 50개를 ID로 가져올 수 있기 때문입니다.

forHandle로 핸들을 채널 ID로

channels.list는 필터를 정확히 하나만 받습니다. forHandle 필터는 YouTube 핸들을 받으며, 참조 문서에 따르면 값 앞에 @ 기호를 붙여도 되므로 GoogleDevelopers와 @GoogleDevelopers 둘 다 작동합니다. ID만 필요하다면 part=id를 요청하세요. 응답은 더 작고 비용은 같습니다.

cURL

curl -s "https://www.googleapis.com/youtube/v3/channels?part=id&forHandle=@GoogleDevelopers&key=$YOUTUBE_API_KEY"

아래 Node 예시는 요청을 listChannels라는 도우미 함수 하나로 감싸며, 이 가이드의 나머지 부분에서 재사용합니다. 일치하는 것이 없으면 빈 목록을 반환하고, 잘못된 키나 할당량 소진 같은 실제 오류에는 예외를 던집니다.

Node.js

const API = "https://www.googleapis.com/youtube/v3/channels";
async function listChannels(params) {
  const key = process.env.YOUTUBE_API_KEY;
  if (!key) throw new Error("Set YOUTUBE_API_KEY");
  const url = new URL(API);
  url.search = new URLSearchParams({ ...params, key }).toString();
  const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
  const data = await res.json();
  if (!res.ok) {
    const reason = data.error?.errors?.[0]?.reason;
    if (reason === "channelNotFound") return [];
    throw new Error(`YouTube API ${res.status} ${reason ?? ""}: ${data.error?.message}`);
  }
  return data.items ?? []; // no match: items is empty or missing
}

/** "@GoogleDevelopers" or "GoogleDevelopers" -> "UC_x5XG1OV2P6uZZ5FSM9Ttw" or null */
export async function channelIdFromHandle(handle) {
  const items = await listChannels({ part: "id", forHandle: handle.trim() });
  return items[0]?.id ?? null;
}

/** Legacy username from a youtube.com/user/<name> URL -> channel ID or null */
export async function channelIdFromUsername(username) {
  const items = await listChannels({ part: "id", forUsername: username.trim() });
  return items[0]?.id ?? null;
}

/** Any number of channel IDs -> Map(id -> { title, customUrl }) */
export async function channelsById(ids) {
  const unique = [...new Set(ids)];
  const found = new Map();
  for (let i = 0; i < unique.length; i += 50) {
    const batch = unique.slice(i, i + 50);
    const items = await listChannels({ part: "snippet", id: batch.join(","), maxResults: "50" });
    for (const ch of items) {
      found.set(ch.id, { title: ch.snippet.title, customUrl: ch.snippet.customUrl ?? null });
    }
  }
  return found; // IDs missing from the map were not found
}

Python

import os

import requests

API = "https://www.googleapis.com/youtube/v3/channels"


def list_channels(**params):
    key = os.environ.get("YOUTUBE_API_KEY")
    if not key:
        raise RuntimeError("Set YOUTUBE_API_KEY")
    response = requests.get(API, params={**params, "key": key}, timeout=10)
    data = response.json()
    if not response.ok:
        errors = data.get("error", {}).get("errors") or [{}]
        reason = errors[0].get("reason")
        if reason == "channelNotFound":
            return []
        raise RuntimeError(f"YouTube API {response.status_code} {reason}: {data.get('error', {}).get('message')}")
    return data.get("items", [])  # no match: items is empty or missing


def channel_id_from_handle(handle):
    """'@GoogleDevelopers' or 'GoogleDevelopers' -> 'UC_x5XG1OV2P6uZZ5FSM9Ttw' or None"""
    items = list_channels(part="id", forHandle=handle.strip())
    return items[0]["id"] if items else None


def channel_id_from_username(username):
    """Legacy username from a youtube.com/user/<name> URL -> channel ID or None"""
    items = list_channels(part="id", forUsername=username.strip())
    return items[0]["id"] if items else None


def channels_by_id(ids):
    """Any number of channel IDs -> {id: {"title", "customUrl"}}; missing IDs were not found"""
    unique = list(dict.fromkeys(ids))
    found = {}
    for i in range(0, len(unique), 50):
        batch = unique[i:i + 50]
        for ch in list_channels(part="snippet", id=",".join(batch), maxResults=50):
            found[ch["id"]] = {
                "title": ch["snippet"]["title"],
                "customUrl": ch["snippet"].get("customUrl"),
            }
    return found

핸들은 URL의 퍼센트 인코딩을 디코딩한 뒤 사용자가 쓴 그대로 넘기세요. 핸들에는 ASCII뿐 아니라 여러 문자 체계의 글자를 쓸 수 있으며, URLSearchParams와 requests 모두 파라미터를 알아서 인코딩합니다.

예전 사용자 이름과 /c/ 맞춤 URL

크리에이터 바이오와 스프레드시트의 오래된 링크는 핸들 이전 형식 중 하나를 쓰는 경우가 많습니다.

채널 ID를 핸들로, 50개씩

반대 방향은 쉼표로 구분한 채널 ID 목록을 받는 id 필터를 씁니다. part=snippet을 요청하고 snippet.customUrl을 읽으세요. 위의 channelsById는 ID 중복을 제거하고, 50개씩 나누어 보내고, ID를 키로 하는 맵을 반환합니다. 맵에 없는 ID는 찾을 수 없거나, 삭제되었거나, API에서 보이지 않는 것입니다.

찾을 수 없음, 오류, 캐싱

아무것도 일치하지 않는 조회는 오류가 아닙니다. forHandle과 forUsername의 응답에는 단순히 항목이 없으므로, items 배열이 비어 있거나 없는지 확인하세요. 문서에 나온 channels.list 오류는 다음과 같습니다.

오류 사유HTTP발생 시점처리 방법
200 응답에 items가 없음200핸들이나 사용자 이름이 존재하지 않음null을 반환하고, 없음 결과를 짧게 캐시
channelNotFound404id 파라미터의 채널을 찾을 수 없음위 도우미 함수처럼 찾을 수 없음으로 처리
invalidCriteria400요청에 필터가 둘 이상 있음forHandle, forUsername, id 중 정확히 하나만 보내기
channelForbidden403채널이 요청을 지원하지 않거나 제대로 승인되지 않음로그를 남기고 해당 채널 건너뛰기
quotaExceeded403프로젝트가 일일 할당량을 모두 사용함중단하고 태평양 시간 자정 이후에 재시도하거나 할당량 증가 요청
badRequest(API 키가 유효하지 않음)400키가 없거나 잘못됨설정을 고치고 재시도하지 않기

할당량과 지연 시간을 아끼려면 결과를 캐시하되, YouTube의 데이터 저장 규칙을 지키세요. YouTube API Services Developer Policies(섹션 III.E.4)는 승인되지 않은 API 데이터를 30일 이내로만 저장할 수 있으며, 그 후에는 삭제하거나 갱신해야 한다고 말합니다. 소문자 핸들을 키로 하고 TTL을 30일보다 충분히 짧게 둔 캐시로 대부분의 이점을 얻을 수 있습니다.

Node.js 캐시

import { channelIdFromHandle } from "./youtube.mjs";

const TTL_MS = 7 * 24 * 60 * 60 * 1000; // refresh well inside YouTube's 30-day limit
const cache = new Map(); // lowercase handle -> { id, at }

export async function cachedChannelId(handle) {
  const key = handle.trim().replace(/^@/, "").toLowerCase();
  const hit = cache.get(key);
  if (hit && Date.now() - hit.at < TTL_MS) return hit.id;
  const id = await channelIdFromHandle(handle);
  cache.set(key, { id, at: Date.now() }); // caches misses (null) too
  return id;
}

운영 환경에서는 같은 로직을 Redis나 expires_at 열이 있는 데이터베이스 테이블에 두고, 새로 만든 핸들을 빨리 반영할 수 있도록 없음 결과의 TTL을 일치 결과보다 짧게 하세요.

API 키 없는 대안: 채널 페이지 읽기

공개 채널 페이지의 HTML에는 채널 ID가 들어 있습니다. 현재 https://www.youtube.com/@GoogleDevelopers를 요청하면 <link rel="canonical">이 https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw를 가리키는 페이지가 돌아오며, /c/와 /user/ 페이지도 마찬가지입니다. 그래서 이것이 추측 없이 /c/ 이름을 변환할 수 있는 유일한 방법입니다.

Node.js

// Unofficial fallback: reads the public channel page. Fragile, prefer the API.
export async function channelIdFromPage(handle) {
  const url = `https://www.youtube.com/@${encodeURIComponent(handle.replace(/^@/, ""))}`;
  const res = await fetch(url, {
    headers: { "Accept-Language": "en-US" },
    signal: AbortSignal.timeout(10_000),
  });
  if (!res.ok) return null;
  const html = await res.text();
  const match = html.match(/<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/(UC[A-Za-z0-9_-]{22})"/);
  return match?.[1] ?? null;
}

Python

import re
from urllib.parse import quote

import requests

CANONICAL = re.compile(r'<link rel="canonical" href="https://www\.youtube\.com/channel/(UC[A-Za-z0-9_-]{22})"')


def channel_id_from_page(handle):
    """Unofficial fallback: reads the public channel page. Fragile, prefer the API."""
    url = "https://www.youtube.com/@" + quote(handle.lstrip("@"), safe="._-")
    response = requests.get(url, headers={"Accept-Language": "en-US"}, timeout=10)
    if not response.ok:
        return None
    match = CANONICAL.search(response.text)
    return match.group(1) if match else None

API로 해결할 수 없는 경우에만 드물게 쓰세요. 비공식적이고 깨지기 쉽습니다.

방법 비교

방법입력비용장점단점
forHandle을 쓴 channels.list핸들(@ 유무 무관)핸들당 1단위공식, 정확한 일치, 모든 핸들에 작동호출당 핸들 하나
forUsername을 쓴 channels.list예전 /user/ 이름이름당 1단위공식, 정확한 일치예전 사용자 이름에만 해당
id를 쓴 channels.list채널 ID 최대 50개ID 50개당 1단위공식, 일괄 처리, 제목과 customUrl 반환customUrl이 없을 수 있음
search.list자유 텍스트기본적으로 하루 100회의 별도 한도이름으로 채널 찾기정확하지 않은 순위 추측, 적은 허용량
채널 페이지 canonical 링크/c/를 포함한 모든 채널 URLAPI 할당량 없음키 없이 작동, /c/ 이름 변환 가능비공식, 깨지기 쉬움, 차단될 수 있음, 약관과 충돌할 수 있음

대부분의 제품에 맞는 구성은 단순합니다. forHandle로 핸들을 변환하고, 채널 ID를 저장하고, id 필터로 핸들을 50개씩 갱신하고, 드문 /c/ 링크를 위해 페이지 대안은 플래그 뒤에 두세요. 크리에이터의 다른 플랫폼 계정도 필요하다면 sociallinks.dev는 Instagram, TikTok, YouTube, X 프로필 URL 하나를 받아 다른 플랫폼의 크리에이터 계정을 일치마다 신뢰도 점수와 근거와 함께 반환하는 API입니다.

자주 묻는 질문

forHandle에 핸들을 넘길 때 @가 필요한가요?

아닙니다. channels.list 참조 문서는 forHandle 값 앞에 @ 기호를 붙여도 된다고 하며, GoogleDevelopers와 @GoogleDevelopers를 모두 예시로 듭니다.

핸들 조회 한 번에 할당량이 얼마나 드나요?

1단위입니다. 모든 channels.list 호출은 1단위이며, 기본 할당량은 하루 10,000단위이고 태평양 시간 자정에 초기화됩니다. 잘못된 요청에도 최소 1단위가 듭니다.

한 번의 호출로 여러 핸들을 조회할 수 있나요?

아닙니다. forHandle은 핸들 하나만 받습니다. 쉼표로 구분한 목록을 받는 것은 id 필터뿐이므로, 반대 방향(ID에서 핸들)은 일괄 처리하고 정방향은 캐시하세요.

채널 ID가 바뀌기도 하나요?

채널 ID는 YouTube가 채널을 고유하게 식별하는 데 쓰는 식별자입니다. 핸들이나 제목을 바꿔도 바뀌지 않습니다. 그래서 기본 키로 적합합니다. 핸들, 제목, 맞춤 URL은 모두 바뀔 수 있습니다.

snippet.customUrl이 왜 비어 있나요?

YouTube의 변경 기록에도 나와 있듯이 모든 채널에 맞춤 URL이 있는 것은 아닙니다. 이 필드는 선택 사항으로 다루고, 없으면 /channel/UC... URL을 보여 주세요.

결과를 얼마나 오래 캐시할 수 있나요?

YouTube Developer Policies는 저장한 API 데이터를 30일 이내에 삭제하거나 갱신하도록 제한합니다. 일치 결과는 1주일 이하, 없음 결과는 그보다 짧은 TTL을 두면 그 범위 안에서 여유 있게 운영할 수 있습니다.

크리에이터 계정 찾기