Skip to content

[6주차] Team 팝업사이클 김철흥&송아영 미션 제출합니다. #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 72 commits into
base: master
Choose a base branch
from

Conversation

DefineXX
Copy link

@DefineXX DefineXX commented May 17, 2025

배포 링크

Netflix Clone

Key Questions

1. 정적 라우팅(Static Routing)/동적 라우팅(Dynamic Routing)

정적 라우팅 (Static Routing)

  • 수동으로 라우팅을 추가한 경우입니다. ex. app/login/page.tsx
  • 넥스트의 동적 라우팅 경로에서도 generateStaticParams 함수 안에서 params의 값을 미리 선언하여 정적 라우팅으로 제한하거나, 일부를 정적 라우팅과 함께 사용할 수 있습니다.

동적 라우팅 (Dynamic Routing)

  • 기본적으로 네트워크 구조나 상태 변화(에러 등)에 따라 자동으로 라우팅 테이블이 갱신되는 방식입니다.
  • 넥스트에서의 동적 라우팅은 이것과는 조금 다릅니다. 넥스트에서의 동적 라우팅은 url 경로를 params로 받아 그 값에 따라 다른 페이지를 렌더링하는 것을 의미합니다.
  • 라우팅 경로를 대괄호로 감싸 사용합니다. `ex. app/detail/[id]/page.tsx
  • /detail/123/12와 같이 여러 개의 id를 받아야 하는 경우 app/detail/[...id].tsx`처럼 ...을 붙여 사용합니다.

2. 무한 스크롤과 Intersection Observer API의 특징

무한 스크롤 (Infinite Scroll)

  • 무한 스크롤은 스크롤을 감지하여 추가 데이터를 페칭하는 방식입니다. (= 스크롤 페이징)
  • 스크롤을 감지하여 기존 데이터의 값을 기반으로 다음 데이터를 불러와야 하기 때문에 서버 컴포넌트만으로는 처리가 어렵습니다.
  • 초기 데이터에 대해 seo를 고려하면서도 무한 스크롤을 적용하려면 추가 데이터 부분을 별도의 클라이언트 컴포넌트로 선언하고, 클라이언트 컴포넌트로 기존 데이터를 넘겨주는 방식을 취할 수 있습니다.

Intersection Observer API

  • observerRef로 참조한 DOM 요소가 뷰포트에 나타나는지를 감지하여 콜백함수를 실행합니다.
  • scroll 이벤트 핸들러를 등록하는 경우 매 스크롤마다 함수가 실행되는 데 반해 훨씬 적은 콜백함수 실행 횟수를 제공합니다.
const observer = new IntersectionObserver((entries) => {
  if (entries[0].isIntersecting) {
    fetchNextPage(); // 다음 페이지 로딩
  }
});
observer.observe(lastElementRef.current);

3. TanStack Query의 사용 이유와 특징

TanStack Query란? (구 React Query)

  • 서버 상태(Server State)를 효율적으로 관리하는 라이브러리입니다.
  • 클라이언트 상태 관리용 라이브러리(Redux, Zustand 등)와는 사용 목적이 다릅니다.
  • 비동기 요청의 캐싱, 리페칭, 에러 핸들링 등을 자동화해줍니다.

기존 상태 관리 라이브러리와의 차이점

항목 Zustand / Redux 등 TanStack Query
주 용도 클라이언트 상태 관리 서버 상태 관리 (API 요청 기반)
데이터 저장 위치 전역 상태 또는 Context 내부 캐시 시스템 (자동 GC 포함)
데이터 요청 방식 수동 fetch + 수동 캐싱 자동 fetch + 자동 캐싱 + 갱신 처리
예시 로그인 상태, 다크모드 설정 등 게시글 목록, 사용자 데이터, 댓글 목록 등

TanStack Query를 사용하는 이유

  • 자동 캐싱: 동일한 요청을 중복하지 않고 결과를 재사용
  • 자동 리패칭(refetching): 창 포커스 복귀 시, 설정된 시간 경과 시 자동 갱신
  • 로딩/에러 상태 관리 내장: isLoading, isError, isSuccess 등 상태를 명확하게 분리
  • 무한 스크롤, 페이지네이션 지원: useInfiniteQuery, getNextPageParam 등 제공
  • SSR/CSR 모두 호환: Next.js와의 통합이 용이

사용 예시 (React, Next.js 공통)

import { useQuery } from '@tanstack/react-query';

const { data, isLoading, error } = useQuery({
  queryKey: ['posts'],
  queryFn: () => fetch('/api/posts').then(res => res.json())
});

DefineXX and others added 30 commits May 2, 2025 23:39
@DefineXX DefineXX changed the title [7주차] Team 팝업사이클 김철흥&송아영 미션 제출합니다. [6주차] Team 팝업사이클 김철흥&송아영 미션 제출합니다. May 17, 2025
Copy link

@2025314242 2025314242 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋은 코드 감사합니다! 덕분에 많이 배워가는 것 같습니다!

별개로 현재 merge conflict가 있는 거 같아서 말씀드립니다!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기본적으로 camel-case 사용해서 네이밍한 것으로 보이는데, svg파일에만 kebab-case 네이밍 적용하신 이유가 있을까요?

app/layout.tsx Outdated

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usePathName hook 때문에 clientLayout으로 분리한 것 같은데, 잘 배워갑니다!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좀 생각해봤는데 이렇게 하는 거 대신에 nav-bar(tab-bar) 컴포넌트를 CSR로 처리해서 그 안에서 return !isSplash ? <div> : null; 이런 식으로 하는 건 어떠신가요? 그러면 layout.tsx 파일이 동일한 레벨에 하나만 생겨서 좀 더 깔끔할 거 같은데 저도 생각만 해본 거라 잘 모르겠네요

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 동일하게 생각합니다! 동욱 님이 말씀하신 대로 작성해도 잘 동작하고, 저도 그렇게 작성하곤 합니닷 👍👍

@@ -0,0 +1,5 @@
import Splash from '@/components/layouts/Splash';

export default async function SplashPage() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 여기서 async function을 사용하신 이유가 있을까요?

}
],
"paths": {
"@/*": ["./*"]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 설정하면 path alias로 절대경로 사용할 수 있는데, 한 번 찾아보시면 더 좋을 것 같습니다!

e.g.
"@components": ["components/*"]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 예전 프로젝트에서는 @components 같은 형태로 절대경로를 사용했는데, 폴더가 추가될 때마다 경로 설정을 추가해야 하는 부분이 많이 번거롭더라고요! 그래서 @/components 같은 형식으로 처리되는 데 만족하고 있습니다.

const PlayButton = () => {
return (
<button
className="w-full h-fit py-2 px-4 flex items-center justify-center gap-2

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 prettier 사용해도 동일한 text string 내에서는 분리가 안되던데 이거 따로 플러그인이 있나요? 아니면 position 관련 클래스랑 이외로 해서 임의로 나누신 건가요? 궁금해서 여쭤봅니다

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

임의로 개행한 거예요! 너무 길어지면 보기가 힘들어서 적당히 맥락에 따라서 개행하는 편입니다

alt={movie.title}
fill
priority={index === 0}
/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 이렇게 했을 때 dev mode에서 sizes property 채우라고 warning 떴었는데, 혹시 안 뜨시나요? (제가 aspect-ratio 클래서 안 써서 그럴 수도 있을 거 같은데, 잘 몰라서 여쭤봅니다)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sizes가 width height와는 별도의 프로퍼티인 건가요? 그렇다면 우선 저는 sizes 를 사용해 본 적이 없습니다. Image 컴포넌트를 사용하면서 본 경고는 width height를 적용하지 않았을 때 정도네요!

Image 컴포넌트의 width height는 이미지가 로드되기 전 공간을 확보하여, 비어 있던 공간에 이미지가 추가되면서 발생 가능한 ui 깜빡거림(용어가 기억이 안 나네요...) 현상을 방지하기 위한 요소입니다. 그래서 width height를 명시적으로 설정하거나, fill 속성을 부여하고 부모 요소에 relative와 적절한 width height를 설정할 수 있다고 알고 있습니다.

정확히 어떤 이유로 sizes를 채우라는 오류가 떴던 건지 세션 때 더 얘기 나눠보아요!

}, []);

return (
<ul className="w-full aspect-[5/7] relative">

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aspect class 사용해서 조절하는 거 좋은 것 같습니다! 배워갑니다!

video: boolean;
vote_average: number;
vote_count: number;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 따로 trending-movie interface를 정의하신 이유가 있으실까요? getTrendingMovies 반환할 때 필요한 속성만 반환할 수 있으면 더 좋을 것 같습니다!

fill
src={'https://image.tmdb.org/t/p/w500' + movie.poster_path}
alt={`${movie.title} poster`}
className={clsx('bg-gray-200 object-cover', isOriginal ? 'rounded-[0.125rem]' : 'rounded-[0.0625rem]')}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clsx 사용하면 이렇게 tailwind class를 동적으로 처리할 수 있군요!

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞습니다! className을 조건부, 동적으로 처리할 때 유용한 라이브러리입니닷

<Image
unoptimized
fill
src={'https://image.tmdb.org/t/p/w500' + movie.poster_path}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 이거 https://image.tmdb.org/t/p/w500 없이 그냥 poster_path 해도 되었던 거 같은데 혹시 에러 뜨셨나요?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

엇 제 기억에 동욱 님 과제에서는 response 자체에서 baseURL을 붙여서 return했던 것 같은데요, 아마 그래서 클라이언트 사이드에서는 별도의 처리가 없어도 되지 않았을까 싶습니다. 없으면 오류 나더라고요!

Copy link

@lemoncurdyogurt lemoncurdyogurt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그제 어제 아파서 이제야 코드리뷰하게되었습니다ㅠㅠㅠㅠㅠ파일구조에서 신경쓰신게 보여서 감탄하면서 보았습니다
404에러 페이지 관련 코드를 못찾아서 여기서 쓰게 됐는데 그것도 width 제한 통일하면 완성도가 높았을 것 같았습니다! 수고하셨습니다!!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

page 기본값이 1이라서, 따로 선언하지 안하도 적용이 되어서 trending/movie/day까지만 선언해도 될 것 같습니다!

Comment on lines +10 to +14
export default async function Page({ params }: { params: Promise<{ type: 'tv' | 'movie'; id: string }> }) {
const { type, id } = await params;

const response = await getDetail(`/${type}/${id}`);
if (!response) return null;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검색하다보니 type에 person도 나오더라구요, tv와 movie 둘 중 type선언하신거 같은데 어떻게 처리가 된걸까요? 의도된게 아니라면 사람 검색이 안되게 해야할 것 같습니다

Comment on lines +1 to +3
export { default as PlusIcon } from '@/public/svgs/Banner/plus.svg';
export { default as InfoIcon } from '@/public/svgs/Banner/info.svg';
export { default as Top10Icon } from '@/public/svgs/Banner/top10.svg';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 따로 선언한 이유가 있을까요?

@@ -0,0 +1,24 @@
import { getMoviesApi } from '@/services/tmdb';

import { categoryEndpointMap } from '@/constants/categoryEndpointMap';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 endpoiontMap을 사용해서 가져오는 방식이굉장히 재밌는 것 같습니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants