MovieApp 만들기 (영화 페이지)

김재훈·2023년 9월 16일

MovieApp 만들기

목록 보기
3/8

영화 목록 페이지

영화 목록 페이지는 모듈화한 API와 React QueryuseInfiniteQuery, react-infinite-scroller 라이브러리를 사용해서 무한스크롤로 영화 목록을 구성한다.

// scr/pages/PopularMoviePage.tsx

import { useInfiniteQuery } from "react-query";
import InfiniteScroll from "react-infinite-scroller";
import { getPopularMovie } from "@/api/movieApi";
import { MovieDetail } from "./Home/Home";

const PopularMoviePage = (props: Props) => {
  const { isLoading, isError, data, fetchNextPage, hasNextPage } =
    useInfiniteQuery(
      ["popular"],
      ({ pageParam = 1 }) => getPopularMovie(pageParam),
      {
        getNextPageParam: (lastPage) => {
          let page = lastPage.page;
          if (lastPage.total_page === page) {
            return false;
          }
          return page + 1;
        },
      }
    );

  if (isLoading) <h1>Loading...</h1>;
  if (isError) <h1>Error</h1>;

  return (
    <Fragment>
      <h2
        style={{
          marginLeft: "25px",
          marginBottom: "50px",
          textAlign: "center",
        }}
      >
        Popular Movie
      </h2>
      <InfiniteScroll loadMore={() => fetchNextPage()} hasMore={hasNextPage}>
        <MovieList>
          {data?.pages?.map((page) => {
            return page?.results.map((movie: MovieDetail) => (
              <Fragment key={movie.id}>
                <MovieCard movieData={movie} />
              </Fragment>
            ));
          })}
        </MovieList>
      </InfiniteScroll>
    </Fragment>
  );
}; 

Popular, Top Rated, Upcoming 각각 호출하는 API만 다르고 내용은 같다. 각 영화 목록의 MovieCard는 조금씩 다르다. Top Rated 페이지는 평점(vote_average)을 표시하고, Upcoming 페이지는 영화 개봉 날짜(release_date)를 표시한다.

// src/pages/TopRated/MovieCard.tsx

import { MovieDetail } from "@/pages/Home/Home";
import { Link } from "react-router-dom";

type MovieProps = {
  movieData: MovieDetail;
};

const MovieCard = ({ movieData }: MovieProps) => {
  const { id, title, poster_path, vote_average } = movieData;
  return (
    <Card>
      <LinkDetail to={`/${id}`}>
        <PosterWrapper>
          {poster_path ? (
            <PosterImg src={`https://image.tmdb.org/t/p/w500/${poster_path}`} />
          ) : (
            <h1>No Image</h1>
          )}
        </PosterWrapper>
        {<h4>{title}</h4>}
        {<h4>{vote_average}</h4>}
      </LinkDetail>
    </Card>
  );
};
// src/pages/Upcoming/MovieCard.tsx

import { MovieDetail } from "@/pages/Home/Home";
import { Link } from "react-router-dom";

type MovieProps = {
  movieData: MovieDetail;
};

const MovieCard = ({ movieData }: MovieProps) => {
  const { id, title, poster_path, release_date } = movieData;
  return (
    <Card>
      <LinkDetail to={`/${id}`}>
        <PosterWrapper>
          {poster_path ? (
            <PosterImg src={`https://image.tmdb.org/t/p/w500/${poster_path}`} />
          ) : (
            <h1>No Image</h1>
          )}
        </PosterWrapper>
        {<h4>{title}</h4>}
        {<h4>{release_date}</h4>}
      </LinkDetail>
    </Card>
  );
};

영화 상세 페이지

영화 상세 페이지는 영화의 ID로 라우팅한다. API는 Detail, Trailer, Credits를 호출한다.

// src/api/movieDetail.ts

import { instance } from "./index";

export const getMovieDetail = async (id: string) => {
  const response = await instance.get(`movie/${id}`);
  return response.data;
};

export const getMovieTrailer = async (id: string) => {
  const response = await instance.get(`movie/${id}/videos`);
  return response.data;
};

export const getMovieCredits = async (id: string) => {
  const response = await instance.get(`movie/${id}/credits`);
  return response.data;
};
// src/pages/Detail/MovieDetailPage.tsx

import { getMovieDetail } from "@/api/movieDetail";
import { useQuery } from "react-query";
import { useParams } from "react-router-dom";
import TrailerButton from "./components/TrailerButton";
import Cast from "./components/Cast";

const MovieDetailPage = (props: Props) => {
  const { id } = useParams() as { id: string };

  const { isLoading, data, isError } = useQuery(["detail"], () =>
    getMovieDetail(id)
  );

  if (isLoading) <h1>Loading...</h1>;
  if (isError) <h1>Error ㅠㅠ</h1>;

  return (
    <DetailContainer>
      <PosterContainer>
        {data?.poster_path ? (
          <img src={`https://image.tmdb.org/t/p/w500/${data.poster_path}`} />
        ) : (
          ""
        )}
        <TrailerButton />
      </PosterContainer>
      <InfoContainer>
        <h2>{data?.title}</h2>
        <h3>{data?.original_title}</h3>
        <p>
          {`${data?.vote_average}`} {`🤩${data?.vote_count}`}
        </p>
        <p>
          {data?.genres?.map((item: any) => (
            <span key={item.id}>{item.name} </span>
          ))}
        </p>
        <p>{data?.overview}</p>
        <Cast />
      </InfoContainer>
    </DetailContainer>
  );
};

useParams()의 타입을 정하는데 오류가 발생했다. react-router-dom v6 이상인 경우 useParams()만 쓰더라도 타입이 string | undefined일거라고 예상해준다.

그런데 위와 같은 오류가 발생했다. 구글링해서 찾아본 결과

const { id } = useParams() as { id: string };

이렇게 사용하면 에러가 사라진다.

영화 트레일러

영화 포스터 밑에 트레일러 보기 버튼을 만들어 버튼을 누르면 화면 중앙에 유튜브로 트레일러가 나오게 했다. 화면 다른 곳을 클릭하면 트레일러 모달창이 닫힌다.

// src/pages/Detail/components/TrailerButton.tsx

import { getMovieTrailer } from "@/api/movieDetail";
import { useQuery } from "react-query";
import { useParams } from "react-router";
import TrailerModal from "./TrailerModal";

import styled from "styled-components";

type Props = {};

const TrailerButton = (props: Props) => {
  const { id } = useParams() as { id: string };
  const [openModal, setOpenModal] = useState<boolean>(false);

  const { isLoading, data, isError } = useQuery(["trailer"], () =>
    getMovieTrailer(id)
  );

  if (isLoading) <h1>Loading...</h1>;
  if (isError) <h1>Error ㅠㅠ</h1>;

  const trailerKey: string = data?.results[0]?.key;

  return (
    <ButtonWrapper>
      {trailerKey && (
        <button onClick={() => setOpenModal(true)}>트레일러 보기</button>
      )}
      <TrailerModal
        trailerKey={trailerKey}
        open={openModal && trailerKey}
        onClose={() => setOpenModal(false)}
      />
    </ButtonWrapper>
  );
};

useState를 사용해서 버튼을 클릭하면 true값을 트레일러 모달 컴포넌트로 넘겨준다.

// src/pages/Detail/TrailerModal.tsx

import ReactPlayer from "react-player";

import styled from "styled-components";

type TrailerProps = {
  trailerKey: string;
  open: string | boolean;
  onClose: () => void;
};

const TrailerModal = ({ trailerKey, open, onClose }: TrailerProps) => {
  if (!open) return null;
  return (
    <ModalWrapper onClick={onClose}>
      <Player>
        <ReactPlayer
          url={`https://www.youtube.com/watch?v=${trailerKey}`}
          width="100%"
          height="100%"
          playing
          controls
        />
      </Player>
    </ModalWrapper>
  );
};

export default TrailerModal;

const ModalWrapper = styled.div`
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
`;

const Player = styled.div`
  position: absolute;
  top: 50%;
  left: 50%;
  height: 800px;
  width: 100%;
  max-width: 1100px;
  transform: translate(-50%, -50%);
`;

open값이 true가 아니면 null값을 반환하게 해서 자동으로 트레일러 모달창이 열리지 않게 한다.

if (!open) return null;

그리고 position css를 이용해 트레일러 모달창이 화면 정중앙 맨 위에 나오게 한다.

profile
김재훈

0개의 댓글