다섯째주 #23 React Js - N*tfilx 클론

김선은·2023년 6월 15일

홈파트

  1. 리액트 쿼리로 데이터 페칭하기
  2. api 데이터의 interface 만들기
  3. Home.tsx에 스타일 컴포넌트 생성
  4. 슬라이더 영화 배너 만들기
  5. 배너 속 박스에 애니메이션 넣기
  6. 상세정보 모달 창 만들기
    그 외: 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>

이미지 api 함수

//이미지 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>

toggleLeaving: 슬라이드 효과 중첩 방지

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>

https://codesandbox.io/s/react-js-master-9-nomflix-wykv89

profile
기록은 기억이 된다

0개의 댓글