UMC 8th Web 워크북

🍿 TMDB 영화 상세 페이지 불러오기 (Umc 3주차 미션3)

minnote29 2025. 4. 7. 02:30
미션 2까지는 영상을 통해서 해결을 했고, 미션 2의 스타일 적용 같은 경우는 tailwind css를 활용하여 변경하고, 미션 3에도 추가적으로 스타일을 적용했다. 또한, MoviePage에서 useEffect를 통해 api를 호출한 내용을 참고하여 MovieDetailPage.tsx 를 생성했고, 코드가 길어짐에 따라 컴포넌트 분리를 했다. 이번 정리는 미션 3를 어떤 식으로 완료했는지 정리를 하려고 한다.
더보기
더보기

📁 폴더 구조

├── components/
│   ├── MovieHero.tsx  // 영화 정보      
│   ├── MovieCredits.tsx  // 출연진 프로필
│   └── LoadingSpinner.tsx  
├── pages/
│   └── MovieDetailPage.tsx  // 영화 상세 페이지
├── types/
│   └── detail.ts           

 

💿 MovieDetailPage 

  • 구현하려던 내용
    • 특정 영화의 상세 정보를 보여주는 페이지 구현
    • 영화 상세 페이지 상단에 배경 이미지 + 영화 관련 정보 출력하기
    • 영화 상세 페이지 하단에는  출연진 썸네일 목록 출력하기
  • useParams는 API 데이터를 불러올 때 url에서 movieId를 추출하기 위해서 사용했고, useState, useEffect를 이용해 비동기 상태 관리를 했다.

 

  • 상태
const [movie, setMovie] = useState<MovieDetailWithCredits | null>(null);
const [isPending, setIsPending] = useState(false);
const [isError, setIsError] = useState(false);

 

  • movie: API로 받아온 영화 전체 데이터 (MovieDetail + credits)
  • isPending: 로딩 스피너 표시
  • isError: 에러 발생 시 알림

 

  • useEffect를 통한 API 호출
useEffect(() => {
  const fetchMovieDetail = async () => {
    setIsPending(true);
    try {
      const { data } = await axios.get<MovieDetailWithCredits>(
        `https://api.themoviedb.org/3/movie/${movieId}?language=ko-KR&append_to_response=credits`,
        {
          headers: {
            Authorization: `Bearer ${import.meta.env.VITE_TMDB_KEY}`,
          },
        }
      );
      setMovie(data);
    } catch {
      setIsError(true);
    } finally {
      setIsPending(false);
    }
  };

  if (movieId) {
    fetchMovieDetail();
  }
}, [movieId]);
  • movieId가 존재할 경우에만 요청하도록 추가적으로 넣어줬고, append_to_response=credits로 출연지에 대한 데이터도 같이 가져오도록 했다. 또한, API 요청 성공 시 setMovie(data)로 상태를 저장하는 방식으로 진행했다.
  • 로딩 및 에러 처리는 아래처럼 처리를 해줬고,
  • MovieHero: 상단 이미지 + 제목 + 설명 + 장르 등을 표시하는 컴포넌트이고,
  • MovieCredits: 배우의 목록을 출력하는 컴포넌트이다. (캐릭터 이름 + 프로필 이미지)
  • 타입 같은 경우는 2주차를 참고해, 아래처럼 작성을 해줬다. (detail.ts)
export type Genre = {
    id: number;
    name: string;
  };
  
  export type MovieDetail = {
    id: number;
    title: string;
    overview: string;
    release_date: string;
    runtime: number;
    poster_path: string;
    backdrop_path: string;
    vote_average: number;
    genres: Genre[];
    tagline: string;
    homepage: string;
  };
  
  export type Cast = {
    cast_id: number;
    name: string;
    character: string;
    profile_path: string | null;
  };
  
  export type MovieDetailWithCredits = MovieDetail & {
    credits: {
      cast: Cast[];
    };
  };

 

 

 

💿 MovieCredits

  • 위에서 import한 컴포넌트이고, 언급했듯이 영화 상단에 대한 부분이다.
  • Cast[] 배열을 받아서, 몇 명의 출연진을 프로필 이미지, 배우 이름, 역할 형태로 일반 소개 페이지에서 나오는 대로 구현을 해봤고, grid레이아웃으로 시각적으로 정렬을 해줬다.
  • 타입을 가져와서 cast가 출연진 목록을 전달받게 했다. 
import { Cast } from "../types/detail";

type Props = {
  cast: Cast[];
};

 

  • 레이아웃 같은 경우 스타일 설명을 하자면, max-w-6xl - 콘텐츠 가로 최대 너비 제한을 주고, mx-auto - 가운데 정렬, px-6 py-10 - 안쪽 여백을 적용하고, text-white - 관련 텍스트 색상은 white 색으로 해줬다.
<div className="max-w-6xl mx-auto px-6 py-10 text-white">
  <h2 className="text-2xl font-semibold mb-6">감독 / 출연</h2>
  • grid 구성은 각가 페이지가 옆으로 늘어나는 정도에 따라 각각 반응하도록 설정해줬다. (영상에서 한 부분.)
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 gap-4">
  • 출연진은 일부러 16명만 보여지도록 설정을 해줬고, key는 필수로 썼다.
{cast.slice(0, 16).map((person) => (
  <div key={person.cast_id} className="text-center">
  • 인물 이미지는 img 태그로~~, w-20 h-20 -> 80px, rounded-full - 동글, object-cover - 이미지 비율 유지 꽉 채우기, mx-auto - 가운데 정렬해주기
<img
  src={
    person.profile_path
      ? `https://image.tmdb.org/t/p/w185${person.profile_path}`
      : "/placeholder.jpg"
  }
  alt={person.name}
  className="w-20 h-20 rounded-full mx-auto object-cover"
/>
  • 인물 이름 및 캐릭터 이름도 적어줬고,
<p className="mt-2 text-sm font-bold">{person.name}</p>
<p className="text-xs text-gray-300">{person.character}</p>

 

 

 

💿 MovieHero

  • 배경 이미지를 큰 영역으로 사용하고, 그 위에 겹쳐서 영화 관련 정보들을 띄워줬다.
  • 영화 데이터를 movie로 받았다.
type Props = {
  movie: MovieDetail;
};
  • 배경 이미지는 img 태그로 하고, 스타일 적용을 해줬다. (이전에 했던 미션을 어느정도 참고했다.) backdrop_path를 통해 영화의 배경 이미지를, object-cover로 비율 유지하며 꽉 채우도록 했다.
<img
  src={
    movie.backdrop_path
      ? `https://image.tmdb.org/t/p/original${movie.backdrop_path}`
      : "/default-backdrop.jpg"
  }
  alt={`${movie.title} 배경`}
  className="w-full h-full object-cover"
/>
  • 배경 위에 오도록 아래처럼 작성했다. absolut, inset-0는 부모 영역 전체를 덮는, bg-black/60은 반투명 필터라고 생각하면 되고, 이는 뒷부분은 흐릿하게 보이고 글씨는 선명하게 하기 위해서 처리한 것이다. 마지막으로는 flex, items-center로 수직 가운데 정렬을 했다.
<div className="absolute inset-0 bg-black/60 flex items-center">
  • 정보를 담은 텍스트는 아래처럼 작성했다.
<div className="max-w-6xl mx-auto px-6">
  <h1 className="text-5xl font-bold">{movie.title}</h1>
  <p className="text-lg italic mt-2 text-violet-300">{movie.tagline}</p>
  <p className="mt-4">{movie.overview}</p>
  • 추가적으로는 평점, 개봉, 상영시간을 지정해줬고 space-x-4로 각각 사이에 가로 간격을 줬다.
  • 그 다음, 장르 관련될 걸 옆으로 나열시켜줬다. 
<div className="flex flex-wrap gap-2 mt-4">
  {movie.genres.map((genre) => (
    <span
      key={genre.id}
      className="bg-violet-600 px-3 py-1 rounded-full text-sm"
    >
      {genre.name}
    </span>
  ))}
</div>
  • 추가적으로는 "공식 홈페이지 바로가기" 라는 란을 만들어줬다. 공식 홈페이지 있는 경우만 링크가 그 홈페이지로 연결된다.
{movie.homepage && (
  <a
    href={movie.homepage}
    target="_blank"
    rel="noopener noreferrer"
    className="text-violet-400 underline mt-4 block"
  >
    공식 홈페이지 바로가기
  </a>
)}

 

 

 

💿 기타 스타일 적용

  • 추가적으로는 Navbar가 글씨만 보여서 스타일을 좀 수정을 해줬다.
  • NavLink 안 스타일은 px-4 py-2로 좌우, 위아래 패딩을 지정해주고, rounded-full, border, text-sm 지정해주고, transition-all duration-200을 줘서 hover/active를 할 때, 자연스럽게 넘어가게 했다. (200ms 애니메이션 적용.)
  • Navbar가 활성화 되면, 'bg-[#FF3D6E] text-white border-[#FF6699]' -> 자주색, 흰색 글씨, border는 연한 핑크 계열
  • Navbar가 비활성화 되면, 'text-[#FFFFFF] border-[#FF3D6E] hover:bg-[#FF3D6E] hover:text-white' -> 기본은 흰색 글씨, border는 자주색, 호버 시 자주색 배경 채우기, text 색상은 흰색으로 되게끔 했다.
import { NavLink } from "react-router-dom";

const LINKS = [
  { to: '/', label: '홈' },
  { to: '/movies/popular', label: '인기 영화' },
  { to: '/movies/now_playing', label: '상영 중' },
  { to: '/movies/top_rated', label: '평점 높은' },
  { to: '/movies/upcoming', label: '개봉 예정' },
];

export const Navbar = () => {
  return (
    <div className="flex gap-3 px-6 py-4 flex-wrap">
      {LINKS.map(({ to, label }) => (
        <NavLink
          key={to}
          to={to}
          className={({ isActive }) =>
            `px-4 py-2 rounded-full border transition-all duration-200 text-sm
            ${
              isActive
                ? 'bg-[#FF3D6E] text-white border-[#FF6699]'
                : 'text-[#FFFFFF] border-[#FF3D6E] hover:bg-[#FF3D6E] hover:text-white'
            }`
          }
        >
          {label}
        </NavLink>
      ))}
    </div>
  );
};