- 리액트 쿼리로 데이터 페칭하기
- api 데이터의 interface 만들기
- Home.tsx에 스타일 컴포넌트 생성
- 슬라이더 영화 배너 만들기
- 배너 속 박스에 애니메이션 넣기
- 상세정보 모달 창 만들기
그 외: router.ts, api.ts, theme.ts. createGlobalStyle

const Banner = styled.div<{ bgPhoto: string }>`
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px;
background-image: linear-gradient(rgba(0, 0, 0, 0.1),
rgba(0, 0, 0, 0.8)), //아래로 점점 짙어짐
url(${(props) => props.bgPhoto});
background-size: cover;
`;
<Banner
onClick={increaseIndex}
bgPhoto={makeImagePath(data?.results[1].backdrop_path || "")}
>
<Title>{data?.results[1].title}</Title>
<Overview>{data?.results[1].overview}</Overview>
</Banner>
//이미지 url 함수 만들기
📂 utilities.ts
function makeImagePath(id: string, format?: string) {
return `https://image.tmdb.org/t/p/${format ? format : "original"}/${id}`;
}
export default makeImagePath;
// Cover 컴포넌트 만들기 - 이미지를 배경으로 가진다.
const Cover = styled.div`
width: 100%;
height: 60vh;
background-size: cover;
background-position: center center;
`
// 사용하기
<Cover
style={{backgroundImage:
`url(${makeImagePath(moive.poster.path, "w500")})`}}
/>
Row에 여러 Box들이 보이고 (한 화면에 5~6개) Box는 영화의 이미지가 보이는 형태.

Slider 안에 배너를 여러개 만들기. 배너가 Row
Row 안에 6개의 Box 보여주기. Box는 영화 이미지
Box는 reactQuery의 data에서 results를 map한다.
slice(2): 첫번째 요소를 메인 화면에 썼기에 그부분 제외하고 시작.const offset = 6; 한 배너에 6개 박스를 보여주기 위함const [index, setIndex] = useState(0) 슬라이드 배너의 page 같은 개념으로 씀. 1page에 6개.data?.results
.slice(2)
.slice(offset * index, offset * index + offset)
// index의 초기값은 0. slice(0, 6) -> slice(6, 12)
//results 배열을 6개씩 잘라먹기
.map() ...
const Slider = styled.div`
position: relative;
top: -200px;
`; // 너무 아래에 있어서 위로 살짝 올려줌
const Row = styled(motion.div)`
display: grid;
gap: 5px;
width: 100%;
grid-template-columns: repeat(6, 1fr);
position: absolute;
`; //영화 박스들 담기
const Box = styled(motion.div)<{ bgPhoto: string }>`
height: 200px;
background-image: url(${(props) => props.bgPhoto});
background-size: cover;
background-position: center center;
&:first-child {
transform-origin: left;
}
/* 영화 박스가 커질 때 첫번째와 마지막 요소의 기준점을 따로 잡아줌 */
&:last-child {
transform-origin: right;
}
`;
const Info = styled(motion.div)`
padding: 20px;
background-color: ${(props) => props.theme.black.lighter};
opacity: 0;
position: absolute;
width: 100%;
bottom: 0;
h4 {
text-align: center;
font-size: 14px;
}
`;
//Info는 기본적으로 안보이게 해놓고 hover시 보이게 만듬
구현부
<Slider>
{/* AnimatePresence initial={false} 처음 mount 효과 없앰*/}
<AnimatePresence initial={false} onExitComplete={toggleLeaving}>
<Row
variants={rowVariants}
initial="hidden"
animate="visible"
exit="exit"
transition={{ type: "tween", duration: 1 }}
key={index}
>
{data?.results
.slice(2)
.slice(offset * index, offset * index + offset)
.map((movie) => (
<Box
layoutId={movie.id + ""}
onClick={() => onBoxClicked(movie.id)}
transition={{ type: "tween" }}
variants={boxVariants}
initial="normal"
whileHover="hover"
bgPhoto={makeImagePath(movie.backdrop_path, "w500")}
key={movie.id}
>
<Info variants={infoVariants}>
<h4>{movie.title}</h4>
</Info>
</Box>
))}
</Row>
</AnimatePresence>
</Slider>
const [leaving, setLeaving] = useState(false);
const toggleLeaving = () => setLeaving((prev) => !prev);
const increaseIndex = () => {
if (data) {
if (leaving) return; // true면 아무것도 안하게 그냥 return
toggleLeaving(); //
const totalMoives = data?.results.length - 1; //메인 배너 한개뺌
const maxIndex = Math.floor(totalMoives / offset) - 1;
// page 계산. 영화개수 / 6. page는 0부터 시작해서 - 1
setIndex((prev) => (prev === maxIndex ? 0 : prev + 1));
// 버튼 연달아 누를때 슬라이더가 겹치는 문제 해결을 위해서 작성.
// index가 변화해야 슬라이더 효과가 생김. 0->1->2->0
// 계속 true면 애니메이션이 안되니까 onExitComplete prop 사용
// onExitComplete: exit 끝났을 때 실행되는 함수 받음.
}
};
구현부
<AnimatePresence initial={false} onExitComplete={toggleLeaving}>
영화를 누르면 홈 화면에서! 새로운 BigBox가 보이면서 url이 movieId로 변경되어야함.
(중첩 라우터)
박스를 누르면 Overlay와 BigBox 나타나게 하기.
Overlay는 박스 바깥 부분으로 누르면 다시 BigBox 모달창이 사라지게 함 (url 홈으로)
const navigate = useNavigate();
const onBoxClicked = (movieId: number) => {
navigate(`/movies/${movieId}`);
};
// Box에서 api의 movie.id를 받아서 url를 변경시킨다.
<Box
onClick={() => onBoxClicked(movie.id)}
/>
// Box에 onClick에 전달. 누르면 movie.id 담긴 url로 navigate
const overlayClicked = () => {
navigate("/");
};
// url을 "/"로 변경시켜서 홈 화면으로. (모달 창 닫기)
모달 창
박스를 누르면 모달창이 뜨고, url이 해당 영화의 movie.id로 바뀐다.
url이 movieId 이용해서 바뀐 것을 확인함. clickedMovie에 특정 영화의 api 정보담김.
위는 이미 홈화면에서 불러왔던 fetcher 함수의 api.
movie.id 를 이용한 api주소로 fetcher한 <Modal> 컴포넌트가 따로 있음.
<Modal> 컴포넌트에 prop으로 movie.id에 해당하는 숫자를 넘겨준다.
const bigMovieBoxMatch = useMatch("/movies/:movieId");
// url 일치 확인용. parmas 객체 안에 movieId 담긴다.
const clickedMovie =
bigMovieBoxMatch?.params.movieId && (data?.results.find(
(movie) => movie.id + "" === bigMovieBoxMatch.params.movieId));
// Box 영화 눌렀을 때 그 영화 id에 맞는 api results 만 꺼내옴
bigMovieBoxMatch &&
<BigBox layoutId={bigMovieBoxMatch.params.movieId}>
{clickedMovie && (
<>
<img
alt={clickedMovie.title}
src={makeImagePath(clickedMovie.poster_path, "w500")}
></img>
<BigBoxContents>
<h2>{clickedMovie.title}</h2>
<div>{clickedMovie.overview}</div>
<span>출시일: {clickedMovie.release_date}</span>
<span>평점: {clickedMovie.vote_average}</span>
<Modal
id={
bigMovieBoxMatch?.params.movieId || clickedMovie?.id
}
/>
</BigBoxContents>
</>
)}
</BigBox>