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 형식으로 변환하게 된다.
간단하게 동작 과정을 살펴보자면,
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)으로 가져온다.
간단하게 설명하자면, useQuery를 통해서 movies를 가져오는데, 그중에서도 nowPlaying을 가져온다! 어디서? getMovies함수에서 --> 그리고 가져온 데이터를 data에 저장한다. 라고 이해하면 될 것 같다 !
TypeScript에게 어떤 데이터를 받아올 건지, 타입을 정해서 알려주어야 한다.
사이트에서 제공하는 API들을 가지고, 어떤 API를 불러올 건지 정하고, 그것을 타입과 함께 지정해두면
--> 우리가 나중에 사용할 때 자동완성의 기능을 얻을 수 있다.
interface로 받아올 데이터들의 타입을 지정해둔뒤, useQuery로 받아오는 데이터에게 어떤 타입의 데이터인지 알려주면 된다.
이후 이 데이터들을 가지고 화면에 뿌려주면 된다.
이제 Home 컴포넌트를 꾸며보겠다.

<IGetMoviesResult> : 타입스크립트를 사용하여 data의 형태를 지정한다. IGetMoviesResult 는 API 응답의 타입을 정의한 인터페이스를 의미한다.
isLoading ?: 로딩 중일 때는 Loader 컴포넌트가 렌더링되어 "Loading" 메시지를 표시한다. data?.results[0].title : data가 존재하고 results 배열에 데이터가 있으면, 첫 번째 영화의 제목을 표시한다.data?.results[0].overview : data의 첫 번째 영화의 개요를 표시한다.간단하게 컴포넌트의 전체 흐름를 살펴보자면,

현재 화면에서는 이렇게 제목, 개요가 화면에 뿌려졌다. 이제 이에 해당하는 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'을 사용한다. 이 부분은 이미지 포맷을 지정한다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를 만들어보자.

const rowVariants = {
hidden: {
x: window.outerWidth + 10,
},
visible: {
x: 0,
},
exit: {
x: -window.outerWidth - 10,
},
};
rowVariants : Row 컴포넌트의 애니메이션 변형을 정의
상태:
이렇게 코드를 작성하게 되면,
간단하게 1-6번이 적힌 Box가 슬라이드 애니메이션으로 화면에 나타나는 것을 확인할 수 있다

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

const [index, setIndex] = useState(0);
const [leaving, setLeaving] = useState(false);
index : 현재 슬라이더의 페이지(또는 슬라이더에서 보여줄 영화 목록의 인덱스)를 나타낸다.
초기값: 0으로 설정되어 있어 처음에는 첫 번째 슬라이드가 표시됩니다.
leaving : 현재 슬라이더 애니메이션이 진행 중인지를 나타낸다. 애니메이션이 끝나기 전에는 새로운 애니메이션이 시작되지 않도록 제어한다.
increaseIndex 함수 :
toggleLeaving 함수
이렇게 정의한 값을
<AnimatePresence initial={false} onExitComplete={toggleLeaving}>
initial={false} : 처음 렌더링될 때 애니메이션을 적용할지 여부를 결정한다
--> false로 설정하면 컴포넌트가 처음으로 마운트될 때는 애니메이션 없이 렌더링된다.
onExitComplete={toggleLeaving} : 애니메이션이 끝나고 요소가 제거되면 toggleLeaving이 실행되어 leaving 상태를 다시 false로 변경
--> 다음 애니메이션이 정상적으로 시작될 수 있게 한다.
간단하게 코드 흐름을 살펴보자면
1 . 슬라이드 애니메이션 시작:
2 .애니메이션 중:
3 .애니메이션 완료:
그럼 위의 두개의 문제점이 해결되는 것을 볼 수 있다!
이제 영화의 이미지를 불러와보자
이전 [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) :
0 페이지 (index가 0일 때):
1 페이지 (index가 1일 때):
.map((movie) => (
<Box
key={movie.id}
bgPhoto={makeImagePath(movie.backdrop_path, 'w500')}
/>
))
이렇게 코드를 작성하게 되면,

이와 같이 영화의 이미지가 잘 나오는 것을 확인할 수 있다(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; :
const maxIndex = Math.ceil(totalMovies / offset) - 1; :
totalMovies / offset : 한 슬라이드에 표시할 영화 수로 나누어 필요한 총 페이지 수를 계산한다.Math.ceil : 소수점을 올림 처리하여 페이지 수를 정수로 만든다.setIndex((prev) => (prev === maxIndex ? 0 : prev + 1)); :
prev === maxIndex : 현재 인덱스가 최대 인덱스에 도달한 경우 0으로 되돌린다(첫 번째 페이지로 순환).prev + 1 : 그렇지 않으면 인덱스를 1 증가시킨다.이렇게 정의하게 되면, 
이렇게 영화가 다 나열이 되고도 다시 인덱스 0으로 이동해서 다시 영화가 렌더링 되는 것을 확인할 수 있다.

이렇게 마우스를 올린 영화에 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)이 적용되는 기준점(원점)을 지정) 속성을 적용해주면 이미지가 짤리지 않고 적용이 되는 것을 확인할 수 있다.