💸딱 하루, 인프런 천원샵 오픈!

[인프런 워밍업 클럽 3기] 풀스택 과정 3주차 발자국 👣

[인프런 워밍업 클럽 3기] 풀스택 과정 3주차 발자국 👣

어느새 3주차다!

느낀점은, 혼자 강의를 신청했으면 절대 이만큼 못왔을 거란 거다. 스터디에 참여했기에 강제성이 있어 이만큼 올 수 있었다.

일하면서 강의를 듣는다는게 시간도 체력도 많이 필요한 일임을 느꼈다.

다행인 점은 과제가 그렇게 많이 어렵진 않아서 다행히도 주말만 투자해서도 해낼 수 있다는 것..!

마지막 주는 좀 더 빡셀지 모르겠으나,, 이번 주 공부한 내용을 정리해본다.

 

깃허브 링크

 

결과물 이미지!

imageimageimage

 

이슈 해결

1. 초기 세팅 후 npm install, npm run dev 다시 하기

  • tailwind 같은 라이브러리는 설치 후 서버 다시 런해야 적용될 때가 있으니 스타일 적용이 안된다 싶으면 npm run dev 다시 해주는 걸 잊지 말자.

2. Recoil과 최신 React 버전 충돌 문제

Error: Cannot destructure property 'ReactCurrentDispatcher' of 'react__WEBPACK_IMPORTED_MODULE_0___default(...).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' as it is undefined.

  • Recoil의 현재 버전은 0.7.7에 멈춰있음

  • React 19, 18과 호환이 안되는 것으로 보임

  • Zustand를 사용할까 했는데, Next.js의 속성과 충돌하는 면이 있다고 함

    아래의 인용 참고 (원문)

    in Next.js

    Next.js 환경에서는 위 장점이 온전히 발휘되지 못한다. 공식적으로 Context 안에서 스토어를 초기화하고 저장하는 것을 권장한다. 때문에 기존 스토어 생성 -> 사용의 두 단계에서 Context 생성 -> 스토어 생성 -> 스토어 초기화 -> Provider 등록 -> 사용의 다섯 단계로 과정이 늘어난다.

    예시 코드는 공식 문서에서 볼 수 있다.

    이렇게 세팅의 차이가 나는 이유는 Zustand의 스토어가 전역 변수 기반이기 때문이다. Next.js는 서버와 클라이언트, 두 가지 환경에서 모두 실행되므로 전역 변수의 동작이 Zustand의 가정과 다르다.

    • 서버 컴포넌트, 클라이언트 컴포넌트 나뉘는 것이 원인인 듯함

    • 추가로, 가끔 발생하던 Hydration 에러를 좀 더 이해하게 됐는데, 서버와 클라이언트 컴포넌트 간에 공유되는 상태의 값이 서로 다를 때 발생하는 에러라고 함

    • ssr과 hydration 관련 zustand 문서

  • 그래서, 궁금하기도 했던 Context API를 활용해보기로 함

3. 무한 스크롤 - 새 데이터 불러오고 스크롤 위치 초기화되는 문제

  • 원인: 데이터 패치 중인 상태에 리턴할 컴포넌트를 먼저 배치하고, 그 아래 리스트 컴포넌트를 배치 → 새 데이터를 불러온 뒤 리스트 컴포넌트가 다시 렌더링되게 하는 듯

    if (isFetching || isFetchingNextPage) {
        return (
            <div className="absolute top-0 bottom-0 left-0 right-0 flex items-center justify-center">
                <i className="fa-solid fa-spinner animate-spin text-3xl"></i>
            </div>
        );
    }
    
    return (
        <div className="w-full h-full grid md:grid-cols-4 grid-cols-3 gap-1">
            {/* 배열(전체 무비 데이터) 속 배열(페이지별 무비 데이터)이므로, 평탄화 */}
            {data?.pages?.map((page) =>
                page.data
                    ?.flat()
                    ?.map((movie) => <MovieCard key={movie.id} movie={movie} />)
            )}
            {/* IntersectionObserver - 무한스크롤 구현을 위해 */}
            <div ref={ref} className="h-10"></div>
        </div>
    );
    
  • 해결: 순서를 바꿈

    return (
        <div className="w-full h-full relative">
            {/* 로딩 인디케이터 */}
            {isFetching && !isFetchingNextPage && (
                <div className="fixed inset-0 flex items-center justify-center z-10">
                    <i className="fa-solid fa-spinner animate-spin text-3xl"></i>
                </div>
            )}
    
            {data && (
                <div className="w-full h-full grid md:grid-cols-4 grid-cols-3 gap-1">
                    {/* 배열(전체 무비 데이터) 속 배열(페이지별 무비 데이터)이므로, 평탄화 */}
                    {data?.pages?.map((page, pageIndex) =>
                        page.data
                            ?.flat()
                            ?.map((movie) => (
                                <MovieCard
                                    key={`page-${pageIndex}-movie-${movie.id}`}
                                    movie={movie}
                                />
                            ))
                    )}
                    {/* IntersectionObserver - 무한스크롤 구현을 위해 */}
                    <div
                        ref={ref}
                        className="h-10 col-span-full flex justify-center items-center">
                        {isFetchingNextPage && (
                            <i className="fa-solid fa-spinner animate-spin text-xl"></i>
                        )}
                    </div>
                </div>
            )}
        </div>
    );
    

4. 검색 - 대소문자 구분 없이 검색 및 로딩 인디케이터 제거

  • 대소문자 구분 없이 검색 - ilike를 사용

    .ilike("title", `%${search.trim().replace(/\\s+/g, "%")}%`) // 대소문자 구분 X
    
  • 로딩 인디케이터 관련: 검색 시 무한 스크롤 코드에서 계속 hasNextPagetrue로 되어 데이터를 무한으로 호출하는 버그였음

    • hasNextPage 값을 처리하는 코드 개선 - 데이터 패치 필요없는 경우들에 명확히 undefined를 반환해 더이상 데이터 불러오지 않게끔 개선

    // useInfiniteQuery 사용하는 코드
    
    getNextPageParam: (lastPage) => {
        // lastPage?.page ? lastPage.page + 1 : null, // 검색어 입력 시 무한으로 다시 호출하는 버그
    
        // 데이터가 없으면 undefined를 반환하여 hasNextPage를 false로 설정
        if (!lastPage.data || lastPage.data.length === 0) {
            return undefined;
        }
        // 마지막 페이지의 데이터 개수가 pageSize보다 작으면 undefined를 반환
        if (lastPage.data.length < pageSize) {
           return undefined;
        }
        return lastPage.page + 1;
    },

 

새로운 정보

1. 무한스크롤 구현

1) React-Intersection-Observer 라이브러리 (useInView 훅)

  • 화면에 요소가 보이게 됐는지 확인할 때 사용

  • ref를 전달해 어떤 요소를 감지할지 명시,

  • 무한스크롤 구현에 사용

    • 보여줄 데이터 제일 밑에 보이지 않는 태그를 추가

    • 이 태그가 화면에 보이는 순간 다음 페이지 데이터를 가져오는 함수 호출

      ‘이 태그가 보이는 순간’을 감지하는 데 useInView 사용

      순간을 감지하면 실행하라,는 코드는 useEffect 로 구현

2) React-Query의 useInfiniteQuery

  • useQuery와 유사한데, fetchNextPage, hasNextPage, isFetchingNextPage라는 특수한 props 가짐

2. Next.js - 페이지별 메타데이터 생성: generateMetadata

  • 영화 리스트 - 영화 상세 페이지로 이동했을 때 그 영화 관련 정보로 그 페이지의 메타데이터를 구성하고 싶을 경우 등에 사용

  • ogImage: 해당 페이지 링크 공유 시 뜨는 이미지

// movies/[id]/page.tsx 상단에 정의

// 각 영화 페이지에 맞는 동적인 메타데이터를 생성
export async function generateMetadata({params}: {
    params: { id: string };
}) {
		// 사용할 데이터 패치
    const movie = await getMovieById({
        movieId: Number(params.id),
    });
    
    return {
        title: movie?.title || "",
        description: movie?.overview || "",

        // 메타데이터를 위한 이미지 URL - og이미지. 사이트 url 공유 시 보이는 이미지임
        openGraph: {
            images: [movie?.image_url || ""],
        },
    };
}

export default function MovieDetail({
// 후략...

미션

Notflix Clone 프로젝트에 "찜하기" 기능을 추가하세요. • 사용자가 특정 영화를 "찜"할 수 있도록 Supabase를 활용해 즐겨찾기 리스트 구현 찜한 영화를 영화 리스트 화면의 최상단으로 보여주도록 정렬

  1. 찜하기 기능 추가 - movie 테이블에 favorited row 추가, boolean 타입으로

  2. 최상단 보여주기 - 데이터 조회 코드를 수정

    • 찜한 데이터를 먼저 불러오고, 그외의 데이터를 불러오도록 수정?

1. 찜하기 update 처리 후 movies 리스트 순서가 바뀌는 문제

  • 쿼리문의 order 기준을 정해 항상 같은 순서로 데이터를 불러오게 함

    • release_date가 같은 경우 존재, 고유 값인 id를 보조 정렬 규칙으로 사용

      .order("release_date", { ascending: false })
      .order("id", { ascending: true }) // 고유한 ID를 보조 정렬 키로 추가
      

2. 찜한 데이터 리스트 따로 조회 및 보여주기

  • 최근에 찜한 순서로 보여주는 게 일반적이라 판단, favoritedfavorited_date (timestampz)로 변경

  • 조회하는 코드 분리 및 따로 리스트 생성: 찜한 컨텐츠는 Row, 가로 스크롤 배치

댓글을 작성해보세요.


채널톡 아이콘