미션 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>
);
};
'UMC 8th Web 워크북' 카테고리의 다른 글
🍎 서버 환경 관련 Ot & Swagger 간단 활용 (umc 정리) (0) | 2025.04.13 |
---|---|
🍎 Custom-hook (4주차 UMC 강의 정리 + @) (0) | 2025.04.12 |
💿 로딩 에러 처리 및 페이지 라우팅 (0) | 2025.04.06 |
🎞️ 영화 리스트 데이터 불러오기 (UMC 3주차 강의 정리) (0) | 2025.04.06 |
📜 useEffect 공식문서 간단 정리 (0) | 2025.04.05 |