영화 데이터 불러오기를 통해 데이터를 호출하는 방법에 대해서 알아보면서 그라데이션 효과를 넣은 UI를 만들어봤는데 이번에는 추가적으로 데이터를 불러올 때(데이터 용량이 클 때, 로딩 스페너를 보여준다거나 스켈레톤 UI 등) 로딩이나 잘못 불러왔을 때의 에러 처리 등을 해볼 생각이다. 추가적으로, 처음 마운트에서 useEffect가 사용되는 것은 맞지만 데이터를 불러오고, dependency 같은 의존성 배열 부분이 변경되면 useEffect가 리렌더링 될 때 다시 동작을 한다는 것을 알아야 한다.
본격적으로 실습으로 들어가보자.
일단은, 기존의 MoviePage는 영화 페이지가 성공적으로 받아온다의 케이스로만 처리를 해줬었기 때문에 수정을 해줘야 한다.
📌 로딩, 에러 처리 (try-catch-finally, spinner 등)
먼저, 로딩 처리를 해줄 것이다.
1. 로딩 상태
const [isPending, setIsPending] = useState(false);
2. 에러 상태
const [isError, setIsError] = useState(false);
- useEffect의 경우, 현재 api를 불러오고 있기는 하지만, 이 경우는 무조건 데이터를 성공적으로 가져왔을 때만 작동한다.
- 그래서 추가적인 작업으로, try-catch문을 사용한다.
- catch문 같은 경우는 에러 메세지를 띄우고 싶은 건 아니고 단순 에러 처리기 때문에 error를 따로 설정해주진 않았다.
- fetchMovies를 가져오는 시점에는 로딩 상태가 데이터를 호출하는 중이니까 setIsPending(true); 인 상태일 것이다.
- try까지 해서 데이터를 정상적으로 호출했고, setMovies에서 데이터를 잘 담기는 것을 볼 수 있을 것이다.
- 그럼 잘 담겼기 때문에 setIsPending(false);를 해주면 된다.
- 그 다음, catch 에러 같은 경우는 setIsError(true);, setIsPending(false); 로 적어주면 된다.
- 근데, 매번 setIsPending(false);는 여러 곳에서 쓰이니까 finally를 추가해서 거기에 넣어주면 공통으로 작용한다.
- 이렇게 로딩과 에러처리가 끝났다.
import { useEffect, useState } from "react"
import axios from "axios";
import { Movie, MovieResponse } from "../types/movie";
import MovieCard from "../components/MovieCard";
export default function MoviePage() {
const [movies, setMovies] = useState<Movie[]>([]);
// 1. 로딩 상태
const [isPending, setIsPending] = useState(false);
// 2. 에러 상태
const [isError, setIsError] = useState(false);
useEffect(() => {
const fetchMovies = async () => {
setIsPending(true);
try {
const {data} = await axios.get<MovieResponse>(
`https://api.themoviedb.org/3/movie/popular?language=en-US&page=1`,
{
headers: {
Authorization: `Bearer ${import.meta.env.VITE_TMDB_KEY}`,
}
}
);
setMovies(data.results);
} catch {
setIsError(true);
} finally {
setIsPending(false);
}
};
fetchMovies();
}, []);
console.log(movies[0]?.adult);
return (
<div className='p-10 grid gap-2 grid-cols-2 sm:grid-cols-3 md:grid-cols-4
lg:grid-cols-5 xl:grid-cols-6'>
{/* 실제 화면에 따라 달라지도록 세팅. 반응형 확인 가능. 패딩 10, gap 2*/}
{movies && movies.map((movie) =>
<MovieCard key={movie.id} movie={movie}/>
)}
</div>
)
}
이제, LoadingSpinner.tsx를 만들어보려고 한다. 잘 화면에 뜨는지 보기 위해 임시로,
if (!isPending) {
return <LoadingSpinner />;
}
이런 식으로 처리를 해주고 진행을 한다.
export const LoadingSpinner = () => {
return (
<div className='size-12 animate-spin rounded-full border-6
border-t-transparent border-[#b2dab1]' // 동그라미 형태로 돌아가는 스피너가 생성.
role='status'
>
<span className="sr-only">로딩 중...</span>
</div> // 시각 장애인 분들 같은 분들이나 스크린 리더에 의존하는 사람들을 고려하기 위해
// 음성 인식이 되게 해주는 처리도 해줄 수 있음.
)
}
그럼, 구현이 정상적으로 됐을 것이다. 그럼 다른 작업을 위해 MoviePage에서 스피너는 주석 처리를 했다.
에러 처리관련해서는
if (isError) {
return (
<div>
<span className='text-red-500 text-2xl'>에러가 발생했습니다.</span>
</div>
)
}
이런 식으로 MoviePage에 텍스트가 뜨도록 처리를 해줬다.
📌 페이지네이션
이제 page를 동적으로 받아오기 위해서 페이지네이션 작업을 해준다.
- 동적으로 페이지 처리를 할 수 있게 url 변경
... ?language=en-US&page=${page}`
- 페이지네이션 관련 UI를 만들어줘야 한다. <></> 부모 태크를 만들어주고, 페이지네이션을 관리하는 컴포넌트를 따로 만들어주는 것이 가능한데 강의 상에서는 인라인으로 처리를 할 것이다.
- 영화 데이터 같은 경우는 페이지가 달라지면 데이터가 바껴야 되는데 그걸 처리하기 위해서 의존성 배열 ('[]')에 page를 추가해준다.
- 1페이지 이하로 안 떨어지게 하는 게 가장 베스트이다. 그에 대한 처리를 위해 disabled 속성을 이용해준다.
- 아래 코드는 page가 1일 때는 동작을 안 하게 할 수 있다.
disabled={page === 1}
- 추가적으로는 UI를 꾸며준다.
- disabled 되는 1 페이지에 스타일을 따로 적용해줘서 시각적으로 구분 가능하고 눌리지 않게 설정을 해준다.
- 그리고, 다음으로 넘어가는 페이지는 잘 작동되야 하기에 disabled 관련 스타일을 빼주고, cursor-pointer를 추가해준다.
// if (!isPending) {
// return <LoadingSpinner />;
// }
- 페이지를 넘길 때 데이터를 불러오면서 로딩 처리를 해줘야 하는데 위 코드를 작성해서 처리하면 전부 없어지고, 다른 식으로 if문 안에 넣어서 작성하더라도 코드 중복이 발생하기 때문에 안 쓰는 것이 좋다.
- 제대로 적용하기 위해 분기처리를 해줄 것이다.
- 그리고 스피너가 중앙에 오도록 하기 위해서 추가적인 스타일을 적용한다.
- 삼항 연산자 처리도 가능. 근데 가독성 면에서는 떨어지는 것 같다.
{isPending ? (
<div className='flex items-center justify-center h-dvh'>
<LoadingSpinner />
</div>
): (
<div className='p-10 grid gap-2 grid-cols-2 sm:grid-cols-3 md:grid-cols-4
lg:grid-cols-5 xl:grid-cols-6'>
{/* 실제 화면에 따라 달라지도록 세팅. 반응형 확인 가능. 패딩 10, gap 2*/}
{movies && movies.map((movie) =>
<MovieCard key={movie.id} movie={movie}/>
)}
</div>
)}
- 원래 요청을 한번에 많이 하면 이 중복 요청에 대해서 abort controller 같은 걸로 막을 필요가 있는데 이 부분은 좀 어려워서 다른 영상 참고해봐도 좋다.
📌 react-router-dom
이를 사용하기 위해서 react-router-dom을 설치해준다.
$ pnpm install react-router-dom
- App.tsx에서 기존에는 MoviePage를 보여줬다면, 이제는 MoviePage 말고도 영화의 상세 페이지, 카테고리 페이지 등을 보여주려고 한다. 카테고리에 대한 api가 여러 있었는데 그걸 처리해 볼 것이다.
- 여기서는 v6 createBrowserRouter를 가지고 설명을 하고 있다.
- 일단 사용하기 위해 router 변수에 할당을 해준다.
- path는 경로를 지정, element는 어떤 페이지로 연결할지 지정한다.
- 그리고 기존에 return 했던 방식이 아닌, RouterProvider를 감싸서 바꿔준다.
- 추가적으로 NotFoundPage도 꾸며준다. 그리고 App.tsx에 errorElement에 에러 났을 때의 페이지를 불러오게 설정하면 된다.
// movies/upcoming
// movies/popular
// movies/now_playing
// movies/top_rated
이 4가지는 쿼리 파라미터로 받는 것을 좀 더 추천을 한다. -> movies?category=upcoming, movies?category=popular, ...
- 그리고 같은 컴포넌트를 사용하기 때문에, 이런 식으로 불러오는 게 더 효율적일 것이다.
children: [
{
path: 'movies/:category',
element: <MoviePage />,
},
],
- 근데 아마 이렇게 하면 화면에 안 나타날 텐데, 자식 요소를 보여주면, HomePage를 기준으로 자식 요소를 내려주고 있다. 이 자식 요소를 보여주기 위해서는 HomePage에서 추가적인 작업을 해줘야 한다.
- 바로, 자식 요소를 보여줄 outlet을 선언을 해줘야 한다.
import { Outlet } from "react-router-dom";
const HomePage = () => {
return (
<div>
<Outlet />
</div>
)
}
export default HomePage;
- 이렇게 하면, 아마 정상적으로 다 나올 것이다.
- 추가로, 부모 요소인 HomPage 같은 애들은 layout을 적용해야 한다.
- HomePage에 Navbar도 보여줄 것이다. 그러기 위해 Navbar.tsx를 만들어준다.
import { Outlet } from "react-router-dom";
import { Navbar } from "../components/Navbar";
const HomePage = () => {
return (
<>
<Navbar />
<Outlet />
</>
)
}
export default HomePage;
이렇게 해주면 아울렛을 사용하는 코드는 Navbar가 다 보일 것이다.
- 근데 Navbar는 카테고리별로 다르게 나타나야 한다. 그렇기 때문에 동적으로 받을 수 있게 해줄 것이다. -> useParams 이용.
const params = useParams<{
category: string;
}>();
`https://api.themoviedb.org/3/movie/${params.category}?language=en-US&page=${page}`
url도 params에 맞게 바꿔주면 되겠다.
-
const {category} = useParams<{category: string;}>();
- 아님 이런 식으로 구조 분해(디스트럭쳐리)를 해줘서 변경해도 된다.
- 이제 카테고리별로 이동이 가능한 Navbar를 구체화 시킬 것이다.
- 일단, 스타일을 적용한다.
-
return (<div className='flex gap-3 p-4'><Link to='/'>홈</Link><Link to='/movies/popular'>인기 영화</Link><Link to='/movies/now_playing'>상영 중</Link><Link to='/movies/now_playing'>상영 중</Link>
</div>) - 이런 식으로 해주게 될 텐데, 여기서 아마 다른 걸 눌러도 영화 데이터가 그대로 인 걸 알 수 있다. 이는 이전에도 비슷한 현상이 있어서 알 것이다. 바로, dependency 즉 의존성 배열을 추가를 해줘야 된다. 이렇게 해야 카테고리별 다른 데이터를 받을 수 있게 된다.
- 이제, NavLink를 사용하고, LINKS로 매핑을 해줘서 반복되는 코드를 줄일 것이다.
- NavLink를 쓰면 className에서 현재 경로들을 반환을 해준다.
- 이렇게 해주면 코드도 깔끔하고, 전반적으로 잘 작동할 것이다.
- 이제 해야 되는 건 영화 상세 페이지로 이동하는 것이다.
- 영화 상세 페이지에 대한 경로를 생성해준다. MovieDetailPage.tsx에 구현할 것이다.
{
path: 'movies/:movieId',
element: <MovieDetailPage />
}
- 저런 식으로 하면 컴퓨터가 인식을 잘 못해서 movies/:category/:movieId로 path를 바꾸어 줘야 한다.
- 이제, movieCard에서 영화 컴포넌트를 눌렀을 때, 상세 페이지가 보이도록 해줘야 한다.
- 2가지 방식으로 할 것이다.
1. onClick={() => (window.location.href = `/movies/now_playing/${movie.id}`)}
이런 식으로 처리를 해주면, 아래 사진에 있는 것이 잠깐 x로 변하는 것을 볼 수 있다.
- 이는 자바스크립트 코드를 한번 더 받아온다는 것을 의미한다. 처음에도 언급했듯이, 리액트 같은 경우에는 모든 것들을 lazy component를 쓰지 않는 이상 처음의 모든 자바스크립트 번들들을 다 불러온다.
- 결국, 2번이나 불러올 필요가 없는데 다시 새로고침이 되어 자바스크립트 번들을 불러 오게 되는 것이다. -> 불필요한 작업.
- 리액트가 싱글 페이지 어플리케이션인 이유는 페이지가 바뀌더라도 우리는 페이지가 바뀐 것처럼 하지만, 이 요소들은 컴포넌트가 바뀐 것처럼 그냥 컴포넌트만 변경된 걸로 바꿔준다.
- 그래서 굳이 이런 식으로 할 필요가 없다.
- 그럼 어떤 식으로 처리를 해 주냐? -> useNavigate
2. useNavigate
- onClick={() => navigate(`/movies/now_playing/${movie.id}`)}
- 이런 식으로 하고 MovieDetailPage에서 params 콘솔을 찍어보면 카테고리에 대한 정보와 무비 아이디에 대한 정보가 뜨는 걸 확인할 수 있다
- now_playing 저런 것을 쿼리 스트링으로 처리를 해줄 것이다.
- 일다은 카테고리가 나타나는 것이 좋으니까 App.tsx에서 :category 이 부분을 지우고 movie/:movieId 이런 식으로 간단히 처리를 하는 게 좋을 것이다.
-
onClick={() => navigate(`/movies/${movie.id}`)}
- movieId로만 갈 수 있게 처리를 해주면 이제 영화에 대한 상세페이지를 받아올 수 있고,
- 여기서부터는 tmdb의 movies의 details 즉, 영화에 대한 상세 페이지를 갖고 오는 건데 여기에 무비 아이디를 넘겨주면 되는 것이다.
- 만약, 278번이면 278번에 대한 데이터를 갖고오고, 타입도 그에 맞춰서 받아오면 되겠다. 이거에 대해서는 잘 정의해서 상세 페이지를 꾸미면 된다.
- 이것은 MoviePage에서 useEffect로 가져온 것처럼 처리를 한번 더 해주면 되겠다.
'UMC 8th Web 워크북' 카테고리의 다른 글
🍎 Custom-hook (4주차 UMC 강의 정리 + @) (0) | 2025.04.12 |
---|---|
🍿 TMDB 영화 상세 페이지 불러오기 (Umc 3주차 미션3) (0) | 2025.04.07 |
🎞️ 영화 리스트 데이터 불러오기 (UMC 3주차 강의 정리) (0) | 2025.04.06 |
📜 useEffect 공식문서 간단 정리 (0) | 2025.04.05 |
🫂왜 useEffect를 쓰는가 (Umc 워크북 3주차 강의.) (0) | 2025.04.04 |