유튜브에서 제공하는 API를 이용하면 내가 만든 웹페이지에서 유튜브와 관련된 다양한 기능을 추가할 수 있다. '영상 검색하기'를 기준으로 유튜브 API 기본 사용법을 익혀보자.
1. API 키 발급부터
KEY 없이는 API를 쓸 수 없다!
API를 사용하기 위해서는 먼저 key를 발급받아야 하고, key를 발급받기 위해선 구글 계정이 필요하다. 인증 없이는 대부분의 API를 사용할 수 없어서 구글 계정이 반드시 필요하다. 구글 계정이 준비되었다면 구글 개발자 콘솔(Google Dev Console) 로 접속하고 하단 왼쪽 그림과 같이 새 프로젝트를 생성한다. 만들기 버튼을 누르면 잠시의 로딩 끝에 하단 오른쪽 그림과 같이 프로젝트 생성이 완료된다. 'API 및 서비스 사용 설정'을 클릭하자.
'API 및 서비스 사용설정'을 클릭하면, 하단 왼쪽 그림과 같이 활성화(enable)할 수 있는 모든 API를 볼 수 있다. API 중에는 완전 무료인 API도 있고 유료인 API도 있다고 한다. 'youtube'라고 검색해서 유튜브 API 를 찾아보자. 휠을 조금만 내려도 바로 보인다. 'Youtube Data API v3'를 선택하고 '사용(Enable)' 버튼을 누른 후 잠시의 로딩을 기다린다.
조금 기다리다 보면 하단 왼쪽 그림과 같이 화면이 표시된다. '사용자 인증 정보 만들기' 를 클릭하면, 마지막 단계인 사용자 인증 정보를 입력하는 단계로 넘어간다. 사용할 API는 'Youtube Data API v3', 호출할 위치는 '웹브라우저(자바스크립트)', 액세스 할 데이터는 '공개 데이터'로 선택하고 하단 중앙 그림을 참고해서 빠르게 눌러보자.
키가 발급되었다! API key는 공개하면 안 되기 때문에 가려서 이미지를 첨부했다. 키값을 .gitignore에 추가하는 등의 방식으로 공개 저장소에 노출되지 않도록 잘 관리해야 한다. key값만을 수집해서 악의적으로 사용하는 봇도 있다고 하니 푸시 전에 한 번씩 확인해보면 좋겠다.
API 키... 잘 받은걸까?
(...이 과정은 생략해도 된다...) 발급받은 API 키로 요청을 보내면 응답이 잘 돌아오는지 Postman으로 빠르게 확인해보자. Postman은 HTTP 요청을 쉽게 보낼 수 있도록 도와주는 프로그램이다. 브라우저 주소창에 직접 쳐서 요청을 보내도 되지만, 깔끔한 GUI를 지원해서 다들 사용하는 것 같다...! postman.com 에서 쉽게 다운로드할 수 있다. 위에서 발급받았던 키를 이용해서 간단한 'GET' 요청을 보내보자. 메서드는 이미 기본값인 'GET'으로 선택되어있으니 요청 URL만 추가해보자. 입력창에 아래의 URL에 본인의 API 키를 넣어 입력하고 Send 버튼을 누르기만 하면 된다. 그러면 'Status 200 OK'라는 상태 코드와 함께 성공적으로 데이터를 받아올 수 있다.
https://www.googleapis.com/youtube/v3/search?part=snippet&q=kpop+music&key={본인의_API_KEY}
요청을 무한정 보낼 수는 없다!
YouTube API는 무료이기 때문에 결제정보(Payment Info)를 제출하지 않아도 잘 작동한다. 다만 사용에 요청을 얼마나 보낼 수 있는지 제한이 있다. 일정 포인트를 할당해주고, 요청을 보낼 때마다 포인트를 차감하는 방식이다. 이 할당량을 Quotas로 표현하는데 그냥 쉽게 포인트라고 얘기하자. 소요 포인트의 양은 어떤 종류의 요청을 보내는지에 따라 다르다. 예를 들어, 영상 목록을 조회할 때는 100 포인트가 소요되는데, 요청에 따라 여러 페이지를 조회하게 된다면 조회한 페이지 수만큼 각각 포인트가 소요된다. 댓글을 추가하는 요청은 50 포인트가 소요된다. 가장 많은 포인트가 소요되는 동영상 추가 요청은 1,600 포인트가 소요된다. (요청별 포인트 소요 더보기)
유튜브 API에서 무료로 사용 가능한 포인트 한도가 얼마인지 확인해보자. IAM 및 관리자 메뉴 - 할당량(Quotas) 에서 일일 요청한도가 10,000 포인트, 1분당 요청한도 1,600 포인트로 설정되어있는 것을 확인할 수 있다. 유튜브 API를 통해 하루에 영상을 100페이지 초과해서 비디오를 받아오거나, 1분 동안 동영상을 1개 초과해서 동영상을 추가할 수 없다는 이야기이다.
위 이미지를 보면 빨간색 표시로 한도 항목 중 하나를 초과한 것을 확인할 수 있다. 개발 시 라이브서버를 켜놓은 상태로 App.js가 호출될 때마다 요청을 보낸다면, 생각보다 금방 한도를 초과하게 된다. 요청한도를 초과한 후 요청을 보내면 아래와 같은 상태코드 403의 응답을 받게 된다.
{
"error": {
"code": 403,
"message": "The request cannot be completed because you have exceeded your <a href=\"/youtube/v3/getting-started#quota\">quota</a>.",
"errors": [
{
"message": "The request cannot be completed because you have exceeded your <a href=\"/youtube/v3/getting-started#quota\">quota</a>.",
"domain": "youtube.quota",
"reason": "quotaExceeded"
}
]
}
}
할당량 관리 페이지에서 원래 '할당량 수정'은 가능하지만 이미 무료 계정에서 사용 가능한 최대한도로 설정되어 있기 때문에 한도를 더 늘릴 수는 없는 상황이다. API KEY를 삭제하고 재발급받더라도 요청한도가 갱신되지는 않는다. 해결방법은 결제정보를 등록해서 한도를 상향 조정하거나, 일일 사용량이 갱신될 때까지 기다리는 것이다. 일일 할당량은 보통 태평양 표준시(PT) 자정에 재설정된다. 한국에서의 오후 5시와 같다. (서머타임 적용 시 오후 4시) 실시간 요청 결과가 필요한 게 아니라면 데이터를 최초 한번 받아와서 JSON파일을 더미데이터로서 저장해두고 중복 요청을 줄이는 방법으로 포인트를 아낄 수 있다.
2. netlify로 10분 만에 API 키 숨기기
프론트에는 숨길 수 없다
앞서 이야기했듯 API 키는 보안을 잘 유지하며 관리해야 한다. 하지만 [왼쪽 그림] 과 같이 클라이언트 측에 KEY를 보관하고, 클라이언트가 유튜브 서버로 바로 요청을 보내는 방식으로는 KEY를 숨길 수 없다. 요청URL에 '&key=MY_API_KEY'와 같은 형태로 내 API 키가 반드시 들어가기 때문에 공개될 수밖에 없다. 그러므로 KEY를 숨기기 위해서는 클라이언트가 아닌 곳에 KEY를 보관해야 한다.
대안은 서버를 하나 두는 것이다. [오른쪽 그림] 을 보면, API 키를 모르는 클라이언트가 추가된 서버에 요청을 보내고 있다. 이어서 API 키를 알고 있는 나의 서버가 유튜브 API서버에 요청을 보낸다. 이 방식으로 내 클라이언트의 요청을 받아줄 서버를 중간에 두면 KEY를 숨길 수 있다.
5분 만에 netlify 설정하기
@bigsaigon333 에게 전수받은 10분 해결법을 적용해보자. 서버리스 함수(Serverless Function)로 배포를 도와주는 netlify를 이용할 것이다. 서버리스는 서버가 없다는 뜻이 아니라 개발자가 서버 관리로부터 완전히 자유롭다는 뜻이다. 그만큼 쉽고 간편하다.
1. 우선, 깃허브에서 'serverless-function'을 위한 repository를 새로 생성한다.
(서버리스 함수가 다 만들어져 있는 이 레포를 fork 하면 시간을 더 아낄 수 있다)
2. netlify에 github로 가입하고 첫 번째 그림의 'New site from Git' 을 클릭한다.
3. 두 번째 그림에서 'Github' 를 클릭해서 연결한다.
4. 1번에서 미리 생성해둔 repository를 선택한다.
(나중에 다른 repository를 추가하고 싶은 경우, 하단 [왼쪽 그림] 맨 밑 부분 'Configure the Netlify app on Github'를 클릭한다.)
5. API키를 환경변수로 추가한다.
6. HOST를 환경변수로 추가한다. 프론트엔드 repository의 주소도 환경변수로 관리한다면 localhost에서 개발할 때도 환경변수만 수정하면 된다. (ex. Github Pages의 경우 https://365kim.github.io)
+ (중요) 환경변수 수정 후에는 수동으로 deploy를 다시 해주어야 한다!
5분 만에 Serverless Function 만들기
✨서버리스 함수 예시 : @bigsaigon333/ hide-api-key (마음껏 fork 해가셔도 된대요!! 📢)
+ 아래의 소스코드 설명은 제가 직접 작성한 함수를 기반으로 하여 위 repository의 serverless-function과 상이할 수 있습니다.
1. 필요한 모듈을 설치한다. (urlencode 대신 querystring 사용 가능)
// 터미널
npm i node-fetch urlencode
2. netlify.toml에 netlify configuration을 설정한다. (공식문서)
// ./netlify.toml
[build]
functions = "./functions"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/myFunction"
3. functions/ 디렉토리에 필요한 함수를 만든다.
// ./functions/myFunction.js
const fetch = require('node-fetch');
const urlencode = require('urlencode');
const YOUTUBE_SEARCH_ENDPOINT = 'https://www.googleapis.com/youtube/v3/search';
exports.handler = async (event) => {
try {
const { queryStringParameters } = event;
const parameters = Object.entries(queryStringParameters)
.map(([key, value]) => `${key}=${urlencode.encode(value)}`) // *** 인코딩 문제해결
.join('&')
.concat(`&key=${process.env.API_KEY}`);
const URI = `${YOUTUBE_SEARCH_ENDPOINT}?${parameters}`;
const response = await fetch(URI);
const { statusCode, statusText, ok, headers } = response;
const body = JSON.stringify(await response.json());
headers['Access-Control-Allow-Origin'] = process.env.HOST; // *** 동일출처정책 문제해결
return {
statusCode,
statusText,
ok,
headers,
body,
};
} catch (error) {
return {
statusCode: 404,
statusText: error.message,
ok: false,
headers: {
'Access-Control-Allow-Origin': '*',
},
};
}
};
3. git add, commit, push 한다. push 하면 netlify에서 자동으로 deploy 된다.
4. 프론트엔드에서 API키를 지운다.
5. 프론트엔드의 Endpoint를 유튜브API서버에서 서버리스 함수 쪽으로 변경해준다.
// ./src/js/contants.js
export const API_SEARCH_ENDPOINT =
'https://suspicious-tesla-29e2bd.netlify.app/.netlify/functions/myFunction/search';
+ 인코딩 문제 해결
서버리스 함수를 도입하고 나서 한글로 검색어를 입력하면 에러가 발생했다. 원인은 서버리스 함수에서 사용하는 node-fetch가 브라우저에서 사용한 fetch와 다르게 알아서 인코딩을 안 해주었기 때문인 듯하다. 결론만 말하면 node-fecth 보내기 직전에 인코딩을 살짝 해주면 문제없이 잘 작동한다. urlencode은 위의 코드블럭에 보이는 것과 같이 직접 인코딩을 해야 하고, querystring 모듈을 이용하면 따로 인코딩을 해주지 않아도 된다.
- 중간서버 없을 때
1. 사용자 Input |
2. 브라우저 fetch() 요청 - 인코딩 O |
3. 유튜브 API서버 |
하루 | %ED%95%98%EB%A3%A8 | %ED%95%98%EB%A3%A8 (인식 잘 됨) |
- 서버리스 함수에서 node-fetch를 사용할 때
1. 사용자 Input |
2. 브라우저 fetch() 요청 - 인코딩 O |
3. 서버리스 함수 도착 - 디코딩 O |
4. node-fetch fetch() 요청 - 인코딩 X |
5. 유튜브 API서버 |
하루 | %ED%95%98%EB%A3%A8 | 하루 | 하루 | 하루 (인식 안됨) |
+ CORS 문제 해결
웹에서 리소스를 공유할 때, 원칙적으로는 보안상의 이유로 오직 출처가 동일한 리소스만 공유할 수 있다. (SOP: Same-Origin Policy, 동일 출처 정책.) 출처가 동일하다는 것은 스킴(https://), 호스트(365kim.github.io), 포트(없음)가 동일해야 한다는 말이다. 그런데 웹에서는 출처가 다른 리소스와의 상호작용이 필요하기 마련이다. 유튜브API서버의 리소스를 클라이언트에게 전달하는 지금의 상황도 이 경우에 해당된다.
Access to fetch at 'https://suspicious-tesla-29e2bd.netlify.app/.netlify/functions/search?&part=snippet&q=%EB%93%9C%EB%A6%BC%EC%BD%94%EB%94%A9&type=video&maxResults=10' from origin 'https://365kim.github.io' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
유튜브 API 서버의 리소스를 클라이언트에게 그대로 보내면 콘솔창에 위와 같은 에러메세지가 출력된다. 에러메세지 중간쯤 CORS policy를 언급하며 'Access-Control-Allow-Origin 헤더'가 없다고 친절하게 알려주고 있다. CORS policy란 Cross-Origin Resource Sharing의 약자로, 출처가 다른 리소스도 공유할 수 있도록 하는 정책을 말한다. 클라이언트의 요청헤더의 'Origin'과 서버의 응답헤더 'Access-Control-Allow-Origin'을 내용을 비교해본 후 유효한 응답인지 아닌지 결정하는 메커니즘으로 작동한다. 최초에 요청을 보낸 요청URL(위 코드에서 process.env.HOST)를 응답헤더에 명시해줌으로써 다른 출처 간에도 리소스를 공유할 수 있도록 명시할 수 있다. 와일드카드(*)를 쓰면 브라우저의 origin에 상관없이 모든 리소스에 접근 가능하도록 한다는 뜻이 된다.
3. 유튜브 영상 검색 API 요청 ✨
어떤 요청 보낼지 정하기
앞서 보냈던 요청 URL의 구조를 다시 살펴보자. search 뒤에 물음표(?)는 쿼리스트링의 시작을 표시하기로 약속한 문자이다. 쿼리스트링(Query String)은 어떤 리소스를 요청할지 더 구체화해서 요청을 보낼 수 있도록 파라미터를 전달하는 방법을 약속한 것이라고 할 수 있다. "key=value" 형태로 이루어져 있고, 여러 개일 경우 &로 연결한다. 여기서 &뒤에 가장 먼저 나오는 'part'는 요청을 보낼 때 필수로 지정해야 하는 파라미터이다.
https://www.googleapis.com/youtube/v3/search?part=snippet&q=kpop+music&key={본인의_API_KEY}
https://www.googleapis.com/youtube/v3/search?{쿼리스트링}
이제 유튜브 API에서 어떤 파라미터(key)를 선택할 수 있는지만 알면, '원하는 대로' 데이터를 구체화해서 요청할 수 있다. 예를 들어, "kpop music으로 검색했을 때 나오는 최신 영상 10개를 보내줘", "javascript로 검색했을 때 나오는 영상 중에 2021년도에 미국에서 만든 영상만 3개를 보내줘"와 같은 요청이 가능하다.
다음은 어떤 옵션을 줄 수 있는지를 보여주는 KEY=VALUE 세트 목록이다. 자주 사용할 것 같은 키값에는 💡 이모지를 추가했다.
KEY | VALUE |
channelId | '채널 아이디'를 지정해서 특정 채널에서만 영상을 검색한다. |
channelType | 'any'는 모든 채널을, 'show'는 유튜브 프로그램만 영상을 검색한다. |
eventType | 'completed'는 완료된 방송만, 'live'는 진행중인 방송만, 'upcoming'은 예정된 방송만 검색한다. |
maxResults 💡 | 필요한 갯수만큼 검색한다. ('0' 부터 '50'까지 가능함. 기본값은 '5') |
onBehalfOfContentOwner | '사용자 인증정보'로 유튜브파트너 컨텐츠 소유자임을 인증하고 검색한다. |
order 💡 | 'date'는 최신순, 'rating'은 평가순, 'relevance'는 관련도순, 'title'은 제목사전순, 'videoCount'는 업로드영상 많은순, 'viewCount'는 조회수순으로 정렬하여 검색한다. (기본값은 relevance) |
pageToken | 결과값으로 받았던 'nextPageToken'는 다음 maxResult만큼을, 'prevPageToken'는 이전 maxResult만큼을 검색한다. |
publishedAfter | '1970-01-01T00:00:00Z'와 같이 지정된 시간 후에 만든 영상만 검색한다. |
publishedBefore | '1970-01-01T00:00:00Z'와 같이 지정된 시간 전에 만든 영상만 검색한다. |
q 💡 | 해당 키워드로 검색한다. |
regionCode💡 | '지역코드'를 지정해서 특정 지역에서만 영상을 검색한다. (한국 'KR', 미국 'US', 영국 'GB') |
safeSearch | 'none'은 모든 영상을, 'moderate'는 위치 등의 기준으로 일부 컨텐츠를 제외하고, 'strict'는 제한된 컨텐츠를 모두 제외하고 검색한다. |
topicId | 'Freebase 주제 아이디'를 지정해서 특정 주제에서만 영상을 검색한다. |
type 💡 | 'video'는 영상을, 'channel'은 채널을, 'playlist'는 재생목록을 검색한다. (여러 개면 쉼표로 구분함. 기본값은 'video,channel,playlist') |
videoCaption | 'any'는 모든 영상을, 'closedCaption'은 자막이 있는 영상만, 'none'은 자막이 없는 영상만 검색한다. |
videoCategoryId | '카테고리 아이디'를 지정해서 특정 카테고리에서만 영상을 검색한다. |
videoDefinition | 'any'는 모든 영상을, 'standard'는 표준 해상도만, 'high'는 HD 영상만 검색한다. |
videoDimension | 'any'는 모든 영상을, '2d'는 2D 영상만, '3d'는 3D 영상만 검색한다. (기본값은 'any') |
videoDuration | 'any'는 모든 영상을, 'short'는 4분 미만, 'medium'은 4분~20분, 'long'은 20분 이상 길이의 영상을 검색한다. (기본값은 'any') |
videoEmbeddable 💡 | 'any'는 모든 영상을, 'true'는 다른 웹페이지로 퍼갈 수 있는 영상만 검색한다. |
videoLicense | 'any'는 모든 영상을, 'creativeCommon'은 다른 영상에서 재사용해도 되는 Creative Commons 라이센스 영상만, 'youtube'는 표준유튜브 라이센스 영상만 검색한다. |
videoSyndicated | 'any'는 모든 영상을, 'true'는 배급되어서 외부에서 재생할 수 있는 영상만 검색한다. |
videoType | 'any'는 모든 영상을, 'episode'는 유튜브 프로그램의 에피소드만, 'movie'는 영화만 검색한다. |
요청 보내고 응답 받기
앞서 보낸 요청은 아래와 같은 구조의 응답으로 돌아온다. 요청했던 구체적인 검색 결과는 items에 배열 형태로 담겨온다. 위의 key, value를 다시 한 번 확인해서 필요한 데이터를 파싱 해서 사용하면 된다.
// 요청 보내기
const request = async (url, method) => {
try {
const response = await fetch(url, { method });
const json = await response.json();
if (!response.ok) {
throw Error(response.statusText);
}
return json; // *** 응답결과
} catch (e) {
console.error(e);
}
};
// 검색결과 요청 시, 응답으로 돌려받는 json 구조
{
"kind": "youtube#searchListResponse",
"etag": etag,
"nextPageToken": string,
"prevPageToken": string,
"pageInfo": {
"totalResults": integer,
"resultsPerPage": integer
},
"items": [
search Resource
]
}
+ 번외 Github REST API
1. Github에서 사용자 프로필 가져오기 (관련 문서)
https://api.github.com/users/{username}
// 예시: 나의 프로필정보 가져오기
https://api.github.com/users/365kim
2. PR 정보 리스트 가져오기 (관련 문서)
https://api.github.com/repos/{owner}/{repo}/pulls
// 예시: 자동차 경주 게임 미션의 PR 정보 리스트 가져오기
https://api.github.com/repos/woowacourse/javascript-racingcar/pulls
3. organization 정보 가져오기 (관련 문서)
https://api.github.com/orgs/{org}
// 예시: woowacourse organization에 대한 정보 가져오기
https://api.github.com/orgs/woowacourse
참고자료
Client-Side에서 Youtube API Key 숨기기
netlify로 정적사이트 배포하기
CORS는 왜 이렇게 우리를 힘들게 하는걸까?
GCP - Capping API Usage
Youtube Data API - Videos, Search, CommentThreads
(영상) YouTube Data API v3 Tutorial
MDN ETag
'FrontEnd+' 카테고리의 다른 글
[React] 공식문서로 시작하는 리액트 입문 1편 (5) | 2021.04.04 |
---|---|
웹팩(Webpack) 밑바닥부터 설정하기 (3) | 2021.03.23 |
cypress - stub 사용법, Alert 테스트 예제, 이벤트타입 (1) | 2021.02.07 |
cypress - 테스트 주도 개발(TDD, BDD) 적용 (4) | 2021.02.03 |
네트워크 면접질문 총정리 (1) | 2021.01.27 |