본문 바로가기

TIL

TIL - 북마크된 영화 목록 기능 구현을 위한 고민과 해결 방안

프로젝트에서 북마크된 영화 목록 기능을 구현하는 과정에서 고민하고 최종적으로 결정한 사항을 공유합니다. 아래는 초기 코드와 최종 코드, 그리고 각 코드의 장단점을 비교한 개선된 사항입니다.

 

고민 이유

  1. 북마크된 영화 목록 기능:
    • 개별 영화의 상세 페이지에서 북마크를 추가하고, 이를 모아 북마크된 영화 목록을 표시하는 기능.
    • 북마크된 영화 목록을 표시하기 위해 각 영화의 세부 정보를 요청할 때 서버 과부하 우려.
  2. 데이터 요청에 대한 고민:
    • 인기순, 최신순 등의 영화 목록에서 중복된 영화가 있음.
    • 실제로 필요한 데이터보다 많은 데이터를 한 번에 요청하게 되어 불필요한 데이터까지 받아오게 됨.

해결 방안

  1. 한 번에 요청하는 방법:
    • 장점:
      • 네트워크 효율성: 여러 개의 요청을 하나로 줄여 네트워크 부하를 줄임.
      • 응답 속도: 한 번의 요청으로 필요한 모든 데이터를 받아옴.
      • 코드 간결화: 요청/응답 패턴이 단순화되어 코드가 간결해짐.
    • 단점:
      • 필요 이상으로 데이터를 요청: 실제로 필요한 데이터보다 많은 데이터를 한 번에 요청하게 되어 불필요한 데이터까지 받아오게 됨.
      • 복잡한 백엔드 구현: 여러 데이터를 한 번에 받아오기 위한 추가적인 백엔드 API 구현 필요.
  2. 필요한 데이터만 여러 번 요청하는 방법:
    • 장점:
      • 데이터 일치 보장: 각 요청마다 최신 데이터를 받아옴.
      • 세분화된 제어: 필요한 데이터만 요청하여 효율적.
      • 기존 API 활용: 추가적인 백엔드 구현 없이 현재 API를 그대로 사용할 수 있음.
    • 단점:
      • 네트워크 부하: 여러 개의 요청이 네트워크 부하를 증가시킬 수 있음.
      • 클라이언트 코드 복잡성: 많은 요청을 처리하는 코드가 복잡해질 수 있음.
  3. 캐싱 사용:
    • 장점:
      • 데이터 무결성 유지: 캐싱을 통해 중복 요청을 방지하고 최신 데이터를 유지.
      • 성능 최적화: 불필요한 네트워크 요청을 줄여 성능 향상.
    • 단점:
      • 캐싱 관리 필요: 데이터의 최신 상태를 유지하기 위한 캐싱 전략 필요.

최종 선택 이유

  1. 북마크 목록이 많지 않을 것으로 예상.
  2. 코드 복잡성 해결: 커스텀 훅을 사용해 복잡한 부분 처리.
  3. 병렬 비동기 요청 최적화: Promise.all → useQueries를 사용하여 응답 시간을 최적화.
  4. 데이터 일치 보장: 각 영화 ID가 일치할 것을 보장.
  5. 캐싱 추가: 데이터 캐싱을 통해 중복 요청 방지 및 성능 최적화.



초기코드

// 북마크 영화 목록 불러오기
export const useGetBookmarksWithDetails = (userId: string) => {
  return useQuery({
    queryKey: ["bookmarksWithDetails", userId],
    queryFn: async () => {
      const bookmarks: BookmarkRow[] = await api.bookmark.getBookmarks(userId);
      const movieDetails = await Promise.all(
        bookmarks.map(async (bookmark) => {
          const movie = await api.movie.fetchMovieDetails(
            Number(bookmark.movie_id)
          );
          return movie;
        })
      );
      return movieDetails;
    },
  });
};
 

 

최종코드

// 북마크 영화 목록 불러오기
export const useGetBookmarksWithDetails = (userId: string) => {
  const queryClient = useQueryClient();

  const bookmarksQuery = useQuery({
    queryKey: ["bookmarks", userId],
    queryFn: () => api.bookmark.getBookmarks(userId),
    enabled: !!userId,
  });

  const movieQueries = useQueries({
    queries: bookmarksQuery.data
      ? bookmarksQuery.data.map((bookmark) => {
          const movieId = Number(bookmark.movie_id);
          return {
            queryKey: ["Movie", movieId],
            queryFn: () => api.movie.fetchMovieDetails(movieId),
            enabled: bookmarksQuery.isSuccess,
          };
        })
      : [],
  });

  useEffect(() => {
    if (bookmarksQuery.data) {
      queryClient.invalidateQueries({ queryKey: ["Movie"] });
    }
  }, [bookmarksQuery.data, queryClient]);

  const movieDetails = movieQueries
    .map((result) => result.data)
    .filter((movie): movie is Movie => movie !== undefined);

  // 전체 상태를 통합하여 하나의 상태로 반환
  const status: "pending" | "error" | "success" =
    bookmarksQuery.status === "pending" ||
    movieQueries.some((result) => result.status === "pending")
      ? "pending"
      : movieQueries.some((result) => result.status === "error")
      ? "error"
      : "success";

  return {
    data: movieDetails,
    status,
  };
};

 

개선된 사항

  1. 구조적 개선:
    • 초기 코드: 모든 북마크된 영화의 세부 정보를 한 번에 가져오는 방식으로, 네트워크 부하가 크고 비효율적일 수 있음.
    • 최종 코드: useQueries를 사용하여 각 영화를 병렬로 가져와 비동기 처리를 최적화하고, 개별 요청 실패 시에도 다른 요청에 영향이 없도록 개선.
  2. 비동기 처리 최적화:
    • 초기 코드: Promise.all을 사용하여 동시 요청을 처리하지만, 이는 네트워크 부하를 줄이기에는 한계가 있음.
    • 최종 코드: useQueries를 통해 각 요청을 병렬로 처리하여 응답 시간을 최적화하고, React Query의 장점을 활용.
  3. 캐싱:
    • 최종 코드: queryClient.invalidateQueries를 사용하여 캐시를 효율적으로 관리함으로써 불필요한 네트워크 요청을 줄이고 성능을 최적화.
  4. 상태 관리:
    • 초기 코드: 단일 상태로 모든 데이터를 관리.
    • 최종 코드: 북마크 상태와 영화 세부 정보 상태를 통합하여 전체 상태를 세밀하게 관리, 더 견고한 코드 작성 가능.
  5. 확장성:
    • 최종 코드: 각 요청을 개별적으로 처리하고 상태를 통합하여 관리함으로써, 새로운 기능 추가나 다른 API와의 통합이 용이.