블로그 / 개발자 가이드
핸들로 YouTube 채널 ID를 얻는 방법(그 반대도)
크리에이터는 YouTube 채널을 @handle로 공유하지만, YouTube Data API는 거의 모든 것을 채널 ID로 다룹니다. 핸들만 저장하면 이름이 바뀔 때 기록이 깨지고, ID만 저장하면 사용자가 알아보는 이름을 보여 줄 수 없습니다. 이 가이드는 API 호출 한 번으로 핸들을 채널 ID로 변환하는 방법, ID를 50개씩 핸들로 되돌리는 방법, 예전 방식의 /user/와 /c/ URL을 처리하는 방법, 그리고 기본 할당량 안에서 운영하는 방법을 알려 드립니다.
핸들, 채널 ID, 예전 방식의 이름
YouTube 채널에는 최대 네 가지 식별자로 접근할 수 있습니다. 그중 안정성이 보장되는 것은 하나뿐입니다.
| 식별자 | 예시 | URL 형식 | 참고 |
|---|---|---|---|
| 채널 ID | UC_x5XG1OV2P6uZZ5FSM9Ttw | youtube.com/channel/UC... | YouTube가 채널을 식별하는 ID. 대소문자를 구분합니다. 기본 키로 쓰세요. |
| 핸들 | @GoogleDevelopers | youtube.com/@GoogleDevelopers | 고유하고 대소문자를 구분하지 않으며, 소유자가 YouTube Studio에서 바꿀 수 있습니다. |
| 예전 사용자 이름 | GoogleDevelopers | youtube.com/user/GoogleDevelopers | 예전 사용자 이름 체계에서 온 것. API의 forUsername 필터로 변환할 수 있습니다. |
| 예전 맞춤 URL | GoogleDevelopers | youtube.com/c/GoogleDevelopers | 예전의 개인 맞춤 URL로, YouTube는 계속 작동한다고 밝힙니다. 이를 변환하는 API 필터는 없습니다. |
채널 ID를 키로 저장하고, 핸들은 주기적으로 갱신하는 표시용 값으로 두세요. 이 가이드의 나머지는 사용자가 무엇을 주든 그 ID에 도달하는 방법에 관한 것입니다.
API 키 설정과 할당량 이해
- Step 1Google Cloud 프로젝트 만들기
Google Cloud 콘솔에서 프로젝트를 만들거나 기존 프로젝트를 고르세요.
- Step 2YouTube Data API v3 사용 설정
API 및 서비스를 열고 라이브러리에서 YouTube Data API v3를 찾아 프로젝트에서 사용 설정하세요.
- Step 3API 키 만들기
사용자 인증 정보에서 API 키를 만드세요. 핸들이나 ID로
channels.list를 조회하는 것처럼 공개 채널 데이터에는 OAuth 없이 키만 있으면 됩니다. 키는 YouTube Data API로만 제한하세요. - Step 4키를 코드에 넣지 않기
YOUTUBE_API_KEY같은 환경 변수에 넣고 실행 시점에 읽으세요. 아래 예시가 바로 그렇게 합니다. - Step 5할당량 확인
콘솔에서 해당 API의 할당량 페이지에 일일 할당량과 사용량이 나옵니다.
Google 문서에서 중요한 숫자는 다음과 같습니다.
channels.list호출은 어떤 필터를 쓰든 1단위입니다.- 프로젝트는
search.list와videos.insert를 제외한 모든 엔드포인트에 대해 기본적으로 하루 10,000단위를 받습니다. 이 두 엔드포인트는 이제 각각 하루 100회라는 별도 한도가 있습니다. - 모든 요청은 잘못된 요청을 포함해 최소 1단위가 듭니다.
- 일일 할당량은 태평양 시간 자정에 초기화됩니다.
따라서 기본 할당량으로 하루에 핸들을 약 10,000개 조회할 수 있고, ID 조회는 그 50배까지 가능합니다. 한 번의 호출로 같은 1단위에 채널 50개를 ID로 가져올 수 있기 때문입니다.
forHandle로 핸들을 채널 ID로
channels.list는 필터를 정확히 하나만 받습니다. forHandle 필터는 YouTube 핸들을 받으며, 참조 문서에 따르면 값 앞에 @ 기호를 붙여도 되므로 GoogleDevelopers와 @GoogleDevelopers 둘 다 작동합니다. ID만 필요하다면 part=id를 요청하세요. 응답은 더 작고 비용은 같습니다.
curl -s "https://www.googleapis.com/youtube/v3/channels?part=id&forHandle=@GoogleDevelopers&key=$YOUTUBE_API_KEY"아래 Node 예시는 요청을 listChannels라는 도우미 함수 하나로 감싸며, 이 가이드의 나머지 부분에서 재사용합니다. 일치하는 것이 없으면 빈 목록을 반환하고, 잘못된 키나 할당량 소진 같은 실제 오류에는 예외를 던집니다.
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
}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
크리에이터 바이오와 스프레드시트의 오래된 링크는 핸들 이전 형식 중 하나를 쓰는 경우가 많습니다.
- `youtube.com/user/<name>`: 위의
channelIdFromUsername으로 이름을forUsername에 넘기세요. 다른channels.list호출처럼 1단위가 듭니다. - `youtube.com/c/<name>`: 맞춤 URL을 위한
channels.list필터는 없습니다. 많은 채널이 예전 맞춤 이름과 같은 핸들을 골랐기 때문에 같은 문자열로forHandle을 시도하면 될 수도 있지만, 일치한다고 같은 채널이라는 증거는 아닙니다. 의존하기 전에 제목을 확인하거나 페이지의 canonical 링크를 읽는 방법(아래 설명)처럼 다른 방식으로 확인하세요. - `search.list`는 정확한 조회가 아니라 검색어에 대한 순위 결과를 반환하며, 기본 한도는 하루 100회입니다. 식별자 변환에는 쓰지 마세요.
채널 ID를 핸들로, 50개씩
반대 방향은 쉼표로 구분한 채널 ID 목록을 받는 id 필터를 씁니다. part=snippet을 요청하고 snippet.customUrl을 읽으세요. 위의 channelsById는 ID 중복을 제거하고, 50개씩 나누어 보내고, ID를 키로 하는 맵을 반환합니다. 맵에 없는 ID는 찾을 수 없거나, 삭제되었거나, API에서 보이지 않는 것입니다.
- 일괄 처리 크기. 참조 문서는
id를 쉼표로 구분한 목록으로 설명하고maxResults를 최대 50으로 제한합니다. ID 개수에 대한 별도 한도는 밝히지 않으므로, 코드는 호출당 50개를 보내고 기본값 5 때문에 결과가 잘리지 않도록maxResults=50을 설정합니다. - 순서. 보낸 순서대로 결과가 돌아온다고 가정하지 마세요. 응답을
id로 색인하세요. - `customUrl`은 선택 사항입니다. 참조 문서는 이를 채널의 맞춤 URL이라고만 설명하며, 2016년 변경 기록에는 모든 채널에 있는 것은 아니라고 적혀 있습니다. null일 수 있는 값으로 다루고, 앞에
@가 있으면 제거하고, 이 값을 기반으로 개발하기 전에 알고 있는 채널 몇 개로 형식을 확인하세요. - 제목은 핸들이 아닙니다.
snippet.title은 표시 이름입니다. 고유하지 않으며 공백과 이모지가 들어갈 수 있습니다.
찾을 수 없음, 오류, 캐싱
아무것도 일치하지 않는 조회는 오류가 아닙니다. forHandle과 forUsername의 응답에는 단순히 항목이 없으므로, items 배열이 비어 있거나 없는지 확인하세요. 문서에 나온 channels.list 오류는 다음과 같습니다.
| 오류 사유 | HTTP | 발생 시점 | 처리 방법 |
|---|---|---|---|
200 응답에 items가 없음 | 200 | 핸들이나 사용자 이름이 존재하지 않음 | null을 반환하고, 없음 결과를 짧게 캐시 |
channelNotFound | 404 | id 파라미터의 채널을 찾을 수 없음 | 위 도우미 함수처럼 찾을 수 없음으로 처리 |
invalidCriteria | 400 | 요청에 필터가 둘 이상 있음 | forHandle, forUsername, id 중 정확히 하나만 보내기 |
channelForbidden | 403 | 채널이 요청을 지원하지 않거나 제대로 승인되지 않음 | 로그를 남기고 해당 채널 건너뛰기 |
quotaExceeded | 403 | 프로젝트가 일일 할당량을 모두 사용함 | 중단하고 태평양 시간 자정 이후에 재시도하거나 할당량 증가 요청 |
badRequest(API 키가 유효하지 않음) | 400 | 키가 없거나 잘못됨 | 설정을 고치고 재시도하지 않기 |
할당량과 지연 시간을 아끼려면 결과를 캐시하되, YouTube의 데이터 저장 규칙을 지키세요. YouTube API Services Developer Policies(섹션 III.E.4)는 승인되지 않은 API 데이터를 30일 이내로만 저장할 수 있으며, 그 후에는 삭제하거나 갱신해야 한다고 말합니다. 소문자 핸들을 키로 하고 TTL을 30일보다 충분히 짧게 둔 캐시로 대부분의 이점을 얻을 수 있습니다.
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/ 이름을 변환할 수 있는 유일한 방법입니다.
// 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;
}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 NoneAPI로 해결할 수 없는 경우에만 드물게 쓰세요. 비공식적이고 깨지기 쉽습니다.
- 마크업은 문서화된 인터페이스가 아니며 언제든 바뀔 수 있습니다.
- YouTube는 자동화된 요청, 특히 클라우드 IP 대역의 요청에 동의 페이지, 속도 제한, CAPTCHA를 보여 줄 수 있습니다. 저희 테스트에서는 클라우드 서버에서
youtu.be링크를 따라가자 Google CAPTCHA 페이지로 끝났습니다. - 사이트에 자동으로 접근하는 것은 YouTube 서비스 약관과 충돌할 수 있습니다. 약관을 읽고 공식 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/를 포함한 모든 채널 URL | API 할당량 없음 | 키 없이 작동, /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을 두면 그 범위 안에서 여유 있게 운영할 수 있습니다.