230901.log

Universe·2023년 9월 1일
0

log

목록 보기
6/14
🗓️ 날짜 : 2023.09.01

📚 할 일 :
	- 10to7
    - 1day 1commit 1post
    - 1mon 3project

📝 오늘의 목표 : 유튜브 클론코딩 프로젝트 진행

⌛ 공부시간 : 10:00 ~ 21:00

✅ 목표달성 : false

Til

객체지향적으로 api instance 관리하기

youtube api 를 이용해서 개발한 서비스에 원하는 정보를 뿌려주기 위해서는
여러가지 방법이 있을 수 있겠지만, 가장 익숙하게 사용했던 방법이
axios 로 인스턴스를 만들고 api 의 규칙에 맞춘 api 함수들을 만들어
react-query 등으로 캐싱하여 관리하고 원하는 컴포넌트에서 사용하는 방법이다.
초기 설계했던 방향은 다음과 같다.

const instance = axios.create({
  baseURL: "https://www.googleapis.com/youtube/v3",
  params: { key: import.meta.env.VITE_YOUTUBE_API_KEY },
});

export const fetchPopularVideos = async ({
  categoryId,
  pageToken,
}: {
  categoryId?: string;
  pageToken?: string;
}) => {
  const params = {
    part: "snippet, contentDetails, statistics",
    chart: "mostPopular",
    regionCode: "KR",
    maxResults: 10,
    pageToken,
    ...(categoryId && { videoCategoryId: categoryId }),
  };

  const response = await instance.get("/videos", { params });
  return {
    videos: response.data.items,
    nextPageToken: response.data.nextPageToken,
  };
};

export const fetchChannelInfo = async (channelIds: string[]) => {
  const params = {
    part: "snippet",
    id: channelIds.join(","),
  };

  const response = await instance.get("/channels", { params });
  return response.data.items;
};

export const getPopularVideos = async ({
  pageToken,
  categoryId,
}: {
  pageToken?: string;
  categoryId?: string;
}) => {
  const { videos, nextPageToken } = await fetchPopularVideos({
    pageToken,
    categoryId,
  });

  const channelIds = videos.map((video: any) => video.snippet.channelId);
  const channels = await fetchChannelInfo(channelIds);
  console.log(channels);

  const items = videos.map((video) => {
    const channelInfo = channels.find(
      (channel) => channel.id === video.snippet.channelId
    );
    return {
      id: video.id,
      title: video.snippet.title,
      duration: video.contentDetails.duration,
      thumbnail: video.snippet.thumbnails.medium.url,
      publishedAt: video.snippet.publishedAt,
      viewCount: video.statistics.viewCount,
      publisher: channelInfo.snippet.title,
      publisherProfileImg: channelInfo.snippet.thumbnails.default.url,
    };
  });

  return { items, nextPageToken: nextPageToken };
};

api response 가 자주 바뀌던 도중이어서 타입에 대한 정보가 없어 any 타입을 일시적으로 사용했다.
이런 개발방식은 프론트엔드 개발에서 아주 흔하게 사용되고,
파일분리 등의 가독성을 신경쓴 리펙토링을 해준다면 크게 문제될게 없어보인다.
실제로 간단한 로직이라면 이렇게 작성하는 쪽이 개발생산성 측면에서 좋다.

그런데 얼마전에 class 를 이용해서 조금 더 객체 지향적으로 axios instance 를 관리하는 방법에 대해서
공부했다. 그래서 프로젝트에 적용해봤다.
초기에 구현한 내용은 아래와 같다.

xport default class YoutubeInstance implements IYoutubeInstance {
  private instance: AxiosInstance;

  constructor() {
    this.instance = axios.create({
      baseURL: "https://www.googleapis.com/youtube/v3",
      params: { key: import.meta.env.VITE_YOUTUBE_API_KEY },
    });
  }

  private getPopularVideosParams = ({
    categoryId,
    pageToken,
  }: VideoParamsProps) => {
    return {
      part: "snippet, contentDetails, statistics",
      chart: "mostPopular",
      regionCode: "KR",
      maxResults: 16,
      pageToken,
      ...(categoryId && { videoCategoryId: categoryId }),
    };
  };

  private fetchPopularVideos = async ({
    categoryId,
    pageToken,
  }: VideoParamsProps): Promise<PopularVideos> => {
    const params = this.getPopularVideosParams({ categoryId, pageToken });
    const response = await this.instance.get("/videos", { params });
    return {
      videos: response.data.items,
      nextPageToken: response.data.nextPageToken,
    };
  };

  private getChannelInfoParams = (channelIds: string[]) => {
    return {
      part: "snippet",
      id: channelIds.join(","),
    };
  };

  private fetchChannelInfo = async (
    channelIds: string[]
  ): Promise<ChannelInfo> => {
    const params = this.getChannelInfoParams(channelIds);
    const response = await this.instance.get("/channels", { params });
    return response.data.items;
  };

  fetchVideos = async ({
    pageToken,
    categoryId,
  }: VideoParamsProps): Promise<FetchVideos> => {
    const { videos, nextPageToken } = await this.fetchPopularVideos({
      pageToken,
      categoryId,
    });
    const channelIds = videos.map((video) => video.snippet.channelId);
    const channels = await this.fetchChannelInfo(channelIds);

    const items = videos.map((video) => {
      const channelInfo = channels.find(
        (channel) => channel.id === video.snippet.channelId
      )!;

      return {
        id: video.id,
        title: video.snippet.title,
        duration: video.contentDetails.duration,
        thumbnail: video.snippet.thumbnails,
        publishedAt: video.snippet.publishedAt,
        viewCount: video.statistics.viewCount,
        publisher: channelInfo.snippet.title,
        publisherProfileImg: channelInfo.snippet.thumbnails,
      };
    });

    return { items, nextPageToken };
  };
}

그냥 로직을 class에 억지로 넣은 것 처럼 보인다.
그리고 이런 class를,

import { createContext } from "react";
import YoutubeInstance, { IYoutubeInstance } from "../service/youtubeInstance";

type ContextProps = {
  instance: IYoutubeInstance;
};

export const YoutubeApiContext = createContext<ContextProps | null>(null);
const instance = new YoutubeInstance();

export function YoutubeApiProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <YoutubeApiContext.Provider value={{ instance }}>
      {children}
    </YoutubeApiContext.Provider>
  );
}

export default function useYouTubeAPI() {
  const context = useContext(YoutubeApiContext);
  if (!context) {
    throw new Error("Cannot find YoutubeApiProvider");
  }
  return context;
}

이처럼 커스텀 훅으로 만들어, root 컴포넌트에 provider 형태로 씌워주면
api 를 사용하고 싶은 곳에서 호출해서 사용할 수 있다.

사실 위의 코드는 객체지향적이라고 할 수 없다.
단지 class 를 사용한다고 해서 무조건 객체지향적으로 코딩이 되는 것은 아니다.
youtube api 의 호출을 캡슐화 하고 있긴 하지만
객체지향의 핵심인 상속이나 확장성에 대한 고려가 없으며,
무엇보다도 '굳이' 이런식으로 작성해야 할 이유가 없는 것 처럼 보인다.

그래서
이 리펙토링을 조금 더 의미있게 하기 위해서 여러가지를 고려해볼 수 있다.
내가 생각한 개선방향은 우선, 클래스를 분리함으로써 책임소재를 명확하게 하고
각각의 역할만 담당할 수 있도록 한다. 그런 다음 클래스를 상속으로 연결한다.
이를 구현한 내용은 아래와 같다.

// 인스턴스를 생성하는 기본 클래스
class BaseYoutubeInstance {
  protected instance: AxiosInstance;

  constructor() {
    this.instance = axios.create({
	  //...
    });
  }
}

// 정보를 가져오는 작업을 담당하는 클래스
class MiddlewareYoutubeInstance extends BaseYoutubeInstance {
  protected fetchPopularVideos = async (params: VideoParamsProps): Promise<PopularVideos> => {
    // ...
  };

  protected fetchChannelInfo = async (channelIds: string[]): Promise<ChannelInfo> => {
    // ...
  };
}

// 실제 서비스에서 사용될 API를 담은 클래스
class ServiceYoutubeInstance extends MiddlewareYoutubeInstance implements IYoutubeInstance {
  fetchVideos = async (params: VideoParamsProps): Promise<FetchVideos> => {
    const { videos, nextPageToken } = await this.fetchPopularVideos(params);
    // ...
    return { items, nextPageToken };
  };
}

이런식으로 코드를 리펙토링 했을 때 얻을 수 있는 몇가지의 장점이 있다.
우선, 책임소재를 명확하게 함으로써 유지보수, 가독성 측면에서 유리하다.
추가적인 구현사항이 있을 때, axios 인스턴스를 추가하게 되더라도,
BaseYoutubeInstance 클래스에 추가하면 api 로직을 수정 할 필요없이 동일한 api 를 사용할 수 있으며,
ServiceYoutubeInstance 클래스에는 지금은 videos 를 fetch 하는 기능밖에
구현하지 않았지만, 추가적인 구현사항이 생기더라도 상속한 class 에서
로직을 끌어다 쓸 수 있다.
조금 더 명확하게 세분화 하면 조금 더 유기적으로 로직을 구성할 수 있겠지만,
현재는 구현사항이 많지 않아서 그렇게 세분화 하지는 않기로 했다.

이런 코드가 장점만 있는 것은 아니다.
분명, 이런식으로 작성된 instance 는 재사용성, 유지보수성, 그리고 확장성에도 이점을 갖고,
내부의 상태 등을 활용할 수 있다는 측면에서 유용하지만,
서로 종속되어 강하게 결합되고, 오히려 유연성이 줄어드는 결과를 낳을수도 있다.
보일러 플레이트등의 복잡성과 개발생산성 저하도 단점으로 꼽을 수 있겠다.

간단한 구현사항이라면 함수로 작성하더라도 크게 문제가 없지만
굉장히 규모가 큰, 관리가 필요한 프로젝트라면 사용해봐도 나쁘지 않을 것 같다.


Feedback

면접이 잡혀서 프로젝트에 크게 손을 대지 못했다.
너무 간만의 면접이라서 뭐부터 준비해야 할 지 모르겠다.
어영부영 회사부터 검색해보고 하다가 오늘을 다 써버리고 말았다. 😔
시간관리에 조금 더 신경쓸 것.


예상되는 내일의 목표

  • 면접준비
    - 회사정보 찾아보기, 서비스 조사해보기
    - 예상질문 및 답변 준비하기
  • 유튜브 클론코딩 프로젝트 이어서
profile
Always, we are friend 🧡

0개의 댓글