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

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

목차

  • 다른 페이지에서 데이터를 받아 전달

    • rocoil을 사용하는 방법

    • Supabase에서 maybeSingle()과 single()

  • react-intersection-observer

    • useInView 함수 사용법

    • useInfiniteQuery 기본 사용법

  • 3주차 미션

    • supabase에 컬럼 추가

    • 찜 기능 설정하기

    • 찜한 영화를 화면 최상단으로 보여주도록 정렬



Netflix Clone Project

 

1. 다른 페이지에서 데이터를 받아 전달

  • search 검색 란은 header에 있고 해당 값을 전달해서 받아 오는 곳은 movie-card-list.tsx 에 있으므로

     

    Recoil(전역 상태 관리 라이브러리)을 사용하여 해당 값을 넘겨준다

  • reccoil도 레이아웃에서 react query와 materia ui를 사용할 수 있게 해준 것처럼 <RecoilRoot>가 있음

  • 하지만 recoil은 기본적으로 클라이언트 라이브러리라 별도의 provider를 정의 해준다.(app/config/RecoilProvider.tsx)

[rocoil을 사용하는 방법]

  • atom 함수를 사용

    • /utils/recoil/atoms.ts 파일 생성

  • recoil을 사용할 페이지에 atomes.ts에 생성한 search atom을 넣어줌

    • ex) header.tsx

       

      const {search, setSerch} = useRecoilState(searchState)
    • ex) movie-card-list.tsx

      • queryKey에 search 값을 넣어야 search 값이 변경될 때마다 query function이 재호출 됨

          const search = useRecoilValue(searchState);
          const getAllMoviesQuery = useQuery({
            queryKey: ["movie",search],
            queryFn: () => searchMovies({ search }),
          });

 

[Supabase에서 maybeSingle()과 single()]

  • single(): 반환되는 데이터가 무조건 한 행이여야 하며, null이 존재 또는 데이터 1개 초과 조회 시 오류 발생

  • maybeSingle(): 반환되는 데이터에 null이 존재해도 오류가 발생하지 않고, 빈 값을 반환함

 

2. react-intersection-observer

  • react-intersection-observer 설치

npm install react-intersection-observer
  • 화면에 이 컴포넌트가 몇 퍼센트 들어왔을 때, inView 값이 true가 됨

  • 즉, 특정 요소가 화면에 노출되었는지 감지하는 기능

import { useInView } from 'react-intersection-observer';

const [ref, inView, entry] = useInView({
  threshold: 0,
});
  • ref는 감지할 요소에 연결해야 하는 참조이며, inview는 해당 요소가 화면에 노출되었는지 여부를 나타내는 불리언 값

  • 즉, 현재 observe할 엔티티에 대해 레퍼런스를 넣기 위한 값

<div ref={ref}>    
  {inView && 'Element is in view!'}
</div>

 

** 우리가 왜 이것을 사용해야 하나?

  • 스크롤 맨 아랫부분에 보이지 않는 태그를 넣어서 해당 태그가 보이면 다음 페이지를 가져올 수 있도록 함수를 만들 예정

  • 즉, 페이징을 커서 방식으로 개발

[useInfiniteQuery]

  • 기존에 사용한 useQuery로 무한 스크롤을 구현하기에는 매우 복잡함(다양한 값 필요)

  • useInfiniteQueryreact-query 라이브러리의 핵심 기능 중 하나입니다. 이를 사용하면 무한 스크롤과 같은 기능을 쉽게 구현할 수 있다.

  • isFetchingNextPage: isLoading 대신 사용

  • fetchNextPage: 다음 페이지

  • hasNextPage: 가지고 있는 다음 페이지

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
  status,
} = useInfiniteQuery('todos', fetchTodos, {
  getNextPageParam: (lastPage, pages) => lastPage.nextPage,
});

3. 3주차 미션

 

[구현 이미지]

image
[supabase에 컬럼 추가]

  • 찜 기능을 구현할 favorit 이라는 컬럼 추가

  • 0이면 false, 1이면 true

    image

    [찜 기능 설정하기]

  • 하트를 클릭하면 하트의 상태가 바뀌면서 데이터 저장

import { updateFavorit } from "actions/movieActions";
import Link from "next/link";
import { useState } from "react";

export default function MovieCard({ movie }) {
  const [isFavorit, setIsFavorit] = useState(movie.favorit);

  const handleClick = async () => {
    setIsFavorit(!isFavorit);
    await updateFavorit(movie.id, movie.favorit);
  };

  return (
    <div className="col-span-1 relative">
      {isFavorit ? (
        <button
          onClick={handleClick}
          className={`absolute top-2 right-2 z-20 fa-solid fa-heart text-red-500 text-3xl`}
        />
      ) : (
        <button
          onClick={handleClick}
          className={`absolute top-2 right-2 z-20 fa-regular fa-heart text-red-500 text-3xl`}
        />
      )}

      {/* image */}
      <div>
        <img src={movie.image_url} className="w-full" />
        <Link href={`/movies/${movie.id}`}>
          <div className="absolute flex items-center justify-center top-0 bottom-0 left-0 right-0 z-10 bg-black opacity-0 hover:opacity-80 transition-opacity duration-300">
            <p className="text-xl font-bold text-white">{movie.title}</p>
          </div>
        </Link>
      </div>
    </div>
  );
}
  • 아이디와 상태 값을 가져와 1 이면 0, 0이면 1로 바꾸어 update해줌


export async function updateFavorit(id, state) {
  const supabase = await createServerSupabaseClient();
  state = state == 1 ? 0 : 1;
  const { data, error } = await supabase
    .from("movie")
    .update({
      favorit: state,
    })
    .eq("id", id);
  handleError(error);

  return data;
}

 

 

[찜한 영화를 화면 최상단으로 보여주도록 정렬]

  • favorit 값을 0과 1로 설정했기 때문에 order에서 ascending를 사용해 내림차순으로 정렬

    export async function searchMovies({ search, page, pageSize }) {
      const supabase = await createServerSupabaseClient();
    
      const { data, count, error } = await supabase
        .from("movie")
        .select("*", { count: "exact" })
        .like("title", `%${search}%`)
        .order("favorit", { ascending: false })
        .range((page - 1) * pageSize, page * pageSize - 1);
    
      const hasNextPage = count > page * pageSize;
    
  • favorit 값이 1이면 꽉찬 하트, 0이면 빈 하트로 보여주며, 이미지보다 상단에 띄워 놓아 클릭 시 해당 값이 바뀌도록 설정함

export default function MovieCard({ movie }) {
  return (
    <div className="col-span-1 relative">
      <button
        className={`absolute top-2 right-2 z-20 ${
          movie.favorit ? "fa-solid fa-heart" : "fa-regular fa-heart"
        } text-red-500 text-3xl`}
      />
      <div>
        <img src={movie.image_url} className="w-full" />
        <Link href={...}>
          <div className="absolute flex items-center justify-center top-0 bottom-0 left-0 right-0 z-10 bg-black opacity-0 hover:opacity-80 transition-opacity duration-300">
            <p className="text-xl font-bold text-white">{movie.title}</p>
          </div>
        </Link>
      </div>
    </div>
  );
}

 

댓글을 작성해보세요.


채널톡 아이콘