✍️Intro.
React를 배우면서 한 번쯤은 만들어본다는 Youtube 클론 사이트를 제작했다. 새로운 프로젝트를 시작할 때마다 "다른 사람들은 하루 이틀 만에 쉽게 만들 수 있는 사이트이지 않을까, 왜 이렇게 고전하는 걸까..?" 이런 생각을 하며 주눅이 들곤 한다. 이번 프로젝트도 세 번이나 만들다 포기하기를 반복했으나 아래의 문구를 읽으며 끝내 완성할 수 있었다.
"
방법을 몰라서 안 하는 겁니다.
방법을 몰라서 못하는 겁니다.
방법을 몰라서 지속할 수 없고
방법을 몰라서 시도할 수 없습니다.
계속하십시오.
시행착오를 겪으십시오.
처음부터 완벽할 수는 절대 없습니다.
꾸중을 들어도 그러려니 하십시오.
익숙해지면 모든 것이 괜찮아질 것입니다.
"
이번 프로젝트의 핵심은 데이터를 어떤 방식으로 받아오고, 이를 어떻게 가공하여 사용하는지에 중점을 두었다. 구현할 기능 몇 가지를 생각해 두었지만, 추후에 조금씩 추가해 볼 계획이다. (OAuth2 방식의 로그인, 실제 개인 Youtube 내에 구독 채널이나 영상 가져오기 , 나중에 보고 싶은 영상 저장하기 등)
이번 사이트를 통해 데이터의 수집과 처리, 그리고 이를 활용하는 방법을 배우는 데 주력했다. 이는 React를 사용해 동적인 웹 애플리케이션을 만드는 데 있어 중요한 기초를 다질 수 있는 좋은 기회가 되었다.
🥲어려웠던 점과 어떻게 해결하였는지에 대해서
#1
1차적으로 완성했을 당시 컴포넌트 내에 네트워크 통신의 내부 구현 사항이 너무 많이 노출되어 있어 가독성이 좋지 못하였으며 코드를 수정하거나 유지보수하는 것이 복잡했다.
또한, YouTube Data API의 일일 할당량은 10,000 쿼터로 제한되어 있어, 호출이 약 15번을 초과하면 할당량 부족으로 에러가 발생한다. 이러한 이유로 Mock Data를 만들어 활용해야 했는데, 매번 Mock Data와 실제 Data API를 비교하면서 개발해야 하므로 비동기 통신 코드를 분리하여 작성할 필요가 있었다.
매번 비교할 때마다 통신 코드 전체를 주석 처리하는 방법은 매우 비효율적이며, 실제로도 그렇게 하는 사람은 없을 것이라고 생각했다.
[1차 개선]
실제 데이터를 담당하는 비동기 네트워크 로직인 `youtube.js`와 Mock Data를 이용하는 로직 `fakeYoutube.js`로 분리하여 모듈로서 구분하였다. 이를 통해 내부 구현 로직을 숨길 수 있으며 코드의 가독성을 높일 수 있었다.
# before
const {
isLoading,
error,
data: videos,
} = useQuery({
queryKey: ['videos', keyword],
// Mock 데이터 이용 로직
queryFn: async () => {
return axios
.get(`/videos/${keyword ? 'search' : 'popular'}.json`)
.then((res) => res.data.items);
},
});
# after
const { youtube } = useYoutubeApi();
const {
data: videos,
isLoading,
error,
} = useQuery({
queryKey: ['videos', keyword],
queryFn: () => youtube.search(keyword),
});
[2차 개선]
코드 구조화와 유지보수성 : 클래스를 사용하여 관련된 메서드를 하나의 객체로 묶음. 이를 통해 코드의 구조가 명확해지고, 특정 기능이나 서비스에 대한 코드 관리를 한곳에서 할 수 있어. 유지보수성과 가독성을 높일 수 있었다.
class FakeYoutube {
// 모든 네트워크 요청 메서드를 클래스 내에 정의
async search(keyword, nextToken = '') { /* ... */ }
async #searchByKeyword(keyword, nextToken = '') { /* ... */ }
async mostPopular() { /* ... */ }
async channelImageURL() { /* ... */ }
// 기타 메서드들...
}
클래스를 통해 인스턴스를 생성하면, 각 인스턴스는 독립적인 상태를 가질 수 있다. 이를 통해 여러 개의 API 서비스 객체를 생성하여 병렬로 사용하거나, 특정 조건에 따라 다른 객체를 사용할 수 있어 실제 데이터 통신 코드와 Mock 데이터 통신 코드를 분리하여 사용할 수 있었다.
import { FakeYoutube } from 'api/fakeYoutube';
import { Youtube } from 'api/youtube';
const {
isLoading,
error,
data: videos,
} = useQuery({
queryKey : ['videos', keyword],
queryFn : () => {
const youtube = new FakeYoutbe();
const youtube = new Youtube();
// 상황에 맞게 인스턴스하여,
// 실제 데이터와 Mock 데이터 상황에 맞게 사용할 수 있음
return youtube.search(keyword);
});
[3차 개선]
위의 코드대로 매번 비동기 네트워크 통신이 필요한 컴포넌트마다 인스턴스를 생성할 경우 호출할 때마다 인스턴스가 생성되는 문제가 있다.
1. 메모리 사용 증가
많은 인스턴스를 생성하면 메모리 사용량이 증가하여 성능에 부정적인 영향을 미친다.
2. 성능 저하
많은 인스턴스를 생성하고 관리하는 과정은 CPU 리소스를 소모한다. 빈번한 인스턴스 생성과 소멸은 성능 저하를 일으킬 수 있으며, 이는 특히 렌더링이 자주 발생하는 UI 컴포넌트에서 문제를 야기할 수 있다.
3. 네트워크 부하 증가
각 인스턴스가 별도의 네트워크 요청을 처리한다면, 네트워크 트래픽이 증가할 수 있다. 이는 서버에 과부하를 주거나, 클라이언트 측에서 네트워크 지연을 초래할 수 있다.
4. 관리의 복잡성
많은 인스턴스를 생성하면 코드의 복잡성이 증가한다. 인스턴스를 추적하고 관리하는 것이 어려워지며, 특히 인스턴스 간의 상태 관리가 복잡해질 수 있다. 이는 디버깅과 유지보수를 어렵게 만든다.
이러한 문제들을 해결하기 위해 다음과 같은 해결책을 고려할 수 있다.
* Context API : React의 Context API를 사용하여 인스턴스를 중앙에서 관리한다. 이를 통해 각 컴포넌트에서 인스턴스를 새로 생성할 필요가 없어지며, 메모리 사용과 성능 저하 문제를 완화할 수 있다.
* 싱글톤 패턴 : 싱글톤 패턴을 사용하여 하나의 인스턴스만 생성하고 이를 재사용한다. 이를 통해 메모리 사용을 최소화하고, 코드 관리와 유지보수성을 높일 수 있다.
싱글톤 패턴(Singleton Patten)은 객체지향 디자인 패턴 중 하나로, 어떤 클래스의 인스턴스가 오직 하나만 존재하도록 보장하는 패턴이다. 이 패턴을 사용하면 애플리케이션 전역에서 해당 클래스의 단일 인스턴스에 접근할 수 있으며, 이를 통해 객체 생성 및 관리의 효율성을 높일 수 있다.
import { FakeYoutube } from 'api/fakeYoutube';
import { Youtube } from 'api/youtube';
import { createContext, useContext } from 'react';
export const YoutubeApiContext = createContext();
const youtube = new FakeYoutube();
// const youtube = new Youtube();
export function YoutubeApiProvider({ children }) {
return (
<YoutubeApiContext.Provider value={{ youtube }}>
{children}
</YoutubeApiContext.Provider>
);
}
export function useYoutubeApi() {
return useContext(YoutubeApiContext);
}
import { useYoutubeApi } from 'context/YoutubeApiContext';
const { youtube } = useYoutubeApi();
const {
isLoading,
error,
data: videos,
} = useQuery({
queryKey : ['videos', keyword],
queryFn : () => youtube.search(keyword));
#2
"더 보기" 버튼을 클릭 시 기존 데이터는 유지하되, 새로운 데이터를 불러오기 기능을 구현 부분
이를 구현하기 위해 useQuery로 이루어진 코드를 수정하는 부분에서 많은 시행착오가 있었다.
1.
"더 보기" 버튼을 클릭하면 데이터를 축적하는 방식으로 재렌더링하는 방식으로 재구성해야 했다. 페이지네이션된 데이터를 저장할 상태와 다음 페이지 토큰을 저장할 상태를 추가했다.
const [data, setData] = useState([]);
const [nextPageToken, setNextPageToken] = useState('');
2.
비디오 데이터를 useQuery의 queryFn 부분에서 state에 담아야 하는지, 아니면 useEffect를 이용해서 데이터가 변경될 때마다 담는 방법이 좋은지 고민하며 테스트하는 과정에서, 결국 useQuery의 onSuccess 콜백을 활용하여 상태를 업데이트는 방법과 useEffect를 사용하는 방법 중에서 고민했다.
* useEffect를 사용하는 방법
useEffect를 사용하여 데이터 페칭 후 상태를 업데이트할 수 있으며, 이 방법은 페칭 로직과 상태 업데이트 로직을 분리하는 데 유용하다.
export default function Videos() {
const { keyword } = useParams();
const { youtube } = useYoutubeApi();
const [data, setData] = useState([]);
const [nextPageToken, setNextPageToken] = useState('');
const fetchLiveVideos = async (pageToken = '') => {
const result = await youtube.search(keyword, pageToken);
return {
items: result.items,
nextPageToken: result.nextPageToken,
};
};
const {
isLoading,
error,
data: videos,
} = useQuery({
queryKey: ['videos', keyword],
queryFn: () => fetchLiveVideos(),
staleTime: 1000 * 60 * 1,
});
const fetchNextPage = async () => {
if (!nextPageToken) return;
const result = await fetchLiveVideos(nextPageToken);
setData((prevData) => [...prevData, ...result.items]);
setNextPageToken(result.nextPageToken);
};
useEffect(() => {
if (videos) {
setData(videos.items);
setNextPageToken(videos.nextPageToken);
} else {
setData([]);
setNextPageToken('');
}
}, [videos]);
return ( .... )
}
* onSuccess 콜백을 사용하는 방법
React Query의 onSuccess 콜백은 데이터가 성공적으로 페칭 되었을 때 실행된다. 이 방법은 데이터 페칭과 상태 업데이트를 하나의 흐름으로 처리할 수 있다.
const {
isLoading,
error,
data: videos,
} = useQuery({
queryKey: ['videos', keyword],
queryFn: () => fetchLiveVideos(),
staleTime: 1000 * 60 * 1,
onSuccess: (data) => {
setData(data.items);
setNextPageToken(data.nextPageToken);
},
});
두 개의 방법을 모두 해본 결과, onSuccess를 사용하는 방법에 문제가 있었다. onSuccess 콜백이 호출되기 전에 컴포너트가 다시 랜더링 되면서 data상태가 업데이트되지 않는 것이다. 아마도 onSuccess 콜백이 비동기적으로 실행되기 때문인 것 같다.
3.
https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery
useInfiniteQuery | TanStack Query React Docs
Does this replace [Redux, MobX, etc]? react
tanstack.com
충격적이게도 onSuccess에 대해 알아보다 useInfiniteQuery를 사용하면 무한 스크롤이 가능하다는 사실을 알게 되었다.
이 쿼리를 사용하면 기존의 코드를 획기적으로 줄일 수 있었다.
const {
data: videos,
error,
fetchNextPage,
hasNextPage,
isLoading,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ['videos', keyword],
queryFn: async ({pageParam = ''}) => {
return youtube.search(keyword, pageParam);
},
getNextPageParam: (lastPage) => lastPage.nextPageToken || undefined,
});
getNextPageParam 함수는 useInfiniteQuery 훅에서 매우 중요한 역할은 한다. 이 함수는 마지막으로 가져온 페이지의 데이터를 기반으로 다음 페이지를 가져올 때 사용할 매개변수를 결정한다.
getNextPageParam의 매개변수로 전달되는 lastPage는 pages 배열의 마지막 요소이다. 즉, 마지막으로 가져온 페이지의 데이터이다.
* 반환된 데이터의 구조
{
"pages": [
{
"items": [
// 25개의 아이템들
],
"nextPageToken": "CBkQAA"
}
],
"pageParams": [undefined]
}
pages: 각 페이지의 데이터를 포함하는 배열
pageParams: 각 페이지를 가져올 때 사용된 파라미터를 포함하는 배열
* 반환된 데이터의 구조에서 lastPage의 데이터 형태는 다음과 같다.
{
"items": [
// 25개의 아이템들
],
"nextPageToken": "CBkQAA"
}
getNextPageParam의 매개변수로 전달되는 lastPage는 pages 배열의 마지막 요소로, 마지막으로 가져온 페이지의 데이터다. 이 데이터에는 현재 페이지의 항목들(items)과 다음 페이지를 가져오기 위한 토큰(nextPageToken)이 포함되어 있다.
위의 코드로 기존의 긴 코드를 대체할 수 있다는 사실에 신기함과 허망함이 들었다. 하지만 이렇게 React-Query에 대해 또 한 가지 배울 수 있어서 좋았다.
#3
다양한 데이터 엔드포인트와 파라미터 설정 :
유튜브 API를 사용하여 shorts, 인기 영상, 관련된 비디오, 채널 정보, 댓글, 라이브 영상 등 다양한 데이터를 불러오기 위해 각기 다른 엔드포인트와 필요한 파라미터를 정확히 설정해야 했다. 각 데이터 소스마다 다른 구조와 요구사항을 가지고 있어, 이를 하나하나 설정하고 관리하는 것이 살짝 복잡했다.
#4
중첩된 데이터 구조 처리
유튜브 API로부터 데이터를 받아올 때, 객체 속에 객체가 중첩된 형태로 데이터가 반환되었습니다. 원하는 정보를 추출하기 위해서 중첩된 구조를 일일이 파헤쳐야 했고, 이를 위해 객체 구조 분해 할당(destructuring)을 적극적으로 사용했다.
const { date, description } = data.snippet;
그러나, 데이터가 없는 경우를 대비하여 옵셔널 체이닝(optional chaining)을 사용해야 했다. 이를 통해 렌더링 과정에서 데이터가 없어서 발생하는 오류를 방지할 수 있었다.
const date = data?.snippet?.date;
const description = data?.snippet?.description;
☕정리
이 회고록의 목적은 React를 배우면서 경험한 Youtube 클론 사이트 제작 과정을 되돌아보고, 그 과정에서 겪었던 어려움과 해결 방법을 정리하여 공유하는 데 있다. 또한, 프로젝트를 통해 얻은 교훈과 성장을 기록하여 앞으로의 학습과 개발에 도움이 되고자 한다.
🤔느낀 점
React를 이용해 Youtube 클론 사이트를 만드는 과정은 쉽지 않았다. 특히, 프로젝트를 여러 번 시도하고 중도에 포기하는 일이 반복되면서 "다른 사람들은 쉽게 하는데 나는 왜 이렇게 어려울까?"라는 생각에 주눅이 들기도 했다. 하지만, "계속 시도하고 시행착오를 겪으라"는 격려의 문구가 큰 힘이 되었다.
프로젝트의 핵심은 데이터를 어떻게 받아오고 가공하여 사용하는지에 중점을 두었다. OAuth2 로그인, 구독 채널이나 영상 가져오기, 나중에 보고 싶은 영상 저장하기 등의 기능을 추가하고자 했지만, 우선 데이터 수집과 처리, 그리고 이를 활용하는 방법을 배우는 데 주력했다.
🥲어려웠던 점과 해결 방법
1. 컴포넌트 내 네트워크 통신의 과도한 노출
* 문제점 : 네트워크 통신 구현이 너무 많이 노출되어 가독성이 떨어지고 유지보수가 어려웠다.
* 해결책 : 실제 데이터를 담당하는 youtube.js와 Mock Data를 담당하는 fakeYoutube.js로 비동기 통신 코드를 분리했다. 이를 통해 내부 구현 로직을 숨기고 코드의 가독성을 높였다.
2. 비동기 통신 코드 구조화와 유지보수성 향상
* 문제점: 코드가 구조화되지 않아 유지보수가 어려웠다.
* 해결책: 관련된 메서드를 하나의 객체로 묶는 클래스를 사용하여 코드 구조를 명확히 하고 유지보수성과 가독성을 높였다.
3. 인스턴스 생성 문제
* 문제점: 각 컴포넌트마다 비동기 네트워크 통신을 위해 인스턴스를 생성하면 메모리 사용 증가, 성능 저하, 네트워크 부하 증가 등의 문제가 발생했다.
* 해결책: Context API와 싱글톤 패턴을 사용하여 인스턴스를 중앙에서 관리하고 재사용함으로써 메모리 사용을 최소화하고 코드 관리와 유지보수성을 높였다.
4. 데이터 축적 및 페이징
* 문제점: "더보기" 버튼 클릭 시 기존 데이터를 유지하면서 새로운 데이터를 불러오는 기능 구현이 어려웠다.
* 해결책: useInfiniteQuery를 사용하여 무한 스크롤을 구현하고, 기존 코드를 획기적으로 줄였다.
5. 다양한 데이터 엔드포인트와 파라미터 설정
* 문제점: 유튜브 API의 각기 다른 엔드포인트와 파라미터를 설정하고 관리하는 것이 복잡했다.
* 해결책: 각 데이터 소스의 구조와 요구사항을 정확히 이해하고 설정을 철저히 관리함으로써 문제를 해결했다.
6. 중첩된 데이터 구조 처리
* 문제점: 유튜브 API로부터 반환된 중첩된 데이터 구조를 처리하는 것이 어려웠다.
* 해결책: 객체 구조 분해 할당과 옵셔널 체이닝을 사용하여 중첩된 데이터를 효과적으로 처리했다.
이번 프로젝트를 통해 React와 관련된 많은 개념을 체득하고, 동적인 웹 애플리케이션을 만드는 데 있어 중요한 기초를 다질 수 있었다. 특히, 문제 해결 과정에서 얻은 경험과 교훈은 앞으로의 개발에 큰 자산이 될 것이다. 계속해서 배우고 성장해 나가며, 더 나은 개발자가 되기 위해 노력할 것이다.
'React' 카테고리의 다른 글
[Webpack] (0) | 2024.06.14 |
---|---|
useState는 동기적일까? 비동기적일까? (0) | 2024.06.13 |
Tanstack Query 옵션에 대한 (0) | 2024.05.17 |
useContext와 useReducer 그리고 Hook에 대해서... (0) | 2024.05.09 |
이중괄호로 데이터를 보내는 이유 (0) | 2024.05.09 |