영화 리스트를 받아오던 중 카테고리명까지 Loading 되는 문제를 해결해보자

BangDori·2024년 3월 29일
0
post-thumbnail
post-custom-banner

문제 상황

TMDB API를 활용하여 영화 정보들을 받아와서 화면에 렌더링해주고 있었는데 영화 카테고리가 영화 정보들과 달리 정적인 정보임에도 불구하고, 영화에 대한 정보를 받아오는 동안 Loading 상태가 보여지는 문제가 발생하였다.

문제점

const MovieList: React.FC<MovieListProps> = ({ categoryNo }) => {
  const { movies, isLoading } = useMovies(categoryNo);

  if (isLoading) return <p>Loading...</p>;

  return (
    <div className='movie-container'>
      <h3 className='main-category'>{Categories[categoryNo].title}</h3>
      <ul className='movie-list'>
        {movies?.results.map((movie) => (
          <MovieCard
            key={movie.id}
            posterPath={movie.poster_path}
            title={movie.title}
            overview={movie.overview}
          />
        ))}
      </ul>
    </div>
  );
};

MovieList 컴포넌트를 확인해보니, useMovies 훅스를 통해 영화들에 대한 정보를 받아오는데 이때 isLoading 상태일 때 바로 Loading 중이라는 것을 표시하는 것을 확인할 수 있었다.

해결 방법

이를 해결해주기 위해서 생각해본 방법은 다음과 같다.

  1. 영화 카테고리와 영화 정보들을 컴포넌트로 분리한다.
  2. isLoading 상태에 대한 조건문을 ul 태그에 국한한다.
  3. 1번과 2번 방법을 섞어보자.

각 방법으로 코드를 작성해보자.

1. 영화 카테고리와 영화 정보들을 컴포넌트로 분리한다.

interface CategoryNoProps {
  categoryNo: CategoryNoType;
}

const MovieBox: React.FC<CategoryNoProps> = ({ categoryNo }) => {
  return (
    <div className='movie-container'>
      <h3 className='main-category'>{Categories[categoryNo].title}</h3>
      <MovieList categoryNo={categoryNo} />
    </div>
  );
};

const MovieList: React.FC<CategoryNoProps> = ({ categoryNo }) => {
  const { movies, isLoading } = useMovies(categoryNo);

  if (isLoading) return <p>Loading...</p>;

  return (
    <ul className='movie-list'>
      {movies?.results.map((movie) => (
        <MovieCard
          key={movie.id}
          posterPath={movie.poster_path}
          title={movie.title}
          overview={movie.overview}
        />
      ))}
    </ul>
  );
};

export default MovieBox;

이 방법의 경우, 정상적으로 영화 카테고리는 표시되고 영화 정보들에 대해서만 Loading 상태가 표시되는 것을 확인할 수 있다.

하지만 위 방법으로 컴포넌트를 분리하게 될 경우 컴포넌트의 depth가 늘어날 수 있으며, MovieBox 컴포넌트에서는 영화 카테고리만을 보여주고 props로 다시 MovieList를 전달해주기 때문에 props drilling 문제가 발생할 수 있다고 생각한다.

Props drilling

Props Drilling이란 상위 props를 하위 컴포넌트로 전달할 때, 중간 컴포넌트에서 props를 사용하지 않음에도 불구하고 props를 추가해서 계속해서 내려주는 이러한 문제를 Props Drilling이라고 합니다.

(추가)

앞서 설명했던 Props drilling을 염두에 두고 컴포넌트를 어떻게 설계할지에 대해 고민을 해보는데, 생각해보니 Props drilling 문제가 크게 심각하지 않을 것 같다는 생각을 했다.

우선 Props drilling은 이런 단점들을 가지고 있다.

  1. 코드의 가독성이 매우 나빠지게 됩니다.
  2. 코드의 유지보수 또한 힘들어지게 됩니다.
  3. state 변경시 props 전달 과정에서 불필요하게 관여된 컴포넌트들 또한 리렌더링이 발생합니다.

하지만 위 단점들을 고려해보았을 때 결국, props drilling의 큰 문제점은 props를 사용하지 않는 중간 컴포넌트에서도 계속해서 props를 전달해주어야 하기 때문에 발생하는 문제점이였다.

그렇다면 괜찮지 않을까?

2. isLoading 상태에 대한 조건문을 ul 태그에 국한한다.

const MovieList: React.FC<MovieListProps> = ({ categoryNo }) => {
  const { movies, isLoading } = useMovies(categoryNo);

  return (
    <div className='movie-container'>
      <h3 className='main-category'>{Categories[categoryNo].title}</h3>
      {isLoading ? (
        <p>Loading...</p>
      ) : (
        <ul className='movie-list'>
          {movies?.results.map((movie) => (
            <MovieCard
              key={movie.id}
              posterPath={movie.poster_path}
              title={movie.title}
              overview={movie.overview}
            />
          ))}
        </ul>
      )}
    </div>
  );
};

export default MovieList;

이 방법도 정상적으로, 영화 카테고리와 영화 정보들이 분리되어서 렌더링 된다. 하지만 추후 Loading 상태일 때 Skeleton UI를 적용해주고, Error 상태일 때도 에러 상태인 UI를 표시해줄 계획이기 때문에 return문이 복잡해질 것으로 예상된다. 그리고 위 방법의 경우에는 movie-list가 리렌더링되는 경우, main-category도 다시 리렌더링이 발생할 수 있다.

(추가) 2번 방법

2번 방법으로 컴포넌트를 다음과 같이 구성해보았다.

const MovieList: React.FC<MovieListProps> = ({ categoryNo }) => {
  const { movies, isLoading } = useMovies(categoryNo);

  return (
    <div className='movie-container'>
      <h3 className='main-category'>{Categories[categoryNo].title}</h3>
      {isLoading ? (
        <SkeletonMovieList />
      ) : isError ? (
        <ErrorComponent
          errorMessage={getMovieLoadErrorMessage(categoryNo)}
          onRefetch={refetch}
        />
      ) : (
        <ul className='movie-list'>
          {movies?.results.map((movie) => (
            <MovieCard
              key={movie.id}
              posterPath={movie.poster_path}
              title={movie.title}
              overview={movie.overview}
            />
          ))}
        </ul>
      )}
    </div>
  );
};

export default MovieList;

사실 컴포넌트가 명확하게 눈에 그려지고 있다. 하지만 위와 같이 컴포넌트를 구성하니, 영화 리스트들에 대한 정보를 조작하는 이벤트를 추가하기에 다소 어려운 점이 있었다.

영화 리스트에서는 왼쪽 오른쪽으로 슬라이드 하는 버튼이 추가되어야 했는데, 위와 같이 컴포넌트를 구성하니 MovieList 컴포넌트가 너무 복잡해지고 있었다.

그래서 최종적으로 결정한 방법은 1번과 2번을 섞은 방법으로 컴포넌트를 구성하였다.

3. 1번과 2번 방법을 섞어보자. ✅

interface MovieBoxProps {
  categoryNo: CategoryNoType;
}

const MovieBox: React.FC<MovieBoxProps> = ({ categoryNo }) => {
  const { movies, isLoading, isError, refetch } = useMovies(categoryNo);

  return (
    <div className='movie-container'>
      <h3 className='main-category'>{Categories[categoryNo].title}</h3>

      {isLoading ? (
        // Loading 상태라면 MovieList에 대한 Skeleton UI가 표시됩니다.
        <SkeletonMovieList />
      ) : isError ? (
        // Error가 발생하게 된다면 Error UI와 재요청 버튼이 표시됩니다.
        <ErrorComponent
          errorMessage={getMovieLoadErrorMessage(categoryNo)}
          onRefetch={refetch}
        />
      ) : (
        // Success라면 영화들에 대한 정보가 화면에 표시됩니다.
        <MovieList movies={movies} />
      )}
    </div>
  );
};

export default MovieBox;

MovieBox 컴포넌트를 다음과 같이 만들고, Success 상태일 때의 컴포넌트를 다음과 같이 분리하였다.

interface MovieListProps {
  movies: Movie[];
}

const MovieList: React.FC<MovieListProps> = ({ movies }) => {
  return (
    <ul className='movie-list'>
      {movies.map((movie) => (
        <MovieCard
          key={movie.id}
          posterPath={movie.poster_path}
          title={movie.title}
          overview={movie.overview}
        />
      ))}
    </ul>
  );
};

export default MovieList;

MovieList에 대한 컴포넌트를 다음과 같이 분리하고 나니, MovieList 자체에 대한 슬라이드 버튼을 추가하기에도 용이해지게 되었다.

(추가)

실제로 영화 API에 대해 Loading, Error, Success 결과를 반환해주는 것은 MovieList 컴포넌트의 역할이라고 생각했다. 그래서 MovieList 컴포넌트에서 API를 통해 데이터를 받아오고, 각 status별로 처리해주는 것이 더 효율적이라고 생각하였다.

interface MovieListProps {
  categoryNo: CategoryNoType;
}

const MovieList: React.FC<MovieListProps> = ({ categoryNo }) => {
  const { movies, isLoading, isError, refetch } = useMovies(categoryNo);

  if (isLoading) return <SkeletonMovieList />;

  if (isError)
    return (
      <ErrorComponent
        errorMessage={getMovieLoadErrorMessage(categoryNo)}
        onRefetch={refetch}
      />
    );

  return (
    <ul className='movie-list'>
      {movies.map((movie) => (
        <MovieCard
          key={movie.id}
          posterPath={movie.poster_path}
          title={movie.title}
          overview={movie.overview}
        />
      ))}
    </ul>
  );
};

export default MovieList;

느낀점

해당 프로젝트를 진행하면서, 나는 컴포넌트 분리 기준에 대해 생각해보았다. 내가 생각해본 컴포넌트의 분리 기준은 다음과 같았다.

  1. 컴포넌트 가독성
  2. 컴포넌트 유지보수성
  3. 컴포넌트 재사용성

위와 같이 생각한 기준은 다음과 같다. 내가 만든 컴포넌트는 결국에는 내가 아닌 다른 팀원이 볼 수 있는 컴포넌트이기 때문에 반드시 가독성이 좋아야만 추후 유지보수와 기능 추가에도 도움이 될 것이라고 생각했다.

그리고 컴포넌트 유지보수성을 나는 컴포넌트의 변경이 최소한 하나의 이유로만 이루어져야만 유지보수성이 좋다고 생각했다. 예를 들어 다음과 같은 컴포넌트가 있다고 가정해보자.

interface ComponentProps {
  movies: Movie[];
  users: User[];
}

const Component: React.FC<ComponentProps> = ({ movies, users }) => {
  return (
    <div className='container'>
      <ul className='movie-list'>
        {movies.map((movie) => (
          <Movie movie={movie} />
        ))}
      </ul>
      <ul className='user-list'>
        {users.map((user) => (
          <User user={user} />
        ))}
      </ul>
    </div>
  );
};

위 Component의 경우에는 movies와 users를 받고 있는 것을 확인할 수 있다. 유지보수의 측면에서 고려를 해보자.

예를 들어서 영화 정보에 대한 UI가 변경되었다고 가정해보면, Component를 변경해주어야 하고, Movie 컴포넌트 내부도 변경해주어야 할 것이다. 뿐만 아니라, 만약 movie-list와 user-list가 스타일에도 서로 영향을 주고 있다면 영화 정보에 대한 UI가 변경되었을 뿐인데, user-list에 대한 스타일도 수정되어야만 한다.

그렇기에 나는 다음과 같이 변경이 최소한 하나의 이유로만 이루어지는 컴포넌트가 좋은 컴포넌트라고 생각한다.

interface ComponentProps {
  movies: Movie[];
}

const Component: React.FC<ComponentProps> = ({ movies }) => {
  return (
    <div className='movie-container'>
      <ul className='movie-list'>
        {movies.map((movie) => (
          <Movie movie={movie} />
        ))}
      </ul>
    </div>
  );
};

그리고 마지막 컴포넌트 재사용성이다. 결국 리액트는 작은 블럭(컴포넌트)들을 조합하여 하나의 페이지를 구성하기 때문에 중복되는 블럭이 있다면 이를 잘 나누는 것이 리액트를 효율적으로 다루는 것이라고 생각한다.

profile
Happy Day 😊❣️ >> bangdori.kr
post-custom-banner

0개의 댓글