
저번에 mock data를 받아오는 것과 실제 API를 받아오는 것을
class를 사용하여 로직을 짜보았다.
but, 이렇게 해도 여전히 문제가 발생한다.
사용하는 곳(네트워크 통신이 일어나는 곳)마다 class 인스턴스를 생성해서 호출해야 한다는 것. 🥺
이렇게 되면 내부 로직이 노출되어 있을 뿐만 아니라,
계속 호출할 때마다 새로운 인스턴스를 생성해야 해서 성능면에서 좋지 못하다.
그렇기 때문에 이번에는
Context API를 사용해서 youtube API를 담당하는 우산을 하나 만들어 줄 거고
그 우산에서 딱 하나의 인스턴스를 가지고 있을 수 있도록 만들어 볼 것이다.
1. 먼저, src 폴더 내에 context 폴더 만들기. 그리고 YoutubeApiContext.js 파일 생성해주기.
createContext()를 사용해서 context를 먼저 만들어준다.
export const YoutubeApiContext = createContext();
2. content 관련 자식 component들이 모두 모여 있는 Root component로 가 우산을 씌워준다.
// Root.js
import React from "react";
import Header from "../components/Header";
import { Outlet } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DarkModeProvider } from "../provider/DarkModeContext";
import { YoutubeApiProvider } from "../provider/YoutubeApiContext";
const queryClient = new QueryClient();
export default function Root() {
return (
<div>
<DarkModeProvider>
<Header />
<YoutubeApiProvider>
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
</YoutubeApiProvider>
</DarkModeProvider>
</div>
);
}
3. 그 다음에 우산(provider)을 만들어 준다.
const youtube = new Youtube();
// Youtube class를 이용하여 인스턴스 객체 만들어주기.
export function YoutubeApiProvider({children}) {
return (
<YoutubeApiContext.Provider value={{youtube}}>
{children}
</YoutubeApiContext.Provider>
)
}
export function useYoutubeApi () {
return useContext(YoutubeApiContext);
}
그리고 Videos component로 돌아가서
youtube를 사용할 수 있도록 useYoutubeApi()를 호출한다.
const { youtube } = useYoutubeApi();
이렇게 하면, 이제 사용하는 곳(네트워크 통신하는 곳)에서
하나하나 인스턴스 객체를 만들어줄 필요도 없다.
이미 인스턴스 객체를 만들어 변수 youtube에 할당을 해줬기 때문에
그냥 youtube 객체 안에 있는 search 함수를 호출하면 된다.
const {
isLoading,
error,
data : videos,
} = useQuery(["videos", keyword], () => youtube.search(keyword));
그런데 provider를 이용해서 우산을 만들 때
{{ youtube }} 이런 식으로 중괄호를 두 번 사용한 이유가 뭘까?
- 확실한 이유는 모르지만,
value 프로퍼티에 객체 { youtube: youtube } 를 전달하고자 이렇게 전달한 것이라 생각한다.
그래야 나중에 { youtube } = useContext()를 사용하여
객체 디컨스트럭쳐링으로 youtube 객체를 사용하고자 하는게 아닌가 싶다.
(추후에 더 알아보도록 하겠다.)
여기까지 코드를 작성해주면
우산 내에 있는 component들은 새로운 인스턴스 객체를 생성할 필요 없이
이미 만들어진 하나의 인스턴스 객체를 가져다 사용할 수 있다.
그런데, Youtube와 FakeYoutube는 동일한 함수를 가지고 있다보니
데이터를 변형하는 코드(map을 돌리는 코드)가 두 군데 다 들어가 있는 등
중복되는 코드들이 꽤 있다.
=> 이걸 방지하는 방법으로 Youtube class에서 모든걸 처리하도록 두고,
httpClient 부분을 두 개로 나뉘어 주는 것이다.
1. 실제 네트워크 통신을 하는, baseURL과 key를 가지고 있는 client.
2. mock data를 가져오는 client.
DI(Dependency Injection) : 의존성 주입 개념을 여기 적용시켜 보는 것이다.
의존성이란 개념은 IT에서는 '어떤 것과 다른 것이 서로 영향을 받아 주고 받는 관계'를 의미한다.
그리고 주입이란 단어는 A에 의해 B, C 등이 사용 된다라는 의미이다.
리액트에서는 Context를 사용 시 의존성이 주입되어 기본 데이터들과 컴포넌트들과의 의존성을 띄고 있다.
하지만, 의존성이 과다할 경우 스파게티 코드가 될 수 있고 리팩터링 시 에러 발생 확률을 높인다고 한다.
따라서 의존성 관리를 해줄 필요가 있는데, 그 중에서 특히 서버의 데이터는 client에서 id기반으로 관리한다.
그 이유는 데이터의 스키마(데이터베이스 구조)를 알 필요가 없고
id만 이용해서 컴포넌트의 데이터에 바로 접근할 수 있기 때문이다.
위에서 말한 대로 youtubeClient와 fakeYoutubeClient를 만들어준다.
그리고 Youtube class 내에서 httpClient를 만드는 것이 아니라, 외부로부터 받아올 것이다.
(apiClient)
export default class Youtube {
constructor(apiClient) {
this.apiClient = apiClient;
}
}
이제, axios를 Youtube class 내에서 바로 사용하는 것이 아니라,
apiClient에 있는 search()를 호출할 것이다.
그리고 search()를 호출할 때 필요한 params를 전달해 줄 것이다.
async #searchByKeyword(keyword) {
return this.apiClient
.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 };
})
);
}
아래 역시 동일하게 apiClient에 있는 videos()를 호출할 것이다.
그리고 videos()를 호출할 때 필요한 params를 전달해 줄 것이다.
async #listByTrendVideo() {
return this.apiClient
.videos({
params: {
part: "snippet",
maxResults: 25,
type: "video",
chart: "mostPopular",
},
})
.then((res) => res.data.items);
}
그리고 Youtube class의 constructor 인자로 받아온 class 내에 search와 videos 함수를 만들어 준다.
search()는 검색된 비디오, videos()는 트렌드 비디오 목록을 각각 보여주게끔 구현한다.
한 마디로 미리 생성해뒀던 FakeYoutubeClient, YoutubeClient class 내에
search(), videos() 함수를 생성해주라는 얘기다.
먼저 FakeYoutubeClient부터 코드를 작성해 보겠다.
import axios from "axios";
export default class FakeYoutubeClient {
constructor() { }
async search() {
return axios.get(`/data/ListByKeyword.json`);
}
async videos() {
return axios.get(`/data/ListByTrendVideo.json`);
}
}
이미 Youtube class에서
keyword가 있냐, 없냐에 따라서 search()냐, videos()냐로 나누고 있기 때문에
keyword는 필요가 없다.
그냥 search()에는 mock data ListByKeyword를,
videos()에는 mock data ListByTrendVideo를 axios로 받아오면 된다.
다음은 YoutubeClient의 코드를 작성해보자.
Youtube class에 있던 httpClient를 YoutubeClient constructor에 담아주고
search(), videos() 함수를 생성해준다.
search()와 videos()에 매개변수로 params를 넘겨주고
search()에는 this.httpClient.get("search", params)를,
videos()에는 this httpClient.get("videos", params)를 return해준다.
import axios from "axios";
export default class YoutubeClient {
constructor() {
this.httpClient = axios.create({
baseURL: "https://www.googleapis.com/youtube/v3",
params: { key: process.env.REACT_APP_YOUTUBE_API_KEY },
});
}
async search(params) {
return this.httpClient.get("search", params);
}
async videos(params) {
return this.httpClient.get("videos", params);
}
}
그리고 우산을 만들어주는 YoutubeApiContext.js로 이동해
const youtube = new Youtube(); 또는 const youtube = new FakeYoutube()를 지워주고
const client = new YoutubeClient();
const youtube = new Youtube(client);를 추가해준다.
[ 최종 코드 ]
- Youtube class
- YoutubeClient class
- FakeYoutubeClient class
'React' 카테고리의 다른 글
[개인 쇼핑몰 개발] ScrollRestoration (1) | 2023.08.20 |
---|---|
React) Youtube API를 이용하여 Youtube 만들기3 (0) | 2023.06.19 |
React) Youtube API를 이용하여 Youtube 만들기 (0) | 2023.05.25 |
리액트 심화과정 노션. (0) | 2023.05.09 |
다시 시작하는 리액트 - 리액트 심화 3-2 (1) | 2023.05.06 |
github : https://github.com/dnjfht
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!