๐Ÿ’ป ํ”„๋กœ์ ํŠธ/๐Ÿงธ TOY-PROJECTS

[ํ† ์ดํ”„๋กœ์ ํŠธ-NETFLIX CLONE] Next.js ๋ฅผ ํ™œ์šฉํ•œ Netflix Clone ํ”„๋กœ์ ํŠธ

์žฅ์˜์ค€ 2023. 6. 20. 03:31

์ด๋ฒˆ์— ๋ฒจ๋กœ๊ทธ์—์„œ ํ‹ฐ์Šคํ† ๋ฆฌ๋กœ ๋ธ”๋กœ๊ทธ๋ฅผ ์˜ฎ๊ธฐ๋ฉด์„œ ์ด์ „์— ์ž‘์„ฑํ–ˆ๋˜ ํ”„๋กœ์ ํŠธ ๊ด€๋ จ ๋ธ”๋กœ๊ทธ๋ฅผ ์˜ฎ๊ฒจ ์ ์–ด๋ณธ๋‹ค.

 

์‹ ์ดŒ ์—ฐํ•ฉ ๋™์•„๋ฆฌ CEOS์˜ ํ”„๋ก ํŠธ์—”๋“œ ์Šคํ„ฐ๋”” ๋งˆ์ง€๋ง‰ ๊ณผ์ œ๋กœ next js๋ฅผ ํ™œ์šฉํ•œ ๋„ทํ”Œ๋ฆญ์Šค ํด๋ก  ์ฝ”๋”ฉ ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ–ˆ๋‹ค.

ํ•ด๋‹น ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ฉ”์ธ ํŽ˜์ด์ง€, searchPage๋งŒ ๊ตฌํ˜„ํ–ˆ๋‹ค.
๋ฐฐํฌ๋งํฌ: https://next-netflix-16th-pre-folio-front.vercel.app/
GITHUB: https://github.com/Pre-folio/next-netflix-16th

I. ํด๋” ๊ตฌ์กฐ

ํด๋” ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

src
|-api
|-components
	|-elements (๊ณต์œ  ์ปดํฌ๋„ŒํŠธ)
	|-homePage
	|-landingPage
	|-searchPage
	|-icons
|-pages
	|-home
		|-index.tsx
		|-[id].tsx
	|-search
		|-index.tsx
|-states
|-styles

II. HomePage

React-Query ๋„์ž…๊ธฐ

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ƒˆ๋กœ์šด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด๋ณด๊ณ  ์‹ถ์–ด react-query๋ฅผ ๋„์ž…ํ–ˆ๋‹ค.
์ด์ „์— ๋…ธ๋งˆ๋“œ์ฝ”๋” ๊ฐ•์˜๋ฅผ ํ†ตํ•ด react-query๋ฅผ ์ž ๊น ๊ณต๋ถ€ํ•œ ์ ์ด ์žˆ๋Š”๋ฐ, ๋‹ค์Œ ๊ธ€์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋‹ค.
React-Query ๊ณต๋ถ€์ž๋ฃŒ

 

1. React-query ๋„์ž…

์šฐ์„  api ํด๋”๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด api ํ˜ธ์ถœ ํ•จ์ˆ˜๋“ค์„ ๋”ฐ๋กœ ๊ด€๋ฆฌํ–ˆ๋‹ค.

export const getNowPlaying = () => {
  return client.get(`movie/now_playing?api_key=${API_KEY}`).then((res) => res.data);
};

export const getTopRated = () => {
  return client.get(`movie/top_rated?api_key=${API_KEY}`).then((res) => res.data);
};

export const getPopular = () => {
  return client.get(`movie/popular?api_key=${API_KEY}`).then((res) => res.data);
};

export const getUpcoming = () => {
  return client.get(`movie/upcoming?api_key=${API_KEY}`).then((res) => res.data);
};

2. ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง

react-query์˜ ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์—๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค. (InitialData, Hydration)
์ด์ค‘ ๋‚˜๋Š” Hydration ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

Hydrate๋ž€?

  • Next.js์—์„œ๋Š” Pre-Rendering ๋œ ์›น ํŽ˜์ด์ง€๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋จผ์ € ๋ณด๋‚ด๊ณ , React๊ฐ€ ๋ฒˆ๋“ค๋ง ๋œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ๋“ค์„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†ก
  • Next.js๋กœ ์ œ์ž‘๋œ ์›นํŽ˜์ด์ง€๋ฅผ ๋ฐฉ๋ฌธํ•˜๊ฒŒ ๋˜๋ฉด ๋งจ ์ฒ˜์Œ document ํƒ€์ž…์˜ ํŒŒ์ผ์„ ์ „์†ก๋ฐ›๊ณ , ๊ทธ ์ดํ›„์— ๋ Œ๋”๋ง ๋œ React.js ํŒŒ์ผ๋“ค์ด Chunk ๋‹จ์œ„๋กœ ๋‹ค์šด๋กœ๋“œ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Œ
  • ์œ„ ๋ฆฌ์•กํŠธ ์ฝ”๋“œ๋“ค์ด ์ด์ „์— ๋ณด๋‚ด์ง„ HTML DOM ์š”์†Œ ์œ„์— ํ•œ๋ฒˆ ๋” ๋ Œ๋”๋ง ํ•˜๋ฉด์„œ ๋ Œ๋”๋ง์„ ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ ์ด ๊ณผ์ •์„ Hydrate๋ผ๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

Hydration ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜๋ฉด getServersideProps์—์„œ prefetch๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์š”์ฒญํ•œ ๋’ค, queryClient๋ฅผ dehydrateํ•˜์—ฌ props์— dehydratedState๋กœ ์ค€๋‹ค.

export async function getServerSideProps() {
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery(['now-playing'], getNowPlaying);
  await queryClient.prefetchQuery(['top-rated'], getTopRated);
  await queryClient.prefetchQuery(['popular'], getPopular);
  await queryClient.prefetchQuery(['up-coming'], getUpcoming);
  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  };

getServerSideProps() ํ•จ์ˆ˜ ์•ˆ์— prefetchQuery๋ฅผ ์‚ฌ์šฉํ–ˆ๊ณ , props์— dehydratedState๋ฅผ ์ฃผ์–ด returnํ–ˆ๋‹ค.

const { isLoading: nowPlayingLoading, data: nowPlayingData } = useQuery(['now-playing'], getNowPlaying);
const { isLoading: topRatedLoading, data: topRatedData } = useQuery(['top-rated'], getTopRated);
const { isLoading: popularLoading, data: popularData } = useQuery(['popular'], getPopular);
const { isLoading: upComingLoading, data: upComingData } = useQuery(['up-coming'], getUpcoming);

setNowPlayingMovies(nowPlayingData.results);
setTopRatedMovies(topRatedData.results);
setPopularMovies(popularData.results);
setUpComingMovies(upComingData.results);

์ดํ›„์—๋Š” useQuery๋กœ ๊ฐ ํ•จ์ˆ˜์˜ isLoading ๊ฐ’๊ณผ data ๊ฐ’์„ ๋ฐ›์•„์™”๋‹ค. (useQuery๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ isLoading๊ณผ data ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.)

const์˜ ์ธ์ž ๋’ค์— ์“ฐ์ธ ๋”ฐ์˜ดํ‘œ๋Š” ๊ทธ ์†์„ฑ์— ์ด๋ฆ„์„ ๋ถ€์—ฌํ•œ๋‹ค๋Š” ๋œป์ด๋‹ค. (js ๋ฌธ๋ฒ•)

React-Query ์‚ฌ์šฉํ•ด ๋ณด๋‹ˆ ์ข‹์€ ์ 
1. isLoading๊ฐ’๋„ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•ด ์คŒ - ๋”ฐ๋กœ set ํ•  ํ•„์š” ์—†์–ด์„œ ํŽธ๋ฆฌํ•จ
2. ์บ์‹ฑ ๊ธฐ๋Šฅ์ด ์žˆ์–ด์„œ ํ•œ๋ฒˆ ๋“ค์–ด๊ฐ„ ํŽ˜์ด์ง€ ๋‹ค์‹œ ๋“ค์–ด๊ฐ€๋ฉด ๋กœ๋”ฉ์‹œ๊ฐ„ ์—†์ด ๋ฐ”๋กœ ๋ณด์ž„
3. ๋™์ผ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ ๋ฒˆ ์š”์ฒญํ•˜๋ฉด ์•Œ์•„์„œ ๊ฑธ๋Ÿฌ์„œ ํ•œ ๋ฒˆ๋งŒ ์š”์ฒญ → ํšจ์œจ up
4. ๋ฆฌ์•กํŠธ ํ›…๋“ค์ด๋ž‘ ์‚ฌ์šฉ ๊ตฌ์กฐ๊ฐ€ ๋น„์Šทํ•ด์„œ ๋ฐฐ์šฐ๊ธฐ ์šฉ์ดํ•จ

III. SearchPage

1. ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์„ ์–ด๋–ป๊ฒŒ?

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜์ž๋ฉด ํด๋ฆญ์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜๋Š” ๋ฐฉ์‹๋„ ์•„๋‹Œ ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์œผ๋กœ ํ•˜๋ ค๋Š” ๊ฒƒ์€ ๋ง๋„ ์•ˆ ๋˜๋Š” ์ผ์ด์—ˆ๋‹ค.

์„œ๋ฒ„์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ๋ฐฉ์‹์€ ์„œ๋ฒ„์— html ํŒŒ์ผ์„ ์ €์žฅํ•ด ๋‘์—ˆ๋‹ค๊ฐ€, ํ›„์— ํด๋ผ์ด์–ธํŠธ์˜ ์š”์ฒญ์„ ๋ฐ›๊ณ  html ํŒŒ์ผ์„ ๋ณ€ํ™˜ํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ €์— ์ถœ๋ ฅํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. html ํŒŒ์ผ์„ ์„œ๋ฒ„์— ๋ฏธ๋ฆฌ ์ €์žฅํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ณ„์† api๋ฅผ ์ƒˆ๋กœ ์š”์ฒญํ•ด์„œ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค๊ฒŒ ์‹œํ‚ค๋Š” ๋ฐฉ๋ฒ•์€ ๋งž์ง€ ์•Š๋Š” ๋ฐฉ๋ฒ•์ด์—ˆ๋‹ค.

useEffect(() => {
    setIsLoading(true);
    if (debouncedSearchWord) {
      searchMovies(searchWord).then((res) => {
        setSearchedMovies(res.data.results);
        setIsLoading(false);
        return res.data;
      });
    } else {
      getPopular().then((res) => {
        setSearchedMovies(res.results);
        setIsLoading(false);
        return res.data;
      });
    }
  }, [debouncedSearchWord]);

๊ทธ๋ž˜์„œ ์ž‘์„ฑ๋œ ์ฝ”๋“œ. ์ผ๋ฐ˜์ ์ธ api ํ˜ธ์ถœ๊ณผ ๋‹ค๋ฅผ ๊ฒƒ ์—†๋‹ค.
์ดˆ๊ธฐ์— ์ž‘์„ฑ๋œ ์ฝ”๋“œ๋ผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ์ดํ„ฐ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ๊ธฐ๋Šฅ์—๋งŒ ์ง‘์ค‘ํ–ˆ๋‹ค. (์Šคํฌ๋กค, ์Šค์ผˆ๋ ˆํ†ค X)

2. ์Šค์ผˆ๋ ˆํ†ค ์ด๋ฏธ์ง€

useQuery๊ฐ€ ๋ฐ›์•„์˜ค๋Š” ๊ฐ’ ์ค‘ isLoading ๊ฐ’์„ ํ†ตํ•ด, ํ•ด๋‹น ๊ฐ’์ด true์ธ์ง€ false ์ธ์ง€์— ๋”ฐ๋ผ true๋ฉด ์Šค์ผˆ๋ ˆํ†ค ์ด๋ฏธ์ง€๋ฅผ, false๋ฉด ์˜ํ™”์— ํ•ด๋‹นํ•˜๋Š” ์ธ๋„ค์ผ ์ด๋ฏธ์ง€๋ฅผ ๋„์›Œ์ค„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.

let arr = new Array(20).fill(1);

const { 
	getBoard, 
	getNextPage, 
	getBoardIsSuccess, 
	getNextPageIsPossible, 
	isLoading 
} = useInfiniteScrollSearchQuery(debouncedSearchWord);
<div>
      <ListTitle>Top Searches</ListTitle>
      {!isLoading
        ? // ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์„ฑ๊ณตํ•˜๊ณ  ๋ฐ์ดํ„ฐ๊ฐ€ 0๊ฐœ๊ฐ€ ์•„๋‹ ๋•Œ ๋ Œ๋”๋ง
          getBoardIsSuccess && getBoard!.pages
          ? getBoard!.pages.map((page_data: any, page_num: any) => {
              const board_page = page_data.board_page;

              return board_page?.map((item: any, idx: any) => {
                if (
                  // ๋งˆ์ง€๋ง‰ ์š”์†Œ์— ref ๋‹ฌ์•„์ฃผ๊ธฐ
                  getBoard!.pages.length - 1 === page_num &&
                  board_page.length - 1 === idx
                ) {
                  return (
                    // ๋งˆ์ง€๋ง‰ ์š”์†Œ์— ref ๋„ฃ๊ธฐ ์œ„ํ•ด div๋กœ ๊ฐ์‹ธ๊ธฐ
                    <div ref={ref} key={item.board_id}>
                      <SkeletonItem key={item.board_id} />
                    </div>
                  );
                } else {
                  return <SearchItem key={item.board_id} {...item} />;
                }
              });
            })
          : null
        : arr.map((arr, index) => {
            return <SkeletonItem key={index} />;
          })}
    </div>

3. Debounce

์ž…๋ ฅ ํ›„ ์ผ์ • ์‹œ๊ฐ„์ด ์ง€๋‚œ ๋’ค์— ๋ Œ๋”๋ง ํ•˜๊ฒŒ๋” ๋งŒ๋“ค๊ณ  ์‹ถ์–ด ๋งŒ๋“  ๊ธฐ๋Šฅ์ด๋‹ค.

hooks ํด๋” ์•„๋ž˜์— useDebounse.tsx ํŒŒ์ผ์„ ๋งŒ๋“ค์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค.

  import { useState, useEffect } from 'react';

  export const useDebounce = (value: string, delay: number) => {
    const [debouncedValue, setDebouncedValue] = useState(value);

    useEffect(() => {
      const handler = setTimeout(() => {
        setDebouncedValue(value);
      }, delay);

      return () => {
        clearTimeout(handler);
      };
    }, [value, delay]);
    return debouncedValue;
  };

props๋กœ value์™€ delay๋ฅผ ๋ฐ›์•„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ฐ’๊ณผ ์‹œ๊ฐ„์„ ์ž…๋ ฅํ•ด ์ฃผ์–ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

const debouncedSearchWord = useDebounce(searchWord, 500);

4. ๋ฌดํ•œ ์Šคํฌ๋กค

์šฐ์„  react-query์˜ ๋‚ด์žฅํ•จ์ˆ˜์ธ useInfiniteQuery์— ๊ด€ํ•œ ๊ณต๋ถ€๋ฅผ ๋งŽ์ด ํ–ˆ๋‹ค.
๊ณต๋ถ€์ž๋ฃŒ

 

๋ฌดํ•œ ์Šคํฌ๋กค ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ๋•Œ ์‚ฌ์šฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค: 
useInfiniteQuery(react-query), useInView(react-intersection-observer)

1. useInfiniteQuery
์šฐ์„  useInfiniteScrollQuery๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ์„ ํ•˜๋‚˜ ์ƒ์„ฑํ•˜์—ฌ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ–ˆ๋‹ค.

import { useInfiniteQuery } from '@tanstack/react-query';
import client from '../../api/client';
const API_KEY = process.env.NEXT_PUBLIC_TMDB_API_KEY;

export function useInfiniteScrollSearchQuery(debouncedSearchWord: string) {
  const getSearchData = async ({ pageParam = 1, searchWord = debouncedSearchWord }) => {
    const res = await client.get(`search/movie/?api_key=${API_KEY}&query=${searchWord}&page=${pageParam}`);

    return {
      board_page: res.data.results,
      current_page: pageParam,
      isLast: res.data.total_pages === pageParam, //๋ฏธ์ณค๋‹ค.
      current_word: debouncedSearchWord,
    };
  };

  const {
    data: getBoard,
    fetchNextPage: getNextPage,
    isSuccess: getBoardIsSuccess,
    hasNextPage: getNextPageIsPossible,
    isLoading: isLoading,
  } = useInfiniteQuery(['search', debouncedSearchWord], getSearchData, {
    getNextPageParam: (lastPage: any, pages: any) => {
      if (!lastPage.isLast) return lastPage.current_page + 1;
      return undefined;
    },
  });

  return {
    getBoard,
    getNextPage,
    getBoardIsSuccess,
    getNextPageIsPossible,
    isLoading,
  };
}

ํ•ด๋‹น ์ฝ”๋“œ๋Š” ๊ฒ€์ƒ‰ ํ›„ ๊ฒ€์ƒ‰์–ด๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค api๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ณ , ๊ทธ๊ณณ์— ๋ฌดํ•œ์Šคํฌ๋กค ๊ธฐ๋Šฅ์„ ๋„ฃ๋Š” ๊ฒƒ์ด๋‹ค.
์šฐ์„ ์ ์œผ๋กœ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ํ•˜๋Š” ๊ฒƒ์— ์ง‘์ค‘ํ•˜์—ฌ api ๋ฐ›๋Š” ๊ฒƒ๋„ ํ•จ๊ป˜ ์ฝ”๋“œ ์•ˆ์— ๋„ฃ์—ˆ๋‹ค…(์ถ”ํ›„ ๋ฆฌํŒฉํ† ๋ง ์˜ˆ์ •)

  • ์ฒ˜์Œ์— getSearchData ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด api๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , return ๊ฐ’์œผ๋กœ
    board_page (ํŽ˜์ด์ง€์˜ ๊ฒฐ๊ณผ),
    current_page (ํ˜„์žฌ ํŽ˜์ด์ง€: 1๋ถ€ํ„ฐ 1์”ฉ ์ฆ๊ฐ€๋˜๋Š” ๊ฐ’),
    isLast (๋งˆ์ง€๋ง‰ํŽ˜์ด์ง€์ธ์ง€ ์•„๋‹Œ์ง€ ํ™•์ธํ•ด ์ฃผ๋Š” boolean ๊ฐ’),
    hasNextPage (๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€ ์•Œ๋ ค์ฃผ๋Š” boolean ๊ฐ’),
    isLoading(๋กœ๋”ฉ ์ค‘์ธ์ง€ ํ™•์ธํ•ด ์ฃผ๋Š” boolean ๊ฐ’)์„ ๋ฐ›์•˜๋‹ค.
  • ๋‹ค์Œ์œผ๋กœ๋Š” useInfiniteQuery ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค.
    ํ•ด๋‹น ํ•จ์ˆ˜๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ props๋ฅผ ๊ฐ€์ง„๋‹ค.
    useInfiniteQuery(queryKey: ๊ณ ์œ ์˜ key๊ฐ’, queryFunction: ํ•จ์ˆ˜, option: ๊ทธ ์™ธ ์˜ต์…˜๋“ค)

option ํ•ญ๋ชฉ์— getNextPageParam์„ ์•„๋ž˜ ์ฝ”๋“œ์™€ ๊ฐ™์ด ์ž‘์„ฑํ•ด ์ฃผ๋ฉด props ํ•ญ๋ชฉ์˜ lastPage์™€ pages๋Š” ์ฝœ๋ฐฑ ํ•จ์ˆ˜์—์„œ ๋ฆฌํ„ดํ•œ ๊ฐ’์„ ์˜๋ฏธํ•œ๋‹ค๊ณ  ํ•œ๋‹ค.
(lastPage: ์ง์ „์— ๋ฐ˜ํ™˜๋œ ๋ฆฌํ„ด๊ฐ’, pages: ์—ฌํƒœ ๋ฐ›์•„์˜จ ์ „์ฒด ํŽ˜์ด์ง€)

const {
    data: getBoard,
    fetchNextPage: getNextPage,
    isSuccess: getBoardIsSuccess,
    hasNextPage: getNextPageIsPossible,
    isLoading: isLoading,
  } = useInfiniteQuery(['search', debouncedSearchWord], getSearchData, {
    getNextPageParam: (lastPage: any, pages: any) => {
      if (!lastPage.isLast) return lastPage.current_page + 1;
      return undefined;
    },
  });

์—ฌ๊ธฐ์„œ ๋กœ๋”ฉ๋œ ํŽ˜์ด์ง€๊ฐ€ ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€์ธ์ง€ ๊ฒ€์‚ฌ ํ›„์— ๋งž์œผ๋ฉด undefined๋ฅผ, ์•„๋‹ˆ๋ฉด current_page ๊ฐ’์— +1์„ ํ•ด์ฃผ์–ด ๋‹ค์Œ ํŽ˜์ด์ง€๊ฐ€ ๋ฐ˜ํ™˜๋˜๋„๋ก ํ•˜์˜€๋‹ค.

์ด์™ธ, useInfiniteQuery๋ฅผ ๊ณต๋ถ€ํ•˜๋ฉฐ ์•Œ๊ฒŒ ๋œ ๋‹ค๋ฅธ option๋“ค์— ๊ด€ํ•ด์„œ๋„ ์ž‘์„ฑํ•ด๋ณธ๋‹ค..(์•„๊นŒ์›Œ์„œ)

  • ์ƒˆ๋กœ์šด ์ฟผ๋ฆฌ ์ธ์Šคํ„ด์Šค๊ฐ€ ๋งˆ์šดํŠธ ํ–ˆ์„ ๋•Œ (refetchOnMount)
    • ์˜ˆ๋ฅผ ๋“ค์–ด ๋ฐ์ดํ„ฐ๋ฅผ fetch ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์žˆ๊ณ , ์ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋‹ค๋ฅธ ๋ฐ์—์„œ๋„ ๋งˆ์šดํŠธ ๋˜๋ฉด ์บ์‹œ ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์“ฐ๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ ๊ทธ๋•Œ ๋˜๋‹ค์‹œ refetch ํ•œ๋‹ค๋Š” ๋œป.
  • ์œˆ๋„๊ฐ€ refocus๋์„ ๋•Œ (refetchOnWindowFocus)
  • ๋„คํŠธ์›Œํฌ๊ฐ€ ๋‹ค์‹œ ์—ฐ๊ฒฐ๋์„ ๋•Œ (refetchOnReconnect)
  • refetch interval ์„ค์ •์„ ํ•ด์คฌ์„ ๋•Œ (refetchInterval)

์‚ฌ์šฉ์˜ˆ์‹œ

const {
    data: getBoard,
    fetchNextPage: getNextPage,
    isSuccess: getBoardIsSuccess,
    hasNextPage: getNextPageIsPossible,
    isLoading: isLoading,
  }: any = useInfiniteQuery(['popular'], getInitialData, {
	   refetchonMount: true,
		 refetchOnWindowFocus: true,
		 ...
    },
  });

์›๋ž˜๋Š” ๊ฒ€์ƒ‰ ๋‹จ์–ด๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค useInfiniteQuery๋ฅผ ์–ด๋–ป๊ฒŒ ์žฌํ˜ธ์ถœํ•˜๋ฉด ์ข‹์„ ์ง€์— ๊ด€ํ•œ ๊ณ ๋ฏผ์„ ์ •๋ง ์˜ค๋žซ๋™์•ˆ ํ–ˆ๋‹ค.
์ฐพ๋‹ค๊ฐ€ ์•„๋ž˜ useInfiniteQuery์˜ ์ฒซ ์ธ์ž์ธ queryKey์— ๋ฐฐ์—ด๋กœ ๋ณ€์ˆ˜๋ฅผ ๋„ฃ์œผ๋ฉด ๋ณ€์ˆ˜๊ฐ€ ๋‹ฌ๋ผ์งˆ ๋•Œ๋งˆ๋‹ค ๋ Œ๋”๋ง ๋œ๋‹ค๊ณ  ํ•˜๋”๋ผ…
(๋‚˜์ค‘์— ์•Œ๊ณ  ๋ณด๋‹ˆ key๊ฐ’์ด ๋ณ€ํ•ด์„œ ๋‹ค์‹œ ์บ์‹ฑ์„ ๊ด€๋ฆฌํ•˜๋ ค๊ณ  ๋ Œ๋”๋ง ํ•œ๋‹ค๊ณ  ํ•œ๋‹ค.)
<๊ฒฐ๊ณผ>
useInfiniteQuery(['search', debouncedSearchWord], getSearchData)
์„ ์‚ฌ์šฉํ•˜๋‹ˆ ๊ฒ€์ƒ‰์–ด๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ๋ Œ๋”๋ง ๋˜์—ˆ๋‹ค!!

 

2. useInview
๋‹ค์Œ์€ react-intersection-observer ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ค์น˜ ํ›„ import ํ•œ useInView๋ผ๋Š” ํ•จ์ˆ˜์ด๋‹ค.

  • ์‚ฌ์šฉ๋ฒ•
    const [ref, isView] = useInView()
    ์•„๋ž˜ ์ฝ”๋“œ์—์„œ div์˜ ref prop์— useInView์˜ ref๋ฅผ ์ฃผ๋ฉด,
    ์‚ฌ์šฉ์ž๊ฐ€ div ์š”์†Œ๋ฅผ ๋ณด๋ฉด isView๊ฐ€ true, ์•ˆ ๋ณด๋ฉด false๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.
const { 
	getBoard, 
	getNextPage, 
	getBoardIsSuccess, 
	getNextPageIsPossible, 
	isLoading 
} = useInfiniteScrollSearchQuery(debouncedSearchWord);

  useEffect(() => {
    if (isView && getNextPageIsPossible) {
      getNextPage();
    }
  }, [isView, getBoard]);

์œ„์™€ ๊ฐ™์€ ์ฝ”๋“œ๋กœ useInfiniteScrollSearchQuery ํ•จ์ˆ˜๋ฅผ import ํ•˜๊ณ  return ํ•œ ๊ฐ’๋“ค์„ ๋ฐ›์•„์™”๋‹ค.

์ดํ›„์—๋Š” useEffect ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•˜๋ฉด (ํ•ด๋‹น ํŽ˜์ด์ง€๊ฐ€ ๋งˆ์ง€๋ง‰ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด) ๋‹ค์Œ ํŽ˜์ด์ง€๋ฅผ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค.


์ด๋ ‡๊ฒŒ ์ด๋ฒˆ netflix ํด๋ก  ์ฝ”๋”ฉ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•œ ๊ธฐ์ˆ ๋“ค์„ ์ •๋ฆฌํ•ด ๋ณด์•˜๋‹ค.
๋„ˆ๋ฌด ๋งŽ์€ ํšจ์œจ์ ์ธ ๊ธฐ์ˆ ๋“ค์„ ์‚ฌ์šฉํ•ด ๋ณด๊ณ  ๊ฒฝํ—˜ํ•ด ๋ดค๋‹ค๋Š” ์ ์—์„œ ์ข‹์€ ๊ฒฝํ—˜์ด์—ˆ๋˜ ๊ฒƒ ๊ฐ™๋‹ค.