
Youtube API를 이용해서 Youtube를 구현하고 있는 지금,
mock data를 받아오는 과정에서 발생한 문제점들.
Google에서 제공하는 Youtube API 실제 데이터를 받아오기 이전에,
Postman을 통하여 데이터 요청을 보내고 넘겨받은 데이터를 실제 데이터인 것처럼
mock data로 만들어 사용했다.
총 만든 mock data는 4개.
1. ChannelDetailContent.json => 채널 상세 콘텐츠 목록.
2. ListByKeyword.json => 검색창에 무언가를 검색했을 때 받아올 동영상 목록.
3. ListByRelatedVideo.json => 연관되어 있는 동영상 목록.
4. ListByTrendVideo.json => 처음 youtube를 켰을 때 가장 먼저 보이게 할 인기 있는 동영상 목록.
받아온 데이터들을 이렇게 mock data로 만들어준다.
그리고 video 목록을 보여줄 Videos component에서 mock data를 불러온다.
react query와 axios를 사용할 것이다.
그런데, react query와 axios를 사용하는 이유는 무엇일까?
먼저 데이터 fetch에 대하여 알아보도록 하자.
일반적으로 우리가 react에서 데이터 fetch를 할 때
필요한 component에서 데이터를 바로 fetch하거나
custom hook을 사용하여 거기 재활용할 useState, 데이터 fetch 등을 담아두는 일이 잦다.
그런데, 이렇게 만들어진 custom hook은
값이나 상태를 재사용하는 것이 아니라,
hook을 호출하는 component마다 외부 상태들이 각각 다르게 설정이 되고
hook을 호출하는 component마다 네트워크 통신이 발생을 한다.
[ custom hook의 문제점 ]
- 문제 1. cache가 되지 않는다.
네트워크에서 받아온 데이터를 별도로 저장해두는 것이 아니라,
이 커스텀 훅을 호출할 때마다 계속 새롭게 데이터를 받아올 것이다.
- 문제 2. letry 기능이 없다.
네트워크 통신에 실패했을 때 다시 재시도 할 수 있는 그런 기능들이 결여되어 있다.
react-query는 이런 커스텀 훅의 문제점들을 해결해준다.
react query는 강력한 비동기 상태 관리 라이브러리로,
네트워크에서 가져온 데이터를 상태 관리할 수 있게 도와주는 라이브러리 중
가장 많이 쓰이고 있는 것이다.
react-query는 react 라이브러리에서 data-fetching을 해주는 것으로만 간주되어지고 있는데,
조금 더 기술적으로 얘기하면
fetching, caching, synchronizing and updating server state (백엔드 데이터와의 동기화 및 업데이트) 등을 해준다.
Promise와 async…await를 사용할 줄 안다면 react-query를 사용하기도 쉽다:)
[ react query의 장점 ]
1. 네트워크 통신도 간편하게 할 수 있을 뿐더러,
2. 로딩 중인지 에러가 발생했는지 데이터를 받아왔는지 손쉽게 알 수 있을 뿐만 아니라
3. 여러 component에 걸쳐서 똑같은 데이터를 요청하는 것이 아니라,
동일한 데이터 요청이라면 얼마 동안 어플리케이션에 캐쉬를 해둘 건지 캐쉬 시스템도 제공해준다.
4. 글로벌 상태 관리도 제공해준다.
5. 네트워크 요청에 실패했다면 조금 후에 다시 시도해보는 letry 기능도 들어 있다.
6. 오래된 데이터를 background에서 업데이트하는 것까지 해준다. (별도로 설정하지 X)
그렇다면, axios는 왜 사용하는 걸까? fetch도 있지 않은가?
axios란, 브라우저, Node.js를 위한 Promise 기반 API를 활용하는 HTTP 비동기 통신 라이브러리이다.
백엔드와 프론트엔드랑 통신을 쉽게하기 위해 ajax와 같이 사용된다.
프레임워크에서 ajax를 구현할 때 axios를 쓰는 편이라고 보면 된다.
[ axios의 특징 ]
1. 서버 사이드에서는 네이티브 node.js의 http 모듈을 사용하고,
클라이언트(브라우저)에서는 XMLHttpRequests를 사용한다.
2. Promise(ES6) API를 사용한다.
3. 요청 및 응답 인터셉트
4. HTTP 요청 취소
5. HTTP 요청과 응답을 JSON 형태로 자동 변경해준다.
6. XSRF를 막기위한 클라이언트 사이드를 지원해준다.
fetch는 .json() 메서드를 사용해야 하는 방면에,
axios는 자동으로 JSON 데이터 형식으로 변환하기 때문에 .json() 메서드가 필요하지 않다!
또한, fetch는 데이터 요청에 실패해도 응답객체가 ok 속성을 포함하면 성공했다고 알려준다.
백엔드에서 무언가 잘못되었다고 반응을 해줘도 그걸 다 성공적인 케이스로 간주한다.
백엔드에서 status 코드를 주는데(성공 => 200대, 실패 => 400대),
fetch의 경우 어찌 되었건 데이터를 받아왔으니 그걸 모두 성공으로 간주한다.
이 경우 then 안에서 코드를 확인하여 200대라면 성공을,
400대라면 throw를 이용해서 직접 error를 던져줘야 한다.
이렇게 되면, 성공한 코드와 실패한 코드가 모두 then 안에 섞여 있다는 문제가 생긴다!
axios의 경우에는 데이터 요청에 실패했을 경우 error를 띄우면서 데이터 요청에 실패했음을 알린다.
status가 200대인 경우에만 then으로 들어오게 된다.
status가 400대인 경우에는 catch로 들어가게 된다.
그리고 이 표를 살펴보면 왜 axios를 권장하는지 정홛하게 알 수 있을 것 같다.
- axios VS fetch
axios | fetch |
써드파티 라이브러리로 설치가 필요 | 현재 브라우저에 빌트인이라 설치 필요 없음 |
XSRF 보호를 해준다 | 별도 보호 없음 |
data 속성을 사용 | body 속성을 사용 |
data는 object를 포함한다 | body는 문자열화 되어있다 |
status가 200이고 statusText가 ‘OK’이면 성공이다 | 응답객체가 ok 속성을 포함하면 성공이다 |
자동으로 JSON데이터 형식으로 변환된다 | .json()메서드를 사용해야 한다. |
요청을 취소할 수 있고 타임아웃을 걸 수 있다. | 해당 기능 존재 하지않음 |
HTTP 요청을 가로챌수 있음 | 기본적으로 제공하지 않음 |
download진행에 대해 기본적인 지원을 함 | 지원하지 않음 |
좀더 많은 브라우저에 지원됨 | Chrome 42+, Firefox 39+, Edge 14+, and Safari 10.1+이상에 지원 |
axios와 fetch, react query의 사용법에 관해서는
다음에 자세하게 다뤄보도록 하겠다.
(지금 할 게 너무 많아서 전부 다루기는 힘들 것 같다... 개인적으로 notion에 적어두기는 했지만서도.)
이제 다시 youtube project로 넘어와서 이야기를 해보자.
위에서 말한 대로 react query와 axios를 사용하여 데이터 fetch를 하도록 하겠다.
난 네트워크 통신을 통해 받은 데이터를 사용할 자식 컴포넌트들이
Root component 내에 모두 있으므로 Root component에서 QueryClientProvider 우산을 씌워주기로 하였다.
데이터는 client를 만들어 거기에 담아준다.
담아줄 데이터의 이름은 queryClient라고 지정해주겠다.
const queryClient = new QueryClient();
queryClient를 만들어줄 때는
@tanstack query에서 제공해주는 QueryClient라는 class를 사용해서 인스턴스를 만들어주면 된다.
이렇게 해주면
Outlet을 QueryClientProvider로 감싸뒀기 때문에
Outlet에서 사용하는 어떤 component에서든 useQuery를 사용할 수 있다.
(useQuery => react query에서 데이터 fetxh를 할 때 사용하는 것)
Videos component에서 데이터를 호출해 map을 돌리기로 하였으니
Videos component에서 useQuery를 사용한다.
react query는 isLoading, error, data 등을 기본으로 제공한다.(필요하면 가져다 쓰면 된다.)
별도로 useState를 만들어 사용할 필요가 없어 매우 편리하다.
const { keyword } = useParams();
// 여기서 하나 설명을 하자면 나는 Header component에서
// input에 입력하고 검색 버튼을 눌렀을 때 입력한 값이 파라미터로 들어가게끔 하였다.
// navigate(`/videos/${title}`);
// 그 이유는, 검색을 통하여 받아오는 search api의 경우에는
// 무엇을 검색했는지 keyword를 필요로 하기 때문이다.
// useParams hook으로 파라미터를 받아 keyword라고 변수 명을 지정해주고
// keyword의 유무에 따라 "videos"라는 key 값이 각각 다른 api를 요청하도록 하기 위해서이다.
const {isLoading, error, data:videos} = useQuery(
["videos", keyword], async () => {
return axios //
.get(`/data/${keyword ? "ListByKeyword" : "ListByTrendVideo"}.json`) //
.then((res) => res.data.items);
}
)
react query가 자유롭게 사용할 수 있도록 만들어 놓은
isLoading과 error를 가져다 사용해보자.
아래의 코드는 isLoading과 error가 true일 때 p 태그들을 보여준다는 이야기다.
isLoading && <p>Loading...</p>
error && <p>Something is wrong...</p>
그리고 아래의 코드는 videos,
즉 useQuery를 통하여 받아온 데이터를 videos라는 key에 저장했으므로
videos의 값이 존재하면 ul 태그 내에서 videos를 이용하여 map을 돌리는 것이다!
나는 map을 돌려 얻어낸 각각의 video 값을 사용하여
UI를 만들어줄 component를 하나 새롭게 만들었다.
이름하여 VideoCard! props로 video를 넘겨주는 것까지 완벽.
key 값의 경우에는 일단 ListByTrendVideo mock 데이터에 맞춰서 설정을 해줬다.
아마 ListByKeyword mock 데이터에는 video.id가 없기 때문에 문제가 발생하겠지만....
일단 설정을 해준다.
{videos && (
<ul>
{videos.map((video) => (
<VideoCard key={video.id} video={video} />
))}
</ul>
)
}
그리고 VideoCard component를 만들어 데이터 넘겨주기!
일단 받아온 데이터를 이용하여 video.snippet.title만 받아와 보도록 하겠다.
import React from "react";
export default function VideoCard({ video }) {
return <li>{video.snippet.title}</li>;
}
여기까지 해주면 데이터를 잘 받아오는 것을 볼 수 있다.
keyword의 경우, 현재 mock 데이터를 사용하고 있다보니
무엇을 검색해도 aespa 관련 리스트들만 받아오게 된다.(내가 aespa로 지정을 해뒀기 때문.)
그리고 아니나 다를까, 무언가를 검색했을 때 받아오는 데이터들은
key값이 일치하지 않아 다시 youtube 홈으로 돌아왔을 때
인기 영상 리스트 아래에 검색한 영상 리스트들이 딸려오더라.
이 부분을 개선하기 위해서는 두 데이터의 key 값을 맞춰줘야 할 것 같다.
이제, 이렇게 코드를 짰을 때 발생하는 문제점들을 알아보자.
1.현재 로직의 경우에는 Mock data를 받아오는 코드이기 때문에
나중에 실제 API를 받아올 때는 사용할 수 없다.
실제 API에서도 잘 동작하는지 확인하고 싶은데,
어떻게 실제 API data와 json(mock data)을 변환할 수 있는지?
2. 컴포넌트에 네트워크 통신 내부 구현 사항이 너무 많이 노출되어 있다.
어떤 url을 요청해야 하고 받아온 data를 어떤 것을 사용해야 하는지, 어떻게 변환해야 하는지 등.
그리고 만약 어플리케이션 다른 부분에서 검색 기능을 써야 한다면 동일한 코드를 다시 작성해야 한다!
=> 재사용성과 유지보수가 떨어진다.
그리고 useQuery를 사용하는 콜백함수에 코드가 많이 들어 있어 가독성이 떨어진다.
- 두 번째 문제 해결 방안 :
useQuery 내의 동작을 함수로 만들어 가져다 사용할 수 있게끔 한 다음,
함수의 이름만 봐도 알 수 있게 하는 것이다.
key 값, 함수 이름, 옵션 값만 보이게 하여 한 눈에 들어올 수 있게끔 해주는게 좋다.
src 폴더 내에 api 폴더를 만들고 폴더 내에 youtube.js 파일을 만들어 준다.
그 파일에 동작에 관한 함수를 따로 만들어준다.
// Videos.js
import { useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import VideoCard from "../components/VideoCard";
import { search } from "../api/youtube";
export default function Videos() {
const { keyword } = useParams();
const {
isLoading,
error,
data: videos,
} = useQuery(["videos", keyword], () => search(keyword));
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Something is wrong...😔</p>;
return (
<div className="w-[100%] h-[calc(100vh-60px)] bg-[#0f0f0f] overflow-y-scroll text-white">
<div className="w-[81.25rem] mx-auto p-10">
Videos {keyword ? `🔍${keyword}` : "🔥"}
{videos && (
<ul>
{videos.map((video, i) => (
<VideoCard key={i} video={video} />
))}
</ul>
)}
</div>
</div>
);
}
// youtube.js
import axios from "axios";
export async function search(keyword) {
if (keyword) {
console.log("fetching... keyword!");
} else {
console.log("fetching... Hot Trend!");
}
return axios
.get(`/data/${keyword ? "ListByKeyword" : "ListByTrendVideo"}.json`)
.then((res) => res.data.items);
}
youtube api와 관련된 로직을 분리해서 하나의 파일에 정리하면,
관련된 로직을 모아서 관리하기에 유지보수가 편해지고,
필요에 따라 로직을 재사용할 수 있는 이점이 있다.
이렇게 두 번째 문제는 해결.
이제 첫 번째 문제를 해결해야 한다.
실제 네트워크를 사용하는 것과 mock data를 사용하는 것, 스위칭(변환)해줘야 한다.
- 첫 번째 문제 해결 방안 :
Youtube라는 동일한 함수들(API)이 들어 있는 두 가지 구현 사항을 만들 것이다.
예를 들어, Youtube에 Search라는 API가 있으면 API의 규격(interface)을 준수하는 두 가지를 만들 것이다.
하나는 Fake Youtube, 하나는 Implementaition Youtube.
전자는 그냥 json을 읽어오는 것이며, 후자는 실제 구현 사항으로, Youtube에서 네트워크 통신을 하도록 만들 것이다.
아까 만들어준 api 폴더 내에 fakeYoutube.js 파일을 하나 더 만들어준다.
youtube.js의 코드를 복사해와서 변경을 해줄 것이다.
왜냐하면, module이 아닌 class를 이용할 것이기 때문이다!!
굳이 일반 함수의 나열(module)이 아니라 class를 사용해 한 번 더 감싸주는 이유는,
private 함수(#)를 사용하기 위해서 이다.
=> 주요 로직을 private으로 만들어서 정보 은닉화를 한다.
또한, class로 묶어 관련된 로직들의 응집도를 높인다.
그리고 DI 및 동일한 인터페이스를 가지고 (외부에서 쓸 수 있는 함수를 가지고)
다른 구현사항을 만들어 주기 위해서 이다. (Mock, 실제 네트워크 구현 등)
1. 먼저, fakeYoutube부터 class로 구현하기.
// fakeYoutube.js
import axios from "axios";
export default class FakeYoutube {
constructor () {
// 기본 상태를 설정해주는 생성자 메서드.
// 생성할 때 따로 전달해 줄 것이 없으므로 비워둔다.
}
async search (keyword) {
return keyword ? this.#searchByKeyword(keyword) : #listByTrendVideo();
// keyword가 있을 때는 searchByKeyword 함수를, 없을 땐 listByTrendVideo 함수를 return.
}
// class member 함수이므로 function이라는 keyword를 적지 않아도 된다.
// 함수 앞에 #을 붙이면 private 함수. class 내부적으로는 호출이 가능하나, 외부에서는 호출이 불가능하다.
async #searchByKeyword () {
return axios
.get(`/data/ListByKeyword.json`)
.then((res) => res.data.items)
.then((items) =>
items.map((item) => {
return { ...item, id : item.id.videoId };
// item을 하나씩 순회하면서 id가 있다면 item.id.videoId 문자열로 덮어씌워주기.
// ListByKeyword.json과 ListByTrendVideo.json의 key 값을 일치시켜주기 위해서이다.
// 데이터가 다르다고 인식해서는 안되기 때문이다.
// 그리고 사실 mock data의 경우, 현재 aespa라는 keyword로 검색을 하도록 통일되어 있기 때문에
// keyword를 따로 인자로 받을 필요가 없다.(ㅋㅋㅋㅋㅋ)
})
);
}
async #listByTrendVideo () {
return axios
.get(`/data/ListByTrendVideo.json`)
.then((res) => res.data.items);
}
}
2. youtube 네트워크 통신 class로 구현하기.
검색하는 API의 경우에는 q에 aespa가 아니라 keyword를 넣어주면 된다.
이전에는 임시로 설정을 해주려고 aespa를 넣어뒀던 것이다.
// youtube.js
export default class Youtube {
constructor () {
// axios 통신을 할 때 필요한 기본 세팅을 여기 해서 httpClient에 할당해 준다.
// .create() => custom config(사용자 지정 구성)를 사용하는 axios의 새 인스턴스를 생성할 수 있다.
this.httpClient = axios.create({
baseURL : "https://www.googleapis.com/youtube/v3",
params : { key : process.env.REACT_APP_YOUTUBE_API_KEY },
// url 속성 값이 절대 url이 아니라면 url 앞에 baseURL이 붙는다.
// axios 인스턴스가 상대 url을 해당 인스턴스의 메서드에 전달하도록 baseURL을 설정하는 것이 좋다.
// params는 요청과 함께 전송될 url 매개 변수.
// 일반 객체이거나 URLSearchParams 객체여야 한다.
// key 값의 경우, 외부 파일(.env)에 환경변수를 정의하여 변수로 받아오고 있다. => 보안과 유지보수에 용이하다.
// 포트, DB 관련 정보, API_KEY 등, 혼자 또는 팀만 알아야 하는 값(git, 오픈소스에 올리면 안되는 값들)
// 환경 변수를 커밋하는 곳에 저장하는 것이 아니라, 컴퓨터 상에 저장해두는 것이다.
// 배포할 때 변수를 설정해주면 된다. => 서버 상에서 환경 변수를 설정해준다.
});
}
async search (keyword) {
// static으로 메서드들을 정의하지 않으면 인스턴스를 생성해야 가져다 사용할 수 있다.
return keyword ? this.#searchByKeyword(keyword) : this.#listByTrendVideo();
}
async #searchByKeyword (keyword) {
return this.httpClient
.get("search", {
params : {
part : "snippet",
maxResults : 25,
type : "video",
q : keyword,
},
})
.then((res) => res.data.items)
.then((items) =>
items.map((item) => {
return { ...item, id : item.id.videoId };
})
);
}
async #listByTrendVideo () {
return this.httpClient
.get("videos", {
params : {
part : "snippet",
maxResults : 25,
type : "video",
chart : "mostPopular",
},
})
.then((res) => res.data.items);
}
}
[ post 요청시 파라미터 전달 방법 ]
axios.post('/url', {
id: myId,
name : myName
}).then(response => {
console.log(response.data);
}).catch(error => {
console.log(error.response);
});
[ get 요청시 파라미터 전달 방법 ]
axios.get('/url', {
params: {
id: myId,
name : myName
}
}).then(response => {
console.log(response.data);
}).catch(error => {
console.log(error.response);
});
여기까지 작업을 끝마쳤다면 Videos component로 돌아가
useQuery 코드를 마저 작성해주면 된다.
물론 mock data를 사용할 건지, 실제 API를 사용할 건지에 따라 class명을 변경해주면 된다.
일단은 하루에 사용할 수 있는 실제 API의 양이 정해져 있으므로
FakeYoutube를 class로 지정해줬다.
const {
isLoading,
error,
data : videos,
} = useQuery(["videos", keyword], () => {
const youtube = new FakeYoutube(); // 인스턴스 객체 생성.
return youtube.search(keyword); // 객체 안에 있는 search 함수를 return.
});
'React' 카테고리의 다른 글
React) Youtube API를 이용하여 Youtube 만들기3 (0) | 2023.06.19 |
---|---|
React) Youtube API를 이용하여 Youtube 만들기2 (0) | 2023.06.15 |
리액트 심화과정 노션. (0) | 2023.05.09 |
다시 시작하는 리액트 - 리액트 심화 3-2 (1) | 2023.05.06 |
다시 시작하는 리액트 - 리액트 심화 3-1 Quiz (0) | 2023.05.05 |
github : https://github.com/dnjfht
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!