React JS 마스터클래스(NOMFLIX CLONE) - Home Screen

짜스의 하루 ·2024년 6월 26일

API

API 받아올 사이트 에서 API를 받아올 예정이다.


회원가입을 하고 API를 받기 위해 몇가지 정보(왜 받는지 이런 것들) 입력하면 이렇게 API를 받을 수 있게 된다.

API 를 사용하기 위해 --> React Query를 사용해보도록 하겠다!

메인 /home 화면에는 현재 상영중인 영화를 화면에 뿌려줄 예정이다.
이에 api.ts 파일을 하나 생성해서 --> 필요한 API를 받아오면 된다.

  • fetch 함수: 주어진 URL로 HTTP 요청을 보내는 함수이다.
    ${BASE_PATH}/movie/now_playing?api_key=${API_KEY} : 상영 중인 영화를 가져오기 위한 API 엔드포인트이다. 여기에 API_KEY를 포함시켜 요청한다.

  • then 메서드: fetch 함수의 결과로 반환된 Promise 객체에서 응답(response) 을 받아서 JSON 형식으로 변환하게 된다.

간단하게 동작 과정을 살펴보자면,

  • fetch 함수가 BASE_PATH와 경로 /movie/now_playing에 api_key 쿼리 파라미터를 추가하여 요청을 보낸다.
  • API 응답이 오면 response.json()을 통해 JSON 형식으로 변환된 데이터를 반환하고,
    이 데이터에는 현재 상영 중인 영화 목록이 포함되어 있다.

now_playing을 사용할 Home 컴포넌트에서 useQuery 훅을 사용하여 now_playing 영화를 비동기적으로 가져오고 상태를 관리해야 한다.

const { data, isLoading } = useQuery(['movie', 'nowPlaying'], getMovies);

useQuery(['movie', 'nowPlaying'], getMovies): useQuery는 데이터 가져오기 및 캐싱을 위한 훅이다.

  • 첫 번째 인자 --> 쿼리 키. 배열 ['movie', 'nowPlaying']는 이 쿼리의 고유 키로 사용된다.
    이를 통해 React Query는 동일한 쿼리를 식별하고 캐싱, 갱신 등의 기능을 제공한다.

  • 두 번째 인자--> getMovies 함수
    데이터 가져오기를 수행하는 함수로, 이전 코드에서 정의한 TMDb API로부터 현재 상영 중인 영화를 가져오는 함수이다.

반환값: useQuery는 객체를 반환하며, 그 객체에서 필요한 데이터를 구조 분해 할당(destructuring)으로 가져온다.

  • data: 쿼리의 결과로 반환된 데이터이다. getMovies 함수의 반환 값, 즉 현재 상영 중인 영화 정보가 여기에 저장된다.
  • isLoading: 데이터가 로딩 중인지 여부를 나타내는 boolean 값. 쿼리가 진행 중일 때 true, 완료되면 false이다.

간단하게 설명하자면, useQuery를 통해서 movies를 가져오는데, 그중에서도 nowPlaying을 가져온다! 어디서? getMovies함수에서 --> 그리고 가져온 데이터를 data에 저장한다. 라고 이해하면 될 것 같다 !


Home Screen

TypeScript에게 어떤 데이터를 받아올 건지, 타입을 정해서 알려주어야 한다.

사이트에서 제공하는 API들을 가지고, 어떤 API를 불러올 건지 정하고, 그것을 타입과 함께 지정해두면
--> 우리가 나중에 사용할 때 자동완성의 기능을 얻을 수 있다.

interface로 받아올 데이터들의 타입을 지정해둔뒤, useQuery로 받아오는 데이터에게 어떤 타입의 데이터인지 알려주면 된다.

이후 이 데이터들을 가지고 화면에 뿌려주면 된다.


이제 Home 컴포넌트를 꾸며보겠다.

<IGetMoviesResult> : 타입스크립트를 사용하여 data의 형태를 지정한다. IGetMoviesResult 는 API 응답의 타입을 정의한 인터페이스를 의미한다.

  • isLoading ?: 로딩 중일 때는 Loader 컴포넌트가 렌더링되어 "Loading" 메시지를 표시한다.
  • 데이터 렌더링:
    data?.results[0].title : data가 존재하고 results 배열에 데이터가 있으면, 첫 번째 영화의 제목을 표시한다.
    data?.results[0].overview : data의 첫 번째 영화의 개요를 표시한다.
  • Banner: 영화의 배너 영역을 표시하는 컴포넌트로, 영화의 제목과 개요를 포함한다.

간단하게 컴포넌트의 전체 흐름를 살펴보자면,

  • 데이터 가져오기: useQuery 훅을 사용해 getMovies 함수로부터 현재 상영 중인 영화 데이터를 가져온다.
  • 로딩 상태 처리: isLoading이 true일 때는 로딩 중 메시지를 표시한다.
  • 데이터 렌더링:
    데이터를 가져오는 데 성공하면 data가 영화 데이터를 포함하게 되고, 이를 사용해 첫 번째 영화의 제목과 개요를 배너로 표시한다.

현재 화면에서는 이렇게 제목, 개요가 화면에 뿌려졌다. 이제 이에 해당하는 backdrop_path 를 뿌려주어야 한다.


backdrop_path 이미지 뿌리기

TMDb API에서 제공하는 이미지 경로를 완성하는 데 사용하기 위해 makeImagePath함수를 생성했다. 이 함수는 이미지의 식별자와 포맷을 받아, 완전한 URL을 생성하여 반환한다.

export function makeImagePath(id: string, format?: string) {
  return `https://image.tmdb.org/t/p/${format ? format : 'original'}/${id}`;
}
  • id: string : 이미지의 고유 식별자--> 주로 이미지 파일의 이름이나 경로 일부를 나타낸다.
  • format?: string : 이미지 포맷 또는 사이즈--> 선택적(optional) 매개변수로, 주어지지 않으면 기본값 'original'을 사용한다.

return

  • https://image.tmdb.org/t/p/ : TMDb의 이미지 기본 URL
  • ${format ? format : 'original'}: 조건부 연산자(? :)를 사용하여 format 값이 주어지면 그 값을 사용하고, 주어지지 않으면 'original'을 사용한다. 이 부분은 이미지 포맷을 지정한다
  • ${id}: 이미지 식별자
    --> 최종적으로 경로가 https://image.tmdb.org/t/p/{format}/{id} 가 된다.

이제 makeImagePath함수를 사용해서 이미지를 불러와보자

const Banner = styled.div<{ bgPhoto: string }>
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-left: 60px;
  background-image: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 1)),
    url(${(props) => props.bgPhoto});
  background-size: cover;
;

 <Banner bgPhoto={makeImagePath(data?.results[0].backdrop_path || '')} >
         <Title>{data?.results[0].title}</Title>
         <Overview>{data?.results[0].overview}</Overview>
</Banner>

makeImagePath(data?.results[0].backdrop_path || '') : data?.results[0].backdrop_path 는 현재 상영 중인 첫 번째 영화의 배경 이미지 경로이다.
이 값이 null 또는 undefined일 경우 빈 문자열 ''을 기본값으로 사용하게 된다.

makeImagePath 함수는 data?.results[0].backdrop_path를 받아서 완전한 이미지 URL을 반환한다.
--> 만약 data?.results[0].backdrop_path가 존재하지 않으면 빈 문자열을 넣어 빈 URL을 생성하므로, 실제 배경 이미지는 설정되지 않는다.

이를 Banner의 props로 넘겨주었기 때문에
<{ bgPhoto: string }>: bgPhoto라는 문자열 타입의 속성을 받는다고 명시한다. 이 속성은 배경 이미지 URL로 사용되게 된다.

--> Banner 컴포넌트는 bgPhoto 속성으로 완전한 이미지 URL을 받아 배경 이미지로 설정한다. 또한 배경 이미지와 그라데이션 효과를 조합하여 배너를 화면에 표시하게 된다.


이렇게 이미지를 정확하게 불러올 수 있었다!


Slider

넷플릭스를 보면 메인 페이지 아래에 slider로 영화를 테마나 주제별로 모아서 보여준다.
이러한 slider를 만들어보자.

  • Slider 내부에 AnimatePresence와 Row를 사용하여 애니메이션 효과를 준다.
  • Row 컴포넌트에 variants, initial, animate, exit, key, transition 등의 props를 통해 애니메이션 상태와 트랜지션을 정의한다.
  • Row 내부에는 Box 컴포넌트를 6개 배치하여 영화 박스를 표시한다.
const rowVariants = {
  hidden: {
    x: window.outerWidth + 10,
  },
  visible: {
    x: 0,
  },
  exit: {
    x: -window.outerWidth - 10,
  },
};

rowVariants : Row 컴포넌트의 애니메이션 변형을 정의
상태:

  • hidden: 슬라이드가 화면 밖 오른쪽에 위치하도록 설정 (x: window.outerWidth + 10)
  • visible: 슬라이드가 화면에 완전히 표시되도록 설정. (x: 0)
  • exit: 슬라이드가 화면 밖 왼쪽으로 나가도록 설정. (x: -window.outerWidth - 10)

이렇게 코드를 작성하게 되면,

간단하게 1-6번이 적힌 Box가 슬라이드 애니메이션으로 화면에 나타나는 것을 확인할 수 있다

현재 문제점을 찾아보면,
1. 새로고침을 했을 때, 왼쪽에서부터 오는 애니메이션이 보인다는 점
2. 두번 연속으로 화면을 로딩했을 때,
먼저 실행중이였던 box가 exit하기 전에 또 로딩이 되어서 다음 box까지도 exit하려는 현상이 발생하게 된다.

이를 막기 위해서 !

const [index, setIndex] = useState(0);
const [leaving, setLeaving] = useState(false);

index : 현재 슬라이더의 페이지(또는 슬라이더에서 보여줄 영화 목록의 인덱스)를 나타낸다.
초기값: 0으로 설정되어 있어 처음에는 첫 번째 슬라이드가 표시됩니다.

leaving : 현재 슬라이더 애니메이션이 진행 중인지를 나타낸다. 애니메이션이 끝나기 전에는 새로운 애니메이션이 시작되지 않도록 제어한다.

increaseIndex 함수 :

  • data가 있는지 확인: data가 존재하는 경우에만 슬라이더 인덱스를 증가시킨다.
    --> data가 없으면 동작하지 않는다.
  • 애니메이션 진행 중 여부 확인: leaving 상태를 체크하여 애니메이션이 진행 중이면 함수 실행을 중단한다. 이는 중복 애니메이션을 방지한다.
  • 애니메이션 시작: toggleLeaving을 호출하여 애니메이션이 시작됨을 표시하고 leaving 상태를 true로 변경한다.
  • 인덱스 증가: setIndex 함수를 사용하여 현재 인덱스를 1 증가시킨다 --> 이를 통해 다음 슬라이드로 넘어간다.

toggleLeaving 함수

  • leaving 상태를 반전시키는 함수--> true를 false로, false를 true로 변경한다.
  • 슬라이드 애니메이션의 시작과 종료를 제어한다.
    애니메이션이 시작될 때와 애니메이션이 완료될 때 호출되어 leaving 상태를 전환한다.

이렇게 정의한 값을

<AnimatePresence initial={false} onExitComplete={toggleLeaving}>

initial={false} : 처음 렌더링될 때 애니메이션을 적용할지 여부를 결정한다
--> false로 설정하면 컴포넌트가 처음으로 마운트될 때는 애니메이션 없이 렌더링된다.

onExitComplete={toggleLeaving} : 애니메이션이 끝나고 요소가 제거되면 toggleLeaving이 실행되어 leaving 상태를 다시 false로 변경
--> 다음 애니메이션이 정상적으로 시작될 수 있게 한다.

간단하게 코드 흐름을 살펴보자면
1 . 슬라이드 애니메이션 시작:

  • increaseIndex 함수가 호출되면, 현재 애니메이션이 진행 중인지(leaving)를 체크
    --> 진행 중이지 않으면(leaving이 false일 때) toggleLeaving을 호출하여 leaving을 true로 변경한다.
    --> index가 증가하고 새로운 슬라이드가 표시되면서 애니메이션이 시작된다.

2 .애니메이션 중:

  • 슬라이드가 애니메이션을 통해 이동하는 동안 leaving 상태는 true로 유지된다.
    --> 이 상태에서는 새로운 슬라이드 애니메이션이 시작되지 않는다.

3 .애니메이션 완료:

  • 애니메이션이 끝나고 슬라이드 요소가 DOM에서 제거되면 AnimatePresence의 onExitComplete 속성에 지정된 toggleLeaving 함수가 호출된다.
    --> 이 함수는 leaving 상태를 false로 변경하여, 다음 슬라이드 애니메이션이 정상적으로 시작될 수 있게 준비한다.

그럼 위의 두개의 문제점이 해결되는 것을 볼 수 있다!
이제 영화의 이미지를 불러와보자

Slider Image 불러오기

이전 [1,2,3,4,5,6] 배열을 불러와서 슬라이더를 만들었던 곳에서 이미지를 불러오도록 하려고 한다.

 {data?.results .slice(1)
       .slice(offset * index, offset * index + offset)
       .map((movie) => (
	<Box key={movie.id}
     bgPhoto={makeImagePath(movie.backdrop_path, 'w500')}
		/>
))}

data?.results.slice(1) : 먼저, results의 첫 번째 항목을 제외한 나머지를 가져오는데, 첫번째 항목은 이미 Banner에서 사용했기 때문에 제외시켰다.

.slice(offset * index, offset * index + offset) :

  • offset: 한 슬라이드에 표시할 영화의 수 (위에서 6으로 지정해둠)
  • index: 현재 슬라이더의 페이지 인덱스입니다.
  • offset * index: 현재 슬라이더 페이지의 시작 위치
  • offset * index + offset: 현재 슬라이더 페이지의 끝 위치

0 페이지 (index가 0일 때):

  • 슬라이싱 범위: slice(6 0, 6 0 + 6) -> slice(0, 6)
  • 결과: 0, 1, 2, 3, 4, 5 인덱스의 영화 렌더링

1 페이지 (index가 1일 때):

  • 슬라이싱 범위: slice(6 1, 6 1 + 6) -> slice(6, 12)
  • 결과: 6, 7, 8, 9, 10, 11 인덱스의 영화 렌더링
.map((movie) => (
  <Box
    key={movie.id}
    bgPhoto={makeImagePath(movie.backdrop_path, 'w500')}
  />
))
  • 잘라낸 영화 목록을 map을 통해 Box 컴포넌트로 변환하여 렌더링한다.
  • key 속성: 각 Box에 고유한 key를 부여하기 위해 영화의 id를 사용
  • bgPhoto 속성: makeImagePath 함수를 사용하여 영화의 backdrop_path(백드롭 이미지 경로)를 완전한 URL로 변환한 후 Box의 bgPhoto로 설정한다.

이렇게 코드를 작성하게 되면,

이와 같이 영화의 이미지가 잘 나오는 것을 확인할 수 있다(background에 관한 css는 따로 주었다)

const increaseIndex = () => {
    if (data) {
      if (leaving) return;
      const totalMovies = data?.results.length - 1;
      const maxIndex = Math.ceil(totalMovies / offset) - 1;
      toggleLeaving();
      setIndex((prev) => (prev === maxIndex ? 0 : prev + 1));
    }
  };

const totalMovies = data?.results.length - 1; :

  • 전체 영화 개수를 계산한다 --> 배너로 사용되는 첫 번째 영화를 제외하기 위해 - 1을 한다.

const maxIndex = Math.ceil(totalMovies / offset) - 1; :

  • 슬라이더의 최대 페이지 인덱스를 계산합니다.
  • totalMovies / offset : 한 슬라이드에 표시할 영화 수로 나누어 필요한 총 페이지 수를 계산한다.
  • Math.ceil : 소수점을 올림 처리하여 페이지 수를 정수로 만든다.
  • -1: 인덱스는 0부터 시작하므로 최대 인덱스를 계산하기 위해 1을 뺀다.

setIndex((prev) => (prev === maxIndex ? 0 : prev + 1)); :

  • 슬라이더의 현재 페이지 인덱스를 증가시킵니다.
  • prev === maxIndex : 현재 인덱스가 최대 인덱스에 도달한 경우 0으로 되돌린다(첫 번째 페이지로 순환).
  • prev + 1 : 그렇지 않으면 인덱스를 1 증가시킨다.

이렇게 정의하게 되면,
이렇게 영화가 다 나열이 되고도 다시 인덱스 0으로 이동해서 다시 영화가 렌더링 되는 것을 확인할 수 있다.


Slider image animation 주기

이렇게 마우스를 올린 영화에 scale의 크기를 변화를 주는 animation과
맨 오른쪽과 왼쪽의 animation의 위치
(나머지는 가운데에서 애니메이션이 이루어지는데, 그렇게 되면 맨 오른쪽과 왼쪽은 이미지가 짤리게 된다)

고로 왼쪽 이미지 --> 애니메이션이 가운데 오른쪽으로 이루어질 수 있도록
오른쪽 이미지 --> 애니메이션이 가운데 왼쪽으로 이루어질 수 있도록
이렇게 수정을 해보도록

const BoxVariants = {
  normal: {
    scale: 1,
  },
  hover: {
    scale: 1.3,
    y: -50,
    transition: { type: 'tween', duration: 0.3, delay: 0.5 },
  },
};

업로드중..

이렇게 적용하면 해결 가능!

업로드중..
<Box/> 컴포넌트의 첫번째 요소와 마지막 요소에 각각 transform-origin(transform)이 적용되는 기준점(원점)을 지정) 속성을 적용해주면 이미지가 짤리지 않고 적용이 되는 것을 확인할 수 있다.

profile
2024. 01. 02 ~ 백앤드 공부 시작, 2024. 04.01 ~ 프론트 공부 시작

0개의 댓글