๐Ÿฌ ๊ธดํ˜ธํก/๊ณ ์Šค๋ฝ ํ‹ฐ์ผ“

[2.0] React-Query ๋ฌดํ•œ์Šคํฌ๋กค (with useInfiniteQuery)

ํ•œ๊ทœ์ง„ 2022. 9. 12. 02:23

์‘์›ํ†ก ํŽ˜์ด์ง€๋ฅผ ์œ„ํ•ด ๋ฌดํ•œ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ–ˆ๋˜ ๊ธฐ๋ก์ด๋‹ค.

๊ทผ๋ฐ ์ด์ œ ๋ฆฌ์•กํŠธ์ฟผ๋ฆฌ์˜ useInfiniteQuery๋ฅผ ๊ณ๋“ค์ธ.

 


 

์ฒ˜์Œ์—” ๋‹จ์ˆœํ•˜๊ฒŒ state๋ฅผ ์ด์šฉํ•ด์„œ ๋ฌดํ•œ์Šคํฌ๋กค์„ ๋งŒ๋“œ๋Š” ๋กœ์ง์„ ์ƒ๊ฐํ•˜๊ณ  ์ ‘๊ทผํ–ˆ๋‹ค๊ฐ€ ์‚ฝ์งˆ์„ ์ข€ ํ–ˆ๋‹ค. 

setList(prev => [...prev, ...res.data.list]);

์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ํŒจ์นญ์„ ํ•˜๊ฒŒ ๋˜๋ฉด, ๊ธฐ์กด์— ์žˆ๋˜ ๋ฐฐ์—ด ์ƒํƒœ์— ์ƒˆ๋กœ ๋ฐ›์•„์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค. ์Šคํ”„๋ ˆ๋“œ ์—ฐ์‚ฐ์ž ๋˜๋Š” concat์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

 

๊ทผ๋ฐ useInfiniteQuery๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์‚ด์งœ์ฟต ๋‹ค๋ฆ„.

 

 

1. useInfiniteQuery

  const { data, fetchNextPage } = useInfiniteQuery<InfiniteTalkType, unknown>(
    ['talks'],
    UsersApi.getTalks,
    {
      getNextPageParam: (lastPage) => lastPage.lastId,
    },
  );

์ฟผ๋ฆฌํ•จ์ˆ˜ ๋ถ€๋ถ„๋งŒ ๊ฐ€์ ธ์™€ ๋ดค๋‹ค. ์ฟผ๋ฆฌ ํ‚ค์™€ ํŒจ์นญ ํ•จ์ˆ˜, ๊ทธ๋ฆฌ๊ณ  ์˜ต์…˜์„ ์ฐจ๋ก€๋กœ ๋„ฃ์–ด์ค€๋‹ค. ์ œ๋„ค๋ฆญ์€ <Data ํƒ€์ž…, Error ํƒ€์ž…>์— ํ•ด๋‹น๋œ๋‹ค. ์ œ์ผ ์ค‘์š”ํ•œ๊ฑด getNextPageParam ํ•จ์ˆ˜๊ฐ€ ๋“ค์–ด๊ฐ€๋Š” ์˜ต์…˜์ด๋‹ค. lastPage๋ผ๋Š” ์ด๋ฆ„์œผ๋กœ ์ธ์ž๋กœ ๋ฐ›์•„์„œ lastId๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

 

lastPage๋Š” ๊ทธ๋Ÿผ ๋ฌด์—‡์ผ๊นŒ(์ด๋ฆ„์€ lastPage๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ๊ฑฐ๋ผ๋„ ์ƒ๊ด€์—†๋‹ค). useInfiniteQuery ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์—ฌ๋Ÿฌ๋ฒˆ์— ๊ฑธ์ณ ๊ฐ€์ ธ์˜ฌ ๋•Œ ๊ทธ ๋ฐ์ดํ„ฐ๋“ค์ด ์•Œ์•„์„œ ๋ฐฐ์—ด ์ƒํƒœ์— ์Œ“์ด๋Š”๊ฒŒ ์•„๋‹ˆ๋‹ค. data ์•ˆ์˜ pages์˜ ๋ฐฐ์—ด๋กœ ์ฐจ๊ณก์ฐจ๊ณก ์Œ“์ธ๋‹คโฝยนโพ. ๊ทผ๋ฐ ๋ณดํ†ต์€ list๋งŒ ๊ฐ€์ ธ์˜ค๋Š”๊ฒŒ ์•„๋‹ˆ๋ผ, lastId๋‚˜ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ์ธ์ง€ ๋“ฑ์˜ ์ •๋ณด๊ฐ€ ๊ฐ™์ด ์˜ด.

 

(1), (2)

์„œ๋ฒ„์—์„œ ๋ฐ›์•„์˜ค๋Š” ๋ฐ์ดํ„ฐ์˜ ํ˜•์‹์€ ์œ„์™€ ๊ฐ™๋‹ค. ์‘์›ํ†ก ๋ฆฌ์ŠคํŠธ๋ฅผ 20๊ฐœ์”ฉ ๋ฐ›์•„์˜ค๊ณ , ๋งˆ์ง€๋ง‰์œผ๋กœ ๋ฐ›์€ ํ†ก์˜ id์™€ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ ์—ฌ๋ถ€๋ฅผ ํ•จ๊ป˜ ๋„˜๊ฒจ์ค€๋‹คโฝยฒโพ

 

UsersApi.getTalks

interface InfiniteTalkType {
  talkList: ITalk[];
  lastId: number;
  isLast: boolean;
};

...
...

 getTalks: async ({ pageParam = null }): Promise<InfiniteTalkType> => {
    const { data } = await axiosPrivate.get(
      `users/comment?lastId=${pageParam}`,
    );

    return {
      talkList: data.data.list,
      lastId: data.data.meta.lastId,
      isLast: data.data.meta.lastPage,
    };
  },

getNextPageParam์œผ๋กœ ๋„ฃ์–ด์ค€ lastId๊ฐ€ ์žˆ์—‡๋‹ค. ์œ„์˜ ์‚ฌ์ง„ 1์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Œ. ๊ทธ ์ค‘์—์„œ ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค์˜ ๊ฐ’์ด queryFn์˜ ์ธ์ž๋กœ ๋“ค์–ด๊ฐ€๋Š” ๊ฒƒ. ๊ทธ ๊ฐ’์„ api url์˜ ์ฟผ๋ฆฌ์ŠคํŠธ๋ง์œผ๋กœ ๋„ฃ์–ด์ฃผ๊ณ  ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค. ๊ทธ๋Ÿผ ๊ทธ id์— ํ•ด๋‹นํ•˜๋Š” ์‘์›ํ†ก์˜ ๋‹ค์Œ id๋ถ€ํ„ฐ 20๊ฐœ์”ฉ ๋˜ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ. meta ๊ฐ์ฒด ์•ˆ์— ์žˆ๋Š” ์ •๋ณด๋“ค์„ ๋” ํŽธํ•˜๊ฒŒ ์“ฐ๊ธฐ ์œ„ํ•ด ๊บผ๋‚ด์„œ ๊ฐ์ฒด ํ•˜๋‚˜๋กœ ๋ฆฌํ„ดํ•˜๋„๋ก ํ–ˆ๋‹ค.

 

<TalkListWrapper isOpen={isOpen} ref={talkListRef}>
  {data?.pages.map((talkList) => (
    <TalkList talkList={talkList.talkList} key={talkList.lastId} />
  ))}
  <Observation />
</TalkListWrapper>;

..
..

const TalkList = ({ talkList }: { talkList: ITalk[] }) => {
  return (
    <Wrapper>
      {talkList.map((talk) => (
        <TalkBubble
          nickName={talk.nickName}
          content={talk.content}
          createdAt={talk.createdAt}
          iComment={talk.iComment}
          key={talk.id}
        />
      ))}
    </Wrapper>
  );
};

export default TalkList;

๋ฌดํ•œ์Šคํฌ๋กค์„ ํ†ตํ•ด ์—ฌ๋Ÿฌ๋ฒˆ ํŒจ์นญํ•œ ์‘๋‹ต์˜ ๋ฐ์ดํ„ฐ๋“ค์€ pages ๋ฐฐ์—ด์— ์ €์žฅ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ๊ทธ ๋ฐ์ดํ„ฐ๋“ค ๊ฐ๊ฐ์— list๊ฐ€ ์žˆ๊ณ , ๊ทธ list๋ฅผ ์‘์›ํ†ก ๋ฒ„๋ธ”๋กœ ๊ทธ๋ ค์ฃผ์–ด์•ผ ํ•˜๋Š” ๊ฒƒ. ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ•˜๋‚˜ ๋” ์จ์„œ ๋ฐฐ์—ด์„ ๋‘๊ฐœ ๋Œ๋ ค์ฃผ์–ด์•ผํ•œ๋‹ค. pages ๋ฐฐ์—ด, ๊ทธ ์•ˆ์˜ list ๋ฐฐ์—ด.

 

 

2. Observer

์Šคํฌ๋กค์„ ๋‚ด๋ฆด ๋•Œ, ํŠน์ • ์‹œ์ ์— ์ƒˆ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์•ผ ํ•œ๋‹ค. react-intersection-observer ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. ์œ„์˜ ์ฝ”๋“œ๋ธ”๋Ÿญ์—์„œ <Observation />์ด๋ผ๋Š” div ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋ณผ ์ˆ˜ ์žˆ๋‹ค. ํ•ด๋‹น div ์š”์†Œ๊ฐ€ ํ™”๋ฉด์— ๋ณด์ด๋ฉด ๋‹ค์Œ ๋ฐ์ดํ„ฐ๋ฅผ ํŒจ์นญํ•˜๋„๋ก ํ•ด๋ณด์ž.

 

useGetTalks.tsx

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

const useGetTalks = () => {
  const { data, fetchNextPage } = useInfiniteQuery<InfiniteTalkType, unknown>(
    ['talks'],
    UsersApi.getTalks,
    {
      getNextPageParam: (lastPage) => lastPage.lastId,
    },
  );

  const Observation = (): ReactElement => {
    const [ref, inView] = useInView();

    useEffect(() => {
      if (!data) return;

      const pageLastIdx = data.pages.length - 1;
      const isLast = data?.pages[pageLastIdx].isLast;
      if (!isLast && inView) fetchNextPage();
    }, [inView]);

    return <div className="observer" ref={ref} style={{ height: '20px' }} />;
  };

  return { data, Observation };
};

export default useGetTalks;

useGetTalks ์ปค์Šคํ…€ ํ›…์„ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ–ˆ๋‹ค. fetchNextPage๋Š” (getNextPageParam์„ ํ†ตํ•ด ์ €์žฅ๋œ) pageParams์˜ ๋งˆ์ง€๋ง‰ ์ธ๋ฑ์Šค ๊ฐ’์„ ์‚ฌ์šฉํ•ด api ํŒจ์นญ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

 

์—ฌ๊ธฐ์„œ ์ค‘์š”ํ•˜๊ฒŒ ๋ณผ ๋ถ€๋ถ„์€ div ์š”์†Œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” Observation ํ•จ์ˆ˜์ด๋‹ค. react-intersection-observer์—์„œ useInView๋ฅผ import ํ•ด์˜จ๋‹ค. useInView์—์„œ ์ œ๊ณตํ•˜๋Š” ref๋ฅผ div ์š”์†Œ์— ๋‹ฌ์•„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•ด๋‹น div๊ฐ€ ํ™”๋ฉด์— ๋ณด์—ฌ์ง€๊ฒŒ ๋˜๋ฉด inView ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์ด true๋กœ ๋ฐ”๋€๋‹ค. ๊ฐ€์žฅ ์ตœ๊ทผ์— ํŒจ์นญํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹ˆ๊ณ , inView๊ฐ€ true์ผ ๋•Œ, fetchNextPage() ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

 

๋งค 20๊ฐœ item ์•„๋ž˜์— ๋†’์ด 20px ์งœ๋ฆฌ Observation ๋ฐ•์Šค๊ฐ€ ๋“ค์–ด๊ฐ„๋‹ค. ์Šคํฌ๋กค์ด ๊ฑฐ์˜ ์ตœํ•˜๋‹จ์œผ๋กœ ๋‚ด๋ ค๊ฐˆ ๋•Œ ์ฆˆ์Œ์— ๋‹ค์Œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐ”๋กœ๋ฐ”๋กœ ํŒจ์นญ๋œ๋‹ค.

 

 

์ด๋ ‡๊ฒŒ ReactQuery์˜ useInfiniteQuery๋ฅผ ์ด์šฉํ•ด์„œ ๋ฌดํ•œ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•ด๋ณด์•˜๋‹ค. ๊ฒฐ๊ตญ ์„ค๋น™์€ ๋‹ค๋ฅธ ์‚ฌ๋žŒ์˜ ํ’ˆ์œผ๋กœใ