들어가며
챗팟 프로젝트를 끝내고, 컴포넌트의 관리와 API의 활용법에 대해 자세히 알고싶어 엘리 쌤의 리액트 강의를 수강하게 되었다. 그 중 거의 막바지 프로젝트로 진행되는 유튜브 프로젝트이다. 본 프로젝트에서 배운 점이 많아 정리해놓으려 한다.
큼직한 단계마다 구현이 완성된 모습을 보여주고, 다음 영상에서는 해당 구현에 대한 해설이 있는 방식으로 진행되었다. 나는 그 사이에 직접 구현해보고, 해설을 통해 개선하였다.
프로젝트를 통해 배운 것
파일관리
- /public
- /mock-data : 유튜브 API mock data
- /src
- /api : 유튜브 API 요청과 관련된 로직 파일
- /components : 사용한 컴포넌트들
- /context : API 요청을 통해 받아온 동영상 데이터를 모든 컴포넌트에서 활용할 수 있게 해주는 Context 코드
- /pages : 라우팅되는 페이지들
- /util : 업로드 날짜 포맷을 변경하는 timeago.js를 사용하기 위함
디렉토리 구성이다. 리액트로 제작한 이전 프로젝트인 챗팟에서는 페이지 별로 분리한 폴더 한 가지 밖에 존재하지 않았다. 그마저도 Components 폴더였다. 좀 더 역할이 확실하게, 가독성 좋게 컴포넌트와 파일들을 관리할 수 있게 되었다.
컴포넌트 분리와 재사용
새로 배운 내용을 기반으로 이전 프로젝트를 돌아 본다면, 난 컴포넌트를 사용하지 않았다. 페이지 별로 분리해놓고 그 페이지 안에서 구현되는 모든 로직을 처리하였다. 그 과정에서 중복되는 내용도 많았을 것이다. 컴포넌트는 모듈이다. 재사용할 낌새가 보인다면 가차없이 형태를 조금 바꿔 재사용하는 것이 보기에도 깔끔하고, 렌더링에도 부담을 덜 줄 것이다. 어릴 때 병뚜껑을 눈에 불을 키고 모으시던 미술학원 선생님이 생각났다. 위 사진에서도 볼 수 있듯 본 프로젝트에서는 모든 페이지에 적용되는 NavBar, 비디오 리스트 페이지와 비디오 상세 페이지에 적용되는 VideoCard, 비디오 상세 페이지에 적용되는 RelatedVideo, ChannelInfo 컴포넌트로 구성돼있다. 눈 여겨 볼 것은 VideoCard 컴포넌트이다.
사진(상)의 비디오 검색 결과 목록들과 사진(하) 우측의 연관비디오 목록은 동일한 <VideoCard /> 컴포넌트를 재사용한 것이다. 연관비디오 목록으로 사용할 때만 type='list'라는 프롭을 전송하여 flex나 gap등의 클래스를 추가로 부착하였다. (tailwindCSS를 사용)
컴포넌트들이 노드처럼 꽤나 Nested하게 구성된다는 것을 알았다. 그러나 그 기준을 정하는 것은 여전히 쉽지 않다. 다음 프로젝트에서는 어떠한 기준으로 컴포넌트를 만들고, 어떻게 재사용할 수 있을지 고려하면서 진행할 필요가 있을 듯 하다.
라우터를 사용하는 방식
기존에는 컴포넌트 형식(<route element={(...)}/>으로 페이지들을 관리하였다. 이를 Nested route라고 하며, react-router-dom의 최신 버전에서는 권장하지 않는 방법이라고 한다. 구현 가능한 다양한 방식을 적용해보고 싶기에 본 프로젝트에서는 다른 방식으로 구현하였다.
App.js
import { RouterProvider, createBrowserRouter } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
errorElement: <NotFound />,
children: [
{ index: true, element: <Videos /> },
{ path: '/videos', element: <Videos /> },
{ path: '/videos/:keyword', element: <Videos /> },
{ path: '/videos/watch/:id', element: <VideoDetail /> },
],
},
]);
export default function App() {
return (
<>
<RouterProvider router={router} />
</>
);
}
createBrowserRouter()을 이용해 router이라는 변수로 페이지들을 관리하였다. 먼저 기본적인 경로나 표시할 컴포넌트들을 지정해주고 (Root.jsx를 사용하였다) Outlet을 씌울 컴포넌트들을 children 속성 안에 작성해주었다.
Root.jsx
export default function Root() {
return (
<div>
<NavBar />
<Outlet />
</div>
);
}
API***
본 프로젝트에서는 Youtube Search API를 이용하였고, Postman을 통해 테스트하였다. 다음과 같은 몇 번의 단계를 통해 리팩토링되었다. 솔직히 흐름을 이해하는데도 시간이 좀 걸렸다. 관련된 공부를 더 해야겠다고 생각했다.
- 먼저 요청과 데이터 수신을 한 번에 끝내는 하나의 클래스로 구성하였다. 클래스 내부에 있는 private한 메서드를 통해 API 서버에 데이터를 요청하고, 받아온 데이터를 정제하였다.
- 데이터 통신 부분만 따로 관리하는 Client 클래스와 params들을 설정해 이를 호출하는 클래스로 분리하였다. 이를 의존성 주입이라 한다고 하는데, 이에 대해서는 더 공부해볼 예정이다.
- 데이터를 받아오는 로직을 useContext를 통해 Custom Hook으로 만들어 준 후, useQuery를 통해 필요한 페이지에서 사용할 수 있게 하였다.Outlet 페이지에 Provider을 감쌌기 때문에 NavBar를 제외한 모든 컴포넌트는 동영상 데이터를 사용할 수 있게 된다.
Mock Data
유튜브의 API 요청량은 일일 제한이 존재하기 때문에, 주로 mock data를 준비해놓고 개발한다고 한다. 본 프로젝트에서는 Mock data와 Real data를 호출하는 클래스를 별도로 생성해둔 후, 인스턴스 생성 시에만 클래스를 바꾸어 개발과 테스트가 용이하게 하였다.
궁금했던 것과 결론
Promise와 Async
Promise와 then을 통해 요청 로직을 구현하였는데, 왜 Async를 통해 메서드들을 선언하였는지 궁금했다. Async는 Await와 짝을 이루어 사용되는 것이 아닌가? 했지만 잘못 알고 있었다. Async를 통해 선언된 함수들은 Promise를 반환한다. 우리는 이를 Axios의 서버 요청에 사용하였고, 해당 비동기 동작이 완료될 때까지 함수의 처리를 일시 중지시키는 await 없이도 사용될 수 있다는 것을 알았다.
추가로 공부할 것
- 의존성 주입과, 프론트엔드에서 이를 어떻게 적용하는지
- Context Custom Hook과 useQuery를 통한 호출
- 다른 기업의 오픈소스, 프로젝트 코드 뜯어보기
- 배운 내용들을 통해 이전 프로젝트 리팩토링