Skip to content

[6주차] Team 하니홈 신수진&원채영 미션 제출합니다. #13

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 111 commits into
base: master
Choose a base branch
from

Conversation

chaeyoungwon
Copy link

@chaeyoungwon chaeyoungwon commented May 17, 2025

과제를 하며

지난 주차에는 랜딩 페이지와 홈 페이지까지만 작업을 진행했기 때문에, 이번 주차 과제에서는 검색 페이지와 상세 페이지를 두 역할로 나누어 분담해 진행하였습니다. 과제 발표 과정에서 받은 피드백과 코드 리뷰를 바탕으로 코드를 다시 점검하며 리팩토링을 진행했고, 이 과정에서 API 함수 수정도 함께 이루어졌습니다. 자연스럽게 역할이 정리되면서, 수진님은 리팩토링이 필요한 API 코드와 관련된 상세 페이지를, 저는 검색 페이지를 맡아 구현하게 되었습니다.

  • 😊 신수진

지난 주차에 받은 API 함수 관련 피드백을 반영하여, 각 API 함수 내부에서 error 코드를 명시적으로 처리하도록 리팩토링을 진행하였습니다. 이와 함께 상세 페이지(Detail Page) 구현을 맡아 작업하였습니다.

상세 페이지는 detail/[media_type]/[id]와 같은 동적 라우팅(dynamic routing) 방식을 통해 구성하였으며, 각 콘텐츠 카드의 이미지에 페이지 이동 링크를 설정하여 접근할 수 있도록 구현하였습니다.

초기에는 media_type 값을 기준으로 영화(movie)와 TV 프로그램(tv)을 구분하는 로직을 작성하였으나, discover/movie 등 특정 미디어 타입만 반환하는 API의 경우 media_type 필드가 포함되지 않아, 데이터에 포함된 title 또는 name 필드를 기준으로 미디어 타입을 유추하는 방식으로 개선하였습니다.

또한, 상세 페이지 내에서는 초기 로딩 시 사용자 경험을 향상시키기 위해 스켈레톤 UI를 도입하여, 콘텐츠 데이터가 로드되기 전까지의 빈 화면을 최소화하였습니다.

  • 🤓 원채영

검색 페이지를 맡아 구현하면서, 초기 검색이 이루어지지 않은 경우에는 인기 영화를 불러오는 API를 사용하여 무한 스크롤 방식으로 렌더링되도록 하였습니다. 이때 초기 렌더링/ 로딩 시에는 사용자 경험을 고려해 스켈레톤 UI를 표시하도록 구현하였습니다.

또한, 검색어 입력 시 모든 입력에 대해 API가 과도하게 호출되지 않도록 useDebounce 훅을 작성하여, 일정 시간 동안 입력이 멈췄을 때 마지막 검색어를 기준으로 API 요청이 이루어지도록 하였습니다.

초성 검색과 같은 부가 기능도 도입하고 싶었지만, TMDB API가 초성 기준의 검색을 지원하지 않아 프론트엔드에서 직접 데이터를 필터링해야 했습니다. 이 과정에서 사용자 경험과 구현 난이도를 고려해 해당 기능은 이번 과제에서는 보류하게 되었습니다.

배포 링크

🔗 넷플릭스 TEAM 하니홈

Key Questions

정적 라우팅(Static Routing)/동적 라우팅(Dynamic Routing)이란?

  • 정적 라우팅:
    • 라우팅 테이블에 경로를 수동으로 추가하는 프로세스
    • useRouter 또는 Link 컴포넌트를 사용해 고정된 경로로 이동한다.
import Link from "next/link";

<Link href="/example">이동</Link>
  • 동적 라우팅:
    • 경로에 대괄호 ([param])를 사용해 동적인 값을 받을 수 있는 페이지를 생성한다.
    • 이때 대괄호 안의 값은 useRouter() 또는 useParams() 등을 통해 라우터 객체에서 쿼리 값으로 접근할 수 있다.
import { useParams } from "next/navigation";

const PostPage = () => {
  const { id } = useParams();

  return <div>Post ID: {id}</div>;
};

export default PostPage;

무한 스크롤과 Intersection Observer API의 특징에 대해 알아봅시다.

무한 스크롤:

  • 사용자가 페이지의 최하단에 도달했을 때 자동으로 새로운 콘텐츠를 불러오는 방식
  • 일반적으로 페이지네이션(pagination)과 함께 콘텐츠 탐색을 위해 많이 사용

(장점)

  • 콘텐츠 탐색이 자연스럽고 직관적
  • 별도의 버튼 클릭 없이 콘텐츠가 자동으로 로드되어 사용자 편의성이 높음

(단점)

  • 많은 콘텐츠를 연속적으로 로드하게 되면 페이지 성능 저하가 발생 가능
  • 스크롤 위치가 계속 변경되므로, 사용자가 이전에 보던 위치로 되돌아가기 어려움

Intersection Observer API:

  • 관찰 중인 요소(element)가 뷰포트(viewport)와 교차되는지 여부를 감시하는 Web API
  • 즉, 사용자가 보고 있는 화면 영역에 요소가 들어왔는지(보이기 시작했는지)를 감지할 수 있게 해주는 API

기존 방식과의 차이점

  • 전통적인 스크롤 이벤트를 활용한 방식은 이벤트 호출이 지나치게 자주 발생해 성능에 악영향을 줄 수 있.
  • 이를 보완하기 위해 일반적으로 throttle 함수를 함께 사용하여, 일정 주기마다 한 번만 이벤트가 발생하도록 제한합니다.
  • throttle: 일정 주기마다 1번의 이벤트만 발생하도록 하며 스크롤 이벤트가 계속해서 발생하는 것을 막음

사용방법:

  1. new IntersectionObserver(callback, options)를 사용해 관찰자 객체 생성
  2. .observe(targetElement)를 호출해 관찰 대상 등록
  3. options에는 아래 3가지 속성을 설정할 수 있음:
  • root: 교차 상태를 감지할 기준 요소. 기본값은 null (즉, 브라우저 viewport)
  • rootMargin: 감지 영역을 확장하거나 축소할 수 있는 여백(margin) 설정. 기본값: '0px 0px 0px 0px'
  • threshold: 관찰 대상 요소가 얼마나 보였을 때 콜백이 호출될지를 설정. 0~1 사이의 숫자. 기본값: 0

콜백 함수 형식

  • callback(entries, observer) 형식
  • entries: 관찰 중인 모든 요소들의 상태 정보가 배열로 전달됨
  • observer: 해당 IntersectionObserver 인스턴스
  • 보통 entries.forEach(entry => ...) 형태로 개별 요소 접근

tanstack query의 사용 이유(기존의 상태 관리 라이브러리와는 어떻게 다른지)와 사용 방법(reactJS와 nextJS에서)을 알아봅시다.

  • 비교 항목 Redux / Zustand / Recoil 등 TanStack Query
    주 목적 클라이언트 상태 관리 (UI 상태 등) 서버 상태 관리 (API 응답 등)
    데이터 중심 상태를 직접 저장하고 조작 API 요청 결과를 자동으로 관리
    캐싱/동기화 직접 로직 작성 필요 내장 기능으로 자동 처리
    복잡성 로딩/에러 상태 직접 관리 기본 제공 (isLoading, isError 등)
    개발 편의성 설정 및 유지 보수 부담 있음 선언형 방식, 간단한 사용법
    서버 통신 제어 useEffect, 상태관리 로직 필요 useQuery/useMutation으로 간단 처리

    서버에서 데이터를 받아와 화면에 표시하고, 캐싱 및 동기화를 자동화하는 데 특화된 도구이다.

    tanstack query의 대표적 기능

    1. 데이터 가져오기 및 캐싱

      • TanStack Query를 활용해서 데이터를 가져올 때는 항상 쿼리 키(queryKey)를 지정함
        • 이 쿼리 키는 캐시된 데이터와 비교해 새로운 데이터를 가져올지, 캐시된 데이터를 사용할지 결정하는 기준
      import { useQuery } from '@tanstack/react-query'
      
      const fetchData = async () => {
        const res = await fetch('/api/example')
        if (!res.ok) throw new Error('Network response was not ok')
        return res.json()
      }
      
      const ExampleComponent = () => {
        const { data, isLoading, error } = useQuery({
          queryKey: ['example'], //캐시 구분용 ID
          queryFn: fetchData,
          staleTime: 1000 * 60, // 1분 동안 신선함 유지
        })
      
        if (isLoading) return <p>Loading...</p>
        if (error) return <p>Error occurred!</p>
      
        return <div>{JSON.stringify(data)}</div>
      }
      • 쿼리 키와 일치하는 데이터가 없을 때, 서버에서 새로운 데이터를 가져오고 그 데이터는 캐시가 됨
        • 그 이후 요청부터는 캐시된 데이터를 사용할 수 있음.
      • 반대로 쿼리 키와 일치하는 데이터가 있다면 서버에 요청하지 않고 캐시된 데이터를 사용하게 됨.
    2. 동일 요청의 중복 제거

      • 동일한 queryKey를 가진 여러 컴포넌트가 동시에 렌더링되더라도 서버 요청은 단 한 번만 발생.
      • 이는 서버 부하를 줄이고, 불필요한 요청을 방지
    3. 신선한 데이터 유지

      캐시된 데이터가 신선하다면 캐시된 데이터를 사용하고, 만약 데이터가 상했다면 서버에 다시 요청해 새로운 데이터를 가져오는 방식으로 데이터의 신선도를 유지

      const { data, isStale } = useQuery({
          queryKey: ['delay'],
          queryFn: async () => (await fetch('/api/example')).json(),
          staleTime: 1000 * 10 //10초 동안 신선
        })
      • staleTime을 통해 캐시된 데이터의 유효기간을 설정할 수 있음
        • isStale 값으로 신선도 여부 확인 가능
      • 설정된 시간 이후에는 데이터가 "stale" 상태가 되어 재요청 발생 가능
    4. 무한 스크롤, 페이지네이션 등의 성능 최적화

      import { useInfiniteQuery } from '@tanstack/react-query'
      
      const fetchPage = ({ pageParam = 1 }) =>
        fetch(`/api/items?page=${pageParam}`).then(res => res.json())
      
      const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
        queryKey: ['items'],
        queryFn: fetchPage,
        getNextPageParam: (lastPage, allPages) => {
          return lastPage.nextPage ?? false
        }
      })

      useInfiniteQuery를 사용하면 스크롤 기반 로딩이나 페이지네이션을 쉽게 구현할 수 있음

    5. 네트워크 재연결, 요청 실패 등의 자동 갱신

      • 요청 실패 시 자동 재시도

        useQuery({
          queryKey: ['posts'],
          queryFn: fetchPosts,
          retry: 3,           // 최대 3번 재시도
          retryDelay: 2000,   // 각 재시도 간격 2초
        })
      • 네트워크 재연결 시 자동 갱신

        useQuery({
          queryKey: ['posts'],
          queryFn: fetchPosts,
          refetchOnReconnect: true,   
          // 브라우저가 오프라인 → 온라인 복귀 시 자동 refetch
        })
        • 사용자가 오프라인 상태였다가 다시 온라인이 되면 자동으로 데이터를 최신화함

        추가적으로 refetchInterval같은 경우 일정 주기로 데이터 자동 갱신 가능함 (ex. 실시간 가격 등)

lemoncurdyogurt and others added 30 commits April 29, 2025 15:38
✨ feat: 레이아웃 컴포넌트 구현 및 404 페이지 구현
feat: 넷플릭스 랜딩페이지 구현
lemoncurdyogurt and others added 29 commits May 12, 2025 17:12
✨ feat: detail 페이지 제작 및 링크 추가
Copy link

@kkys00 kkys00 left a comment

Choose a reason for hiding this comment

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

스켈레톤이랑 오디오도 넣은 팀이라 저번 발표에서 인상깊었어요. 수고하셨습니다~

Comment on lines +7 to +22
export const metadata: Metadata = {
title: "Netflix",
description: "CEOS 21th NETFLIX Clone Coding",
robots: "index, follow",
authors: [{ name: "Sujin" }, { name: "Chaeyoung" }],
icons: {
icon: "/icons/favicon.svg",
},
openGraph: {
title: "Netflix Clone",
description: "CEOS 21th NETFLIX Clone Coding by SuyoungSwim",
url: "https://next-netflix-21th-suyoungswim.vercel.app",
siteName: "Netflix Clone",
type: "website",
},
};
Copy link

Choose a reason for hiding this comment

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

메타 데이터까지 신경쓰신 게 인상 깊네여

Comment on lines +19 to +22
const handleStart = () => {
setStarted(true);
audioRef.current?.play();
};
Copy link

Choose a reason for hiding this comment

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

useEffect 사용하여 페이지 입장 시 바로 재생하지 않고 클릭 시 재생으로 하신 이유가 있으실까요?

Copy link
Author

Choose a reason for hiding this comment

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

넷플릭스 로고에 마우스를 올렸을 때 음악이 재생되도록 해보려 했는데, 브라우저 보안 정책 때문에 사용자 액션 없이 자동 재생이 막히더라고요! 그래서 클릭 시 재생되도록 방식만 조금 바꿨습니다 :)

Comment on lines +1 to +5
import ComingSoonIcon from "@/public/icons/bottomNavBar/ComingSoonIcon.svg";
import DownloadIcon from "@/public/icons/bottomNavBar/DownloadIcon.svg";
import HomeIcon from "@/public/icons/bottomNavBar/HomeIcon.svg";
import MoreIcon from "@/public/icons/bottomNavBar/MoreIcon.svg";
import SearchIcon from "@/public/icons/bottomNavBar/SearchIcon.svg";
Copy link

Choose a reason for hiding this comment

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

chatgpt에서 svgr 라이브러리를 사용할 거면 svg 파일을 src 하위에서 관리하는 게 낫다고 하더라고요.
svg를 image 컴포넌트로 렌더링할 거면 public 하위에서 관리하는 게 최적화가 알아서 된다고 하네요.

Comment on lines +25 to +34
if (loading) {
return (
<SkeletonCard
title="Previews"
itemWidth="102px"
itemHeight="102px"
shape="circle"
/>
);
}
Copy link

Choose a reason for hiding this comment

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

스켈레톤을 loading 상태를 관리하시면서 구현하셨군요. 저는 Suspense 사용해보려다가 안되더라고요...

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.

3 participants