React Query 정리해보자!

dwell·2022년 6월 13일
0

Project Setup


  • CRA : npx create-react-app
  • json-server
    • npm install json-server --save-dev
    • db.json 파일 생성
    • package.json 파일에서 script 수정
      • "start": "json-server --watch src/data/db.json --port 4000”
      • db.json의 파일의 위치에 유의
      • port는 바꿀 수 있음
    • mock data 추가 : db.json에
            {
              "superheroes": [
                {
                  "id": 1,
                  "name": "Batman",
                  "alterEgo": "Bruce Wayne"
                },
                {
                  "id": 2,
                  "name": "Superman",
                  "alterEgo": "Clark Kent"
                },
                {
                  "id": 3,
                  "name": "Wonder Woman",
                  "alterEgo": "Princess Diana"
                }
              ]
            }
        
    • [localhost:4000/superheroes](http://localhost:4000/superheroes) 에서 위 데이터를 볼 수 있음
  • route 추가
    • npm install react-router-dom
    • pages 폴더 생성 & Homepage, SuperHeroesPage, RQSuperHeroesPage 생성
      • index.js를 만들어서 한꺼번에 export하면 import할때 편리

        export { Homepage } from "./Homepage";
        export { SuperHeroesPage } from "./SuperHeroesPage";
        export { RQSuperHeroesPage } from "./RQSuperHeroesPage";
    • routes 폴더 생성 & index.jsx 만들기
      • 주의) Route에 component가 아닌 element 속성으로 사용해야함!! (v6에서 바뀜)

        import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
        import { Homepage, RQSuperHeroesPage, SuperHeroesPage } from "../pages";
        
        export const RootRoute = () => {
          return (
            <Router>
              <Routes>
                <Route path="/" element={<Homepage />} />
                <Route path="/rq-superheroes" element={<RQSuperHeroesPage />} />
                <Route path="/superheroes" element={<SuperHeroesPage />} />
              </Routes>
            </Router>
          );
        };
    • App.js에 RootRoute 추가
      import { RootRoute } from "./routes";
      
      function App() {
        return <RootRoute />;
      }
      
      export default App;


Basic Settings & Syntax


Install

 npm install react-query

Setting

  • Provider로 전체 App 감싸주기

    • QueryClient를 이용해서 새로운 Instance 생성은 컴포넌트 밖에서 해주기
    • devtools 설정
      • initialIsOpen : 창을 계속 열어둘건지 설정
      • position : devtool 버튼 위치 (default = bottom-left)
    import { QueryClient, QueryClientProvider } from "react-query";
    import { ReactQueryDevtools } from "react-query/devtools";
    import { RootRoute } from "./routes";
    
    const queryClient = new QueryClient();
    
    function App() {
      return (
        <QueryClientProvider client={queryClient}>
          <RootRoute />
          <ReactQueryDevtools initialIsOpen={false} position="bottom-right"/>
        </QueryClientProvider>
      );
    }
    
    export default App;

    Devtools


    • key값을 토대로 data들을 확인할 수 있음

    Screen Shot 2022-07-02 at 6.31.16 PM.png

    Basic Syntax


    • useQuery
      • useQuery(key, function, option) : 3가지 인자를 받음

        • key : devtools에서 해당 data에 매핑되는 key 이름 (해시맵)
        • function : 일반적인 data fetching 함수 → promise를 반환해야함
        • option : react-query에서 제공되는 여러 option들 사용가능
      • useQuery 반환값

        → react-query 사용전에는 별도의 state값을 선언해서 저장했는데 반에 react-query를 사용하면 반환값으로 바로 받을 수 있음
        
        - data : data fetching해서 얻은 data값
        - isLoading : 로딩중인지 알려주는 값
        - isError : 에러가 발생했는지 알려주는 값
            - true 일때 react-query가 자동적으로 한번 더 fetch를 시도함.
        - error : 발생한 에러에 대한 정보
        // RQSuperHeroesPage.jsx
        
        import axios from "axios";
        import { useQuery } from "react-query";
        
        export const RQSuperHeroesPage = () => {
          const { isLoading, data, isError, error } = useQuery("superheroes", () =>
            axios.get("http://localhost:4000/superheroes1")
          );
        
          if (isLoading) {
            return <h2>Loading...</h2>;
          }
        
          if (isError) {
            return <h2>{error.message}</h2>
          }
        
          return (
            <div>
              <h2>RQSuperHeroesPage</h2>
              {data?.data.map((superhero) => (
                <div key={superhero.id}>{superhero.name}</div>
              ))}
            </div>
          );
        };
        // SuperHeroesPage.jsx
        
        import axios from "axios";
        import { useEffect, useState } from "react";
        
        export const SuperHeroesPage = () => {
          const [superheroes, setSuperheroes] = useState([]);
          const [isLoading, setIsLoading] = useState(true);
          const [error, setError] = useState("");
        
          useEffect(() => {
            axios
              .get("http://localhost:4000/superheroes")
              .then((res) => {
                setSuperheroes(res.data);
                setIsLoading(false);
              })
              .catch((err) => {
                setError(err.message);
                setIsLoading(false);
              });
          }, []);
        
          if (isLoading) {
            return <h2>Loading....</h2>;
          }
        
          if (error) {
            return <h2>{error}</h2>;
          }
        
          return (
            <div>
              <h2>SuperHeroesPage</h2>
              {superheroes.map((superhero) => (
                <div>{superhero.name}</div>
              ))}
            </div>
          );
        };

Working Mechanism of React-Query


React-Query 사용 vs 일반적인 데이터 통신

  • 일반적인 데이터 통신 : 매번 데이터를 다시 fetch → Loading이 보임
  • react-query : data를 캐싱해서 특정 시간동안 저장. → Loading이 보이지 않음

Work Flow

  • 첫번째 data-fetch에서는 기존 data-fetch와 같은 방식으로 작동
    • isLoading : false (fetching 전) → true (fetching 중) → false (fetching 완료)
  • key 값을 이용해서 fetch한 data를 캐싱 : 기본값으로 5분간
  • 페이지 재방문시 key값에 해당하는 데이터가 있을 경우
    • 먼저 cache된 데이터 렌더, 뒤에서는 fetch를 다시해서 업데이트가 있는 경우 업데이트된 데이터를 렌더.
      • isLoading : false
      • isFetching : false → true → false


Structure

  1. fetching : 모든 query는 fetching이 일어남

  2. fresh : staleTime (기본값 = 0) 시간동안 머무름.

    • fresh한 데이터이기 때문에 refetching이 일어나지 않음
    • staleTime이후에는 stale 상태가 됨.
  3. stale : 데이터를 사용하는 화면에 있는 동안 머무름.

    • stale = fresh하지 않음을 의미. refetching이 필요한 데이터를 의미. 다음과 같은 상황에서 refetching 발생
      - 새로운 query 인스턴스가 마운트될 때 (useQuery가 처음 호출될 때)
      • 브라우저 화면을 이탈했다가 다시 포커스할 때
      • 네트워크가 다시 연결될 때
      • 특별히 설정한 refetch interval에 의해서 (refetchInterval)
    • stale한 데이터인 경우 캐싱된 데이터여도 뒤에서 fetching이 한번 더 일어남. → devtools에서 확인하면 fetching후 stale로 상태가 바뀜
  4. inactive : cacheTime (기본값 = 5min) 시간동안 머무름.

  5. deleted : cacheTime이 지난 후 cache된 query가 삭제

    Screen Shot 2022-07-03 at 12.03.14 AM.png



useQuery Options


Caching Time

  • 정의 : cache에서 inactive queries가 지원지는데 걸리는 시간

  • 기본적으로 캐싱되는 시간 : 5분

  • cache time 커스텀 :

    • useQuery의 3번째 인자인 option에 {cacheTime : ms} 으로 설정
    • 단위 : ms / ‘infinity’
  • 주의사항 : cache time은 페이지에 머무르는 시간은 상관없음. 다른 페이지에서 머무르는 시간을 측정. (즉, inactive time)

    → ex) cacheTime 5초로 설정시 다른 페이지에서 5초 이상 머무르면 devTools에 확인했을 때 해당 key가 사라짐을 확인. 만약 4초 후에 해당 페이지 진입시 그대로 cache된 데이터 사용.

Stale Time

- 정의 : fetch한 데이터가 fresh 단계에서 머무는 시간. 이 단계에서는 refetching이 일어나지 않음 (신선한 데이터로 여겨지므로 추가적인 데이터 통신을 하지 않음)
- 기본값 : 0
- stale time 커스텀 :
    - useQuery의 3번째 인자인 option에 `{staleTime : ms}` 으로 설정
    - 단위 : ms / ‘infinity’
- 주의사항 : staleTime이라고 해서 stale 단계에서 머무는 시간이 아님. fresh 단계에서 stale로 넘어오기까지 걸리는 시간임.


 

refetchOnMount & refetchOnWindowFocus

  • refetchOnMount
    • 정의 : data가 stale 상태일때 mount될 때마다 refetch 하도록 함.
    • 기본값 : true
    • 값 : boolean, ‘always’
      • always : data가 어떤 상태이든 mount 될때마다 refetch → staleTime 있어도 refetch (원래는 staleTime만큼 fresh에 머무르므로 refetch 안함)
  • refetchOnWindowFocus
    • 정의 : data가 stale 상태일 때 윈도우 포커싱될 때마다 refetch 하도록 함.
    • 기본값 : true
      • 데이터를 변경하고 다시 window로 돌아오면 data 자동으로 업데이트 (refetching 했기 때문)
      • 크롬에서 다른 탭 눌렀다가 다시 돌아왔을 때, 개발자 도구를 열거나 닫을 때도 해당.
    • 값 : boolean, ‘always’
      - always : data가 어떤 상태이든 윈도우 포커싱될 때마다 refetch
           → staleTime 있어도 refetch (원래는 staleTime만큼 fresh에 머무르므로 refetch 안함)
           

refetchInterval & refetchIntervalInBackground

  • refetchInterval
    • 정의 : data를 refetch하는 간격 설정
    • 기본값 : false
    • 값 : number (ms)
    • 예시 : refetchInterval : 1000 → 1초마다 자동적으로 refetching
    • 주의사항 :
      • 다른 option들과 달리 사용자 interaction과 관계없이 적용됨
      • window가 focus된 상태에서만 refetch 진행됨
  • refecthIntervalInBackground
    • 정의 : window focus와 관계없이 data를 refetch하는 간격 설정
    • 기본값 : false
    • 값 : number (ms)
    • 예시 : refetchIntervalInBackground : 2000 → 2초마다 window focus 되어있는지 여부와 관계없이 refetch 계속 진행


Data Fetching on Events


Events

⇒ 이벤트 발생시에만 data fetch를 해야할때 사용하는 방법

- enabled 옵션 :
    - 쿼리가 자동적으로 실행되지 않도록 설정하는 옵션
    - disabled 상태라고 뜸
- refresh 속성 :
    - useQuery에서 반환하는 값 중 하나
    - 해당 이벤트에 refresh를 넣어주면 됨.

```jsx
export const RQSuperHeroesPage = () => {
  const { isLoading, isFetching, data, isError, error, **refetch** } = useQuery(
    "superheroes",
    () => axios.get("http://localhost:4000/superheroes"),
    {
      enabled: false,
    }
  );

  if (isLoading) {
    return <h2>Loading...</h2>;
  }

  if (isError) {
    return <h2>{error.message}</h2>;
  }

  return (
    <div>
      <h2>RQSuperHeroesPage</h2>
      <button **onClick={refetch}** >show superheroes</button>
      {data?.data.map((superhero) => (
        <div key={superhero.id}>{superhero.name}</div>
      ))}
    </div>
  );
};
```
  • Callback Function

    Callback Function


    • onSuccess : data fetching 성공한 후에 실행할 것 → data에 접근가능 (인자)

    • onError : data fetching이 실패한 후에 실행할 것 → error에 접근가능 (인자)
      - error가 발생하는 경우 react-query는 기본적으로 3번 더 fetching 시도

      Screen Shot 2022-07-03 at 12.49.38 PM.png

      import axios from "axios";
      import { useQuery } from "react-query";
      
      export const RQSuperHeroesPage = () => {
        const onSuccess = (data) => {
          console.log("success");
          console.log(data);
        };
      
        const onError = (error) => {
          console.log("error");
          console.log(error);
        };
      
        const { isLoading, isFetching, data, isError, error, refetch } = useQuery(
          "superheroes",
          () => axios.get("http://localhost:4000/superheroes1"),
          {
            onSuccess,
            onError,
          }
        );
      
  • Select Specific Data

    Select Option


    • data fetching 중에 일부만 select해서 가져오는 옵션

    • data에 접근 가능 (인자)

    • return을 해줘야함. map / filter 사용가능

    • jsx에서 data로 접근가능

    • 주의사항
      - devtools에 캐싱된 데이터는 원본이 저장됨

      const { isLoading, isFetching, data, isError, error, refetch } = useQuery(
          "superheroes",
          () => axios.get("http://localhost:4000/superheroes"),
          {
            select: (data) => {
              const superheroNames = data.data.map((superhero) => superhero.name);
              return superheroNames;
            },
          }
        );
      
      return (
          <div>
            <h2>RQSuperHeroesPage</h2>
            <button onClick={refetch}>show superheroes</button>
            {data.map((superheroName) => (
              <div key={superheroName}>{superheroName}</div>
            ))}
          </div>
        );
  • Custom Hook using UseQuery

    Custom Hook 사용하는 이유


    • 재사용성을 위해서

      Custom Hook 쓰기 전


      import axios from "axios";
      import { useQuery } from "react-query";
      
      export const RQSuperHeroesPage = () => {
        const onSuccess = (data) => {
          console.log("success");
          console.log(data);
        };
      
        const onError = (error) => {
          console.log("error");
          console.log(error);
        };
      
        const { isLoading, isFetching, data, isError, error, refetch } = useQuery(
          "superheroes",
          () => axios.get("http://localhost:4000/superheroes"),
          {
            onSuccess,
            onError,
            select: (data) => {
              const superheroNames = data.data.map((superhero) => superhero.name);
              return superheroNames;
            },
          }
        );
      
        if (isLoading) {
          return <h2>Loading...</h2>;
        }
      
        if (isFetching) {
          return <h2>Fetching...</h2>;
        }
      
        if (isError) {
          return <h2>{error.message}</h2>;
        }
      
        return (
          <div>
            <h2>RQSuperHeroesPage</h2>
            <button onClick={refetch}>show superheroes</button>
            {data.map((superheroName) => (
              <div key={superheroName}>{superheroName}</div>
            ))}
          </div>
        );
      };

      Custom Hook 쓴 후


      // useSuperheroesData.jsx
      
      import { useQuery } from "react-query";
      import axios from "axios";
      
      export const useSuperheroesData = (onSuccess, onError) => {
        return useQuery(
          "superheroes",
          () => axios.get("http://localhost:4000/superheroes"),
          {
            onSuccess,
            onError,
            select: (data) => {
              const superheroNames = data.data.map((superhero) => superhero.name);
              return superheroNames;
            },
          }
        );
      };
      // RQSuperHeroesPage.js
      
      import { useSuperheroesData } from "../hooks/useSuperheroesData";
      
      export const RQSuperHeroesPage = () => {
        const onSuccess = (data) => {
          console.log("success");
          console.log(data);
        };
      
        const onError = (error) => {
          console.log("error");
          console.log(error);
        };
      
        const { isLoading, isFetching, data, isError, error, refetch } =
          useSuperheroesData(onSuccess, onError);
      
        if (isLoading) {
          return <h2>Loading...</h2>;
        }
      
        if (isFetching) {
          return <h2>Fetching...</h2>;
        }
      
        if (isError) {
          return <h2>{error.message}</h2>;
        }
      
        return (
          <div>
            <h2>RQSuperHeroesPage</h2>
            <button onClick={refetch}>show superheroes</button>
            {data.map((superheroName) => (
              <div key={superheroName}>{superheroName}</div>
            ))}
          </div>
        );
      };
  • Queries

    Query by Id


    • 정의 : 전체 데이터를 이용하는 것이 아닌 데이터의 일부만을 이용할때 사용함

    • 사용법 : useQuery key 값으로 배열을 줌 → [key, id]

    • 주로 상세페이지에서 데이터를 렌더링할 때 많이 쓰임

    • param의 경우 react-router-dom에서 제공하는 useParams를 사용해도 되지만 (이때 fetcher함수는 콜백형태여야함 → id값을 인자로 보내야하므로) query by Id는 queryKey를 사용해서 key 배열을 사용할 수 있음. 배열의 두번째 값이 heroId이므로 queryKey[1]로 접근가능

      // useSuperHeroData.jsx
      
      import axios from "axios";
      import { useQuery } from "react-query";
      
      const fetchSuperHeroData = ({ queryKey }) => {
      	const heroId = queryKey[1]
        return axios.get(`http://localhost:4000/superheroes/${heroId}`);
      };
      
      export const useSuperHeroData = (heroId) => {
        return useQuery(["super-hero", heroId], fetchSuperHeroData);
      };
      // RQSuperHeroPage.jsx
      
      import { useParams } from "react-router-dom";
      import { useSuperHeroData } from "../hooks/useSuperHeroData"
      
      export const RQSuperHeroPage = () => {
      	const {heroId} = useParams();
      	const {isLoading, data, isError, error} = useSuperHeroData(heroId);
      
      	if (isLoading) {
      		return <h2>Loading...</h2>
      	}
      
      	if (isError) {
      		return <h2>{error.message}</h2>
      	}
      
      	return (
          <div>
            <h2>RQSuperHero details page</h2>
            <span>
              {data?.data.name} - {data?.data.alterEgo}
            </span>
          </div>
        );
      }

      Parallel Queries


    • 정의 : 하나의 컴포넌트안에서 여러개의 data-fetching을 하는 것

    • 사용법 : useQuery를 여러개 사용하면 됨

    • 주의사항 : destructuring 할때 이름이 중복되므로 alias 사용하기

      import axios from "axios";
      import { useQuery } from "react-query";
      
      const fetchSuperHeroes = () => {
        return axios.get("http://localhost:4000/superheroes");
      };
      
      const fetchFriends = () => {
        return axios.get("http://localhost:4000/friends");
      };
      
      export const ParallelQueriesPage = () => {
        const { data: superheroes, isLoading: isLoadingSuperHeroes } = useQuery(
          "super-heroes",
          fetchSuperHeroes
        );
      
        const { data: friends, isLoading: isLoadingFriends } = useQuery(
          "friends",
          fetchFriends
        );
      
        if (isLoadingSuperHeroes || isLoadingFriends) {
          return <h2>Loading...</h2>;
        }
      
        return (
          <div>
            <ul>
              <h2>SuperHero List</h2>
              {superheroes?.data.map((superhero) => (
                <li key={superhero.id}>{superhero.name}</li>
              ))}
            </ul>
            <ul>
              <h2>Friend List</h2>
              {friends?.data.map((friend) => (
                <li key={friend.id}>{friend.name}</li>
              ))}
            </ul>
          </div>
        );
      };

      Dynamic Parallel Queries


    • 정의 : 동적으로 하나의 컴포넌트 안에서 여러개의 data-fetching이 필요한 경우 사용

    • 차이점 : parallel queries를 사용할 수 없는 이유는 동적이기 때문에 몇개의 query인지 정확히 알 수 없기 때문임.

    • 사용법 : useQueries를 사용함

      // HARD CODING
      const results = useQueries([
         { queryKey: ['post', 1], queryFn: fetchPost },
         { queryKey: ['post', 2], queryFn: fetchPost },
       ])
      
      // MAP 
       const queryResults = useQueries(
          heroIds?.map((heroId) => {
            return {
              queryKey: ["superhero", heroId],
              queryFn: () => fetchSuperhero(heroId),
            };
          })
        );
      import axios from "axios";
      import { useQueries } from "react-query";
      
      const fetchSuperhero = (heroId) => {
        return axios.get(`http://localhost:4000/superheroes/${heroId}`);
      };
      
      export const DynamicParallelQueriesPage = ({ heroIds }) => {
        const queryResults = useQueries(
          heroIds?.map((heroId) => {
            return {
              queryKey: ["superhero", heroId],
              queryFn: () => fetchSuperhero(heroId),
            };
          })
        );
      	
        return (
          <div>
            DynamicParallelQueriesPage
            {queryResults?.map(({ isLoading, data }) => {
              if (isLoading) {
                return <h2>Loading ...</h2>;
              }
      
              return <h2>{data?.data.name}</h2>;
            })}
          </div>
        );
      };

      Dependent Queries


    • 정의 : 다른 하나의 query 결과를 토대로 data-fetching을 진행할때 사용

    • 사용법 : option에 enabled property를 해당 query 결과로 주기
      - !! 와 보통 많이 쓰임 : !!는 확실한 boolean 값을 리턴 (ex. undefinded ⇒ false)

      import axios from "axios";
      import { useQuery } from "react-query";
      
      const fetchUserByEmail = (email) => {
        return axios.get(`http://localhost:4000/users/${email}`);
      };
      
      const fetchChannelById = (channelId) => {
        return axios.get(`http://localhost:4000/channels/${channelId}`);
      };
      
      export const DependentQueriesPage = ({ email }) => {
        const { data: user } = useQuery("user", () => fetchUserByEmail(email));
        const channelId = user?.data.channelId;
        const { data: channel } = useQuery(
          "channel",
          () => fetchChannelById(channelId),
          {
            enabled: !!channelId,
          }
        );
      
        return (
          <div>
            <h2>DependentQueriesPage</h2>
            {channel?.data.courses.map((course) => (
              <div key={course.id}>{course}</div>
            ))}
          </div>
        );
      };

      ** 주의사항 : 만약 enabled 없이 fetching을 하게되면 channelId값이 undefined이므로 /channel/undefined로 fetching을 진행해서 오류발생

      Screen Shot 2022-07-04 at 8.25.49 AM.png

  • Initial Query Data

    Initial Query Data


    • 정의 : query data by id 처럼 query list나 일부가 벌써 캐싱되어 있는 상태일때 loading 대신 그 데이터를 먼저 사용자에게 보여주는 기능, fetching이 완료되면 데이터를 다시 렌더

    • 사용법 : useQueryClient를 이용해서 queryData에 접근 → getQueryData(key) 를 사용

    • 주의사항 : return undefined의 경우 error를 발생시키는 것이 아니라 data-fetching이 완료될때까지 기다림.

      import axios from "axios";
      import { useQuery, useQueryClient } from "react-query";
      
      const fetchSuperHeroData = ({ queryKey }) => {
        const heroId = queryKey[1];
        return axios.get(`http://localhost:4000/superheroes/${heroId}`);
      };
      
      export const useSuperHeroData = (heroId) => {
        const queryClient = useQueryClient();
        return useQuery(["super-hero", heroId], fetchSuperHeroData, {
          initialData: () => {
            const hero = queryClient
              .getQueryData("superheroes")
              ?.data?.find((hero) => {
                return hero.id === parseInt(heroId);
              });
      
            if (hero) {
              return {
                data: hero,
              };
            } else {
              return undefined;
            }
          },
        });
      };
  • Pagination

    Pagination


    • Json Server의 경우 pagination 지원 → _limit _page 사용하기

      http://localhost:4000/colors?_limit=2&_page=3
    • useQuery 이용해서 pagination 적용
      - pageNumber을 useState로 관리
      - prev page button 클릭시 setPageNumber을 -1, 1일때는 disable
      - next page button 클릭시 setPageNumber을 +1, 4일때는 disable

      import axios from "axios";
      import { useState } from "react";
      import { useQuery } from "react-query";
      
      export const PaginatedQueriesPage = () => {
        **const [pageNumber, setPageNumber] = useState(1);
        const { isLoading, data, isError, error } = useQuery(
          ["colors", pageNumber],
          () =>
            axios.get(`http://localhost:4000/colors?_limit=2&_page=${pageNumber}`),
          {}
        );**
      
        if (isLoading) {
          return <h2>Loading...</h2>;
        }
      
        if (isError) {
          return <h2>{error.message}</h2>;
        }
      
        return (
          <div>
            <h2>PaginatedQueriesPage</h2>
            <ul>
              {data?.data.map((color) => (
                <li key={color.id}>
                  {color.id}. {color.label}
                </li>
              ))}
            </ul>
            <div>
              **<button
                onClick={() => setPageNumber((prevPageNumber) => prevPageNumber - 1)}
                disabled={pageNumber === 1}
              >
                Prev Page
              </button>
              <button
                onClick={() => setPageNumber((prevPageNumber) => prevPageNumber + 1)}
                disabled={pageNumber === 4}
              >
                Next Page
              </button>**
            </div>
          </div>
        );
      };
    • UX 개선부분

      • keepPreviousData 옵션 true로 설정 : 이전 데이터를 보여준채로 fetching을 진행

        → 사용자가 로딩화면이 아닌 현재 페이지의 데이터를 계속 보다가 다음 페이지의 데이터 fetching이 완료되면 UI 업데이트
        const { isLoading, isFetching, data, isError, error } = useQuery(
            ["colors", pageNumber],
            () =>
              axios.get(`http://localhost:4000/colors?_limit=2&_page=${pageNumber}`),
            {
              **keepPreviousData: true,**
            }
          );
  • Infinite Queries

    Infinite Queries


    • 정의 : infinite scrolling과 같이 데이터를 계속 아래에 추가해서 보여줄때 사용

    • 사용법 :

      • useInfiniteQuery 사용 → pageParam 인자를 가짐 : 현재 pageNumber을 의미
      • getNextPageParam / getPreviousPageParam : 다음 api 요청시 사용할 pageParam 을 설정하는 함수 → return에 원하는 값을 넣어주면 됨
        • lastPage / firstPage : useInfiniteQuery를 이용해서 호출된 마지막 / 처음 페이지 데이터
        • pages : useInfiniteQuery를 이용해서 호출된 모든 페이지 데이터
      • fetchNextPage / fetchPreviousPage :
        • useInfiniteQueries의 return 값 중 하나
        • getNextPageParam / getPreviousPageParam에서 설정한 pageParam을 이용해서 다음 / 이전 page의 data - fetching 진행
      • hasNextPage / hasPreviousPage :
        • useInfiniteQueries의 return 값 중 하나
        • boolean 형태
        • getNextPageParam / getPreviousPageParam 값에 의해서 결정됨 → getNextPageParam이나 getPreviousPageParam 존재하는 경우 true, 존재하지 않으면 false 값이 담김
    • 주의사항 :

      • useInfiniteQueries의 data 구조는 useQuery와 다름에 유의
        Data
        	ㄴ pages (배열)  :  [{page1Data}, {page2Data} ...]pageParams (배열) : [1,2,3...]
        Screen Shot 2022-07-04 at 8.07.13 PM.png
    • 궁금증 :
      - 마지막 page인지 알 수 있는 방법은 ? 백엔드에서 주어져야하는 데이터인 것인가?

      import axios from "axios";
      import { Fragment } from "react";
      import { useInfiniteQuery } from "react-query";
      
      export const InfiniteQueriesPage = () => {
        const { isLoading, data, isError, error, hasNextPage, fetchNextPage } = useInfiniteQuery(
          "colors",
          ({ pageParam = 1 }) =>
            axios.get(`http://localhost:4000/colors?_limit=2&_page=${pageParam}`),
          {
            getNextPageParam: (lastPage, pages) => {
              if (pages.length < 4) {
                return pages.length + 1;
              } else {
                return undefined;
              }
            },
          }
        );
      
        if (isLoading) {
          return <h2>Loading...</h2>;
        }
      
        if (isError) {
          return <h2>{error.message}</h2>;
        }
      
        return (
          <div>
            <h2>InfiniteQueriesPage</h2>
            <div>
              {data?.pages.map((page, idx) => {
                return (
                  <Fragment key={idx}>
                    {page.data.map((color) => (
                      <div key={color.id}>{color.id}. {color.label}</div>
                    ))}
                  </Fragment>
                );
              })}
            </div>
            <button disabled={!hasNextPage} onClick={fetchNextPage}>Load More</button>
          </div>
        );
      };
  • Mutation (sending data to server)

    Mutation


    • 정의 : server에 data를 전달할때 사용

    • 사용법 :
      - useMutation : axios의 post/put 등과 같은 request를 리턴하는 함수를 인자로 갖음

          → useQuery와 달리 key 값은 불필요
          
      - mutate : useMutation이 리턴하는 값 중 하나. mutation을 invoke 해주는 함수
          - 인자를 추가하는 경우 useMutation의 mutateFn의 인자로 들어감.
      - useQuery와 동일하게 isLoading, isError, error 사용가능
      const addSuperhero = (newHero) => {
        return axios.post("http://localhost:4000/superheroes", newHero);
      };
      
      export const useAddSuperheroData = () => {
        return useMutation(addSuperhero);
      };
      const [name, setName] = useState("");
      const [alterEgo, setAlterEgo] = useState("");
      
      const handleSubmit = (e) => {
          e.preventDefault();
          console.log({ name, alterEgo });
          addHero({ name, alterEgo });
        };
      
      const { mutate: addHero } = useAddSuperheroData();
      
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="name">Hero Name: </label>
          <input
            type="text"
            id="name"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="alterEgo">Alter Ego: </label>
          <input
            type="text"
            id="alterEgo"
            value={alterEgo}
            onChange={(e) => setAlterEgo(e.target.value)}
          />
        </div>
        <button type="submit">Add Hero</button>
      </form>

      Invalidate Queries


    • 정의 : mutation을 이용하면 변화한 데이터를 다시 refetching 해야지만 업데이트된 데이터를 볼 수 있다. 자동적으로 refetching을 도와주는 것이 invalidateQueries api이다.

    • 고민 : refetch를 이용할 수 있을 것이라 생각했지만 불가능

    • 사용법 : useMutation이 성공했을 때 queryClient.invalidateQueries(keyName)을 사용

      → invalidate = 무효화시키다
      
      → invalidateQueries는 해당 query를 stale로 인식하고 바로 fetching을 진행 
      
      → staleTime override
      export const useAddSuperheroData = () => {
        const queryClient = useQueryClient();
        
        return useMutation(addSuperhero, {
          onSuccess: () => {
            rqueryClient.invalidateQueries("superheroes");
          },
        });
      };

      SetQueryData


    • 정의 : mutation 이후 업데이트된 데이터를 사용하기 위해 invalidateQueries를 이용해서 다시 fetch 진행. 하지만 이것은 추가적인 데이터 통신이 필요. post의 경우 response로 추가된 데이터를 반환하므로 이것을 사용하여 새로운 데이터를 보여줄 수 있음

    • 사용법 :
      - onSuccess 함수 인자로 response 값을 받아옴
      - setQueryData를 사용 → 이전 데이터를 oldQueryData로 받아와서 spread Operator를 사용해서 업데이트된 data를 리턴

      export const useAddSuperheroData = () => {
        const queryClient = useQueryClient();
      
        return useMutation(addSuperhero, {
          onSuccess: (data) => {
            queryClient.setQueryData("superheroes", (oldQueryData) => {
              return {
                ...oldQueryData,
                data: [...oldQueryData.data, data.data],
              };
            });
          },
        });
      };
  • Optimistic Update

    Optimistic Update


    • 정의: optimistic이란 낙관적인이라는 뜻.

      • 정석적인 데이터 업데이트 방식 : 서버에 먼저 post, put과 같은 리퀘스트를 한 후 성공적이면 UI를 업데이트함
      • optimistic 데이터 업데이트 방식 : 먼저 UI를 업데이트한 후 서버에 리퀘스트를 날림. 성공할 것이라고 생각하고 미리 UI를 업데이트함. 하지만 실패시 rollback할 수 있음 (업데이트 전 데이터 보여주기)
    • 사용법 : useMutation의 option (두번째인자)에 3가지 콜백함수를 추가
      - onMutate :
      - 해당 query 관련 refetch를 모두 중지
      - onError를 대비해서 현재 queryData를 저장 → 리턴 : getQueryData 사용
      - 새로운 데이터를 이용하여 queryData 업데이트 : setQueryData 사용

              → 주의사항 : id값이 없으므로 id는 자체적으로 만들어줘야함.
              
      - onError : 데이터 통신이 실패로 끝난 경우 실행되는 함수 → onMutate에서 리턴한 previousData를 이용해서 롤백 (setQueryData 사용)
          - context (3번째 인자) : onMutate에서 리턴된 값이 담김
      - onSettled : 데이터 통신이 성공적으로 끝난 경우 실행되는 함수 → invalidateQueries를 통해 refetch 진행
      export const useAddSuperheroData = () => {
        const queryClient = useQueryClient();
      
        return useMutation(addSuperhero, {
          onMutate: async (newHero) => {
            await queryClient.cancelQueries("superheroes");
            const previousSuperheroesData = queryClient.getQueryData("superheroes");
            queryClient.setQueryData("superheroes", (oldQueryData) => {
              return {
                ...oldQueryData,
                data: [
                  ...oldQueryData.data,
                  {
                    id: oldQueryData?.data?.length + 1,
                    ...newHero,
                  },
                ],
              };
            });
      
            return previousSuperheroesData;
          },
          onError: (_error, _hero, context) => {
            queryClient.setQueryData("superheroes", context.previousSuperheroesData);
          },
          onSettled: () => {
            queryClient.invalidateQueries("superheroes");
          },
        });
      };
  • Axios Utils (Interceptor)

    Axios Interceptor


    • 정의 : axios의 request들을 util 함수로 빼서 사용하는 것이 일반적 → 이유는 auth가 포함된 경우가 많으므로

    • 사용법 : axios 대신 utils 폴더에 axios-utils.js 를 만들어서 그것을 이용!
      - axios.create() 을 이용해서 axios 인스턴스 생성
      - baseURL을 설정하면 options에서 url 넣을때 뒤 주소만 입력가능
      - request 함수를 만들어서 custom axios 인스턴스를 리턴
      - 인자로는 모든 options (config)를 받음 : spread operator 이용

              → ex) method, url, data etc…
              
          - auth 정보 (token)이 있을 경우 : header에 default로 넣어주기
          - onSuccess, onError 함수를 정의해서 axios 인스턴스 리턴시 같이 체이닝해서 리턴
          
      import axios from "axios";
      
      const client = axios.create({ baseURL: "http://localhost:4000" });
      
      export const request = ({ ...options }) => {
        client.defaults.headers.common.Authorization = `Bearer token`;
        const onSuccess = (response) => response;
        const onError = (error) => error;
      
        return client(options).then(onSuccess).catch(onError)
      };
      import { request } from "../utils/axios-utils";
      
      const fetchSuperhero = (heroId) => {
        return request({ url: `/superheroes/${heroId}` });
      };
      
      const addSuperhero = (newHero) => {
        return request({ method: "post", url: "/superheroes", data: newHero });
      };
profile
code for a change

0개의 댓글