Skip to content

[6주차 과제] TanStack Query 연습#6

Open
yumin-kim2 wants to merge 32 commits into
mainfrom
Week6/Homework
Open

[6주차 과제] TanStack Query 연습#6
yumin-kim2 wants to merge 32 commits into
mainfrom
Week6/Homework

Conversation

@yumin-kim2

@yumin-kim2 yumin-kim2 commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

🏡 과제 명세

💡 과제

  • React + TypeScript
  • TanStack Query 사용
  • HTTP 클라이언트 라이브러리 사용 (axios, ky, ...)
  • ThemeProvider, GlobalStyle 사용 (CSS 라이브러리 사용)
  • any 사용 금지
  1. 영화 목록 페이지(메인 페이지)
  • 리스트 형태로 영화 데이터 불러오기
  • 별점을 통한 필터링 기능 구현
  • 각 영화 카드에는 포스터, 영화 제목, 개봉일, 줄거리 표시
  • 영화 카드 hover시 효과(확대 or 그림자 or 색상 변경 등 자유)
  • 영화 카드 클릭 시, 영화 상세 페이지로 이동
  • 무한 스크롤 구현
  1. 영화 상세 페이지
  • movieId 로 라우팅
  • 영화 포스터, 배경 이미지, 제목, 개봉일, 장르, 평점, 상영 시간, 투표 수, 상태, 줄거리 표시
  • 기본 정보(원제, 원어, 제작 국가, 사용 언어, 예산, 수익) 표시
  • ‘<- 목록으로 돌아가기’ 클릭 시 영화 목록 페이지로 이동
  1. 별점 남기기/삭제하기
  • 영화 상세 페이지에 존재
  • 이전에 매긴 별점이 없을 경우 비어 있으며, 있을 경우 해당 값으로 채워짐
  • 범위 밖 숫자 입력 시 저장 x
  • 성공시, 성공 메시지 하단에 출력
  • ‘별점 삭제하기’ 클릭 시, 별점 삭제

✍️ 구현 요약 및 새로 배운 점

전체 폴더 구조 정리

처음에는 컴포넌트를 전부 공통 components에 둘지 고민했는데, 구현하면서 아래처럼 정리했습니다.

src/
├─ apis/
│  ├─ movieClient.ts
│  ├─ movieList.ts
│  ├─ movieDetail.ts
│  └─ movieRating.ts
│
├─ pages/
│  ├─ movie-list/
│  │  ├─ MovieListPage.tsx
│  │  └─ components/
│  │     ├─ MovieCard.tsx
│  │     └─ RatingFilter.tsx
│  │
│  └─ movie-detail/
│     ├─ MovieDetailPage.tsx
│     └─ components/
│        ├─ MovieBasicInfo.tsx
│        └─ MovieRatingForm.tsx
│
├─ styles/
│  ├─ globalStyle.ts
│  └─ theme.ts
│
└─ types/
   └─ movie.ts

공통으로 쓸 수 있는 컴포넌트는 src/components, 특정 페이지에서만 쓰는 컴포넌트는 해당 페이지 폴더 안의 components에 두는 방식으로 정리했습니다.

API 요청 구조 분리

TMDB 요청에서 반복되는 baseURL과 Authorization 설정은 movieClient.ts로 분리했습니다.

export const movieClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  headers: {
    Authorization: `Bearer ${import.meta.env.VITE_API_KEY}`,
  },
});

그리고 기능별 API는 파일을 나눠 관리했습니다.

movieList.ts   → 영화 목록 조회
movieDetail.ts → 영화 상세 조회
movieRating.ts → 별점 저장/삭제

무한 스크롤 구현

처음에는 useQuery로 1페이지만 불러오다가, 무한 스크롤 구현을 위해 useInfiniteQuery로 변경했습니다.
useInfiniteQuery의 응답은 페이지별로 쌓이기 때문에, flatMap으로 영화 배열을 하나로 합쳐 렌더링했습니다.

const movies = data?.pages.flatMap((page) => page.results) ?? [];

스크롤 하단 감지는 IntersectionObserver를 사용했습니다.

if (firstEntry.isIntersecting && hasNextPage && !isFetchingNextPage) {
  fetchNextPage();
}

이를 통해 사용자가 페이지 하단에 도달했을 때 다음 페이지를 자동으로 불러오도록 구현했습니다.

별점 필터링

별점 필터링은 vote_average.gte를 사용해서 선택한 평점 이상인 영화만 조회하도록 구현했습니다.
필터 값은 useState로 관리하고, queryKey에 포함해서 필터 변경 시 TanStack Query가 새로운 요청을 보내도록 했습니다.

const [rating, setRating] = useState(0);

useQuery({
  queryKey: ["movies", rating],
  queryFn: () => getMovieList(1, rating),
});

별점 저장 및 삭제

별점 입력은 0.5 ~ 10.0 범위만 저장되도록 검증했습니다.

if (Number.isNaN(ratingNumber) || ratingNumber < 0.5 || ratingNumber > 10) {
  setMessage("0.5 이상 10 이하의 숫자를 입력해주세요.");
  return;
}

저장/삭제 성공 시에는 메시지를 출력했습니다.

setMessage("별점이 저장되었습니다.");
setMessage("별점이 삭제되었습니다.");

💦 구현 과정에서 어려웠던 & 고민했던 부분

컴포넌트 구조 고민

처음에는 컴포넌트를 전부 공통 components 폴더에 넣으려고 했지만, 페이지에서만 쓰는 컴포넌트까지 모두 공통 폴더에 두면 오히려 찾기 어려울 것 같았습니다.
그래서 아래처럼 기준을 나눴습니다.

여러 페이지에서 쓰는 컴포넌트
→ src/components

영화 목록에서만 쓰는 컴포넌트
→ pages/movie-list/components

영화 상세에서만 쓰는 컴포넌트
→ pages/movie-detail/components

이 구조로 바꾸면서 MovieCard, RatingFilter, MovieBasicInfo, MovieRatingForm의 위치가 더 명확해졌습니다.

별점 삭제 후 값이 남아있던 문제

별점 삭제 API는 성공했는데, 상세 페이지를 나갔다가 다시 들어오면 input에 이전 값이 남아 있는 문제가 있었습니다.
문제 원인은 삭제 후 rated movies 조회 결과가 바로 반영되지 않거나, 조회 실패 시 기존 상태가 남아 있었기 때문입니다.
그래서 조회 실패 시 input을 비우고, 삭제 성공 시 쿼리를 무효화하도록 처리했습니다.

if (isRatedMoviesError) {
  setRating("");
  return;
}
queryClient.invalidateQueries({
  queryKey: ["rated-movies", guestSessionId],
});

이 과정을 통해 TanStack Query의 캐시 갱신이 왜 필요한지 이해하게 되었습니다.

🙋‍♀️ 리뷰 요청 포인트 & 질문

  • 페이지별 컴포넌트를 pages/.../components 안에 두는 구조가 적절한지 궁금합니다.
  • 상세 페이지 UI를 어느 정도까지 컴포넌트로 분리하는 게 좋은지 피드백 받고 싶습니다!
  • 별점 저장/삭제 기능에서 guest session을 localStorage에 저장해서 재사용하도록 했는데, 이 방식이 과제 범위에서 괜찮은지 궁금합니다.
  • useInfiniteQueryIntersectionObserver를 함께 사용한 무한 스크롤 구현 방식이 적절한지 봐주시면 좋겠습니다.
  • 현재 Tailwind와 styled-components를 함께 사용하고 있는데, ThemeProvider/GlobalStyle 사용 방식이 과제 조건을 충족하기에 충분한지 확인 부탁드립니다.

📷 결과물

https://drive.google.com/file/d/1c0Ch63YGIEFqRUg6MiR6w6oerbCELbl_/view?usp=sharing
https://drive.google.com/file/d/19xIwNpump_Bzn_3qyEk-Eq4jt8bLRc38/view?usp=sharing

yumin-kim2 added 30 commits May 6, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant