[react]react-query 정리

young0_0·2023년 7월 28일
1

react

목록 보기
7/7
post-custom-banner

React-query란

  • 서버 상태 가져오기
  • 캐싱
  • 동기화 및 업데이트

등 을 쉽게 다룰 수 있도록 도와주는 라이브러리.
클라이언트상태와 서버상태를 명확히 구분하기 위해서 만들어졌다.

react-query의 주요기능

  1. 캐싱
  2. 동일한 데이터 중복 요청을 단일 요청으로 통합
  3. 백그라운드에서 오래된 데이터 업데이트
  4. 데이터 상태 확인 (오래된 상태, 업데이트상태-빠르게 반영)
  5. 페이지네이션, 데이터 지연 로드 와 같은 성능 최적화(페이지네이션,인피니티스크롤)
  6. 서버상태의 메모리 및 가비지 수집 관리
  7. 구조 공유를 사용하여 쿼리 결과 메모화

기본설정

import {QueryClient} from '@tanstack/react-query'

//기본 옵션 
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity,
      // ...
    },
  },
});

function App() {
  return (
    // react-query연결
   <QueryClientProvider client={queryClient}>
	<App/>
   </QueryClientProvider>;
  );
}
  • QueryClient를 사용하여 캐시와 상호작용 할수 있다.
  • QueryClient에서 모든 query/ mutation에 기본 옵션을 추가 할수있다.
  • QueryClientProvider 를 최상단에 감싸고 client prop으로 QueryClient 인스턴스를 연결 시킨다.(이 context는 앱에서 비동기 요청을 알아서 처리하는 background 계층이된다.)

용어 정리

  • caching: 특정 데이터의 복사본을 저장하여 이후 동일한 데이터의 재접근 속도를 높이는 것
  • fresh : 새롭게 추가된 쿼리 인스턴스이며 만료되지 않은 쿼리.(컴포넌트가 마운트, 업데이트 되어도 데이터 재요청을 하지 않고 항상 캐시된 데이터를 가져온다.)
  • fetching: 요청 상태인 쿼리 (대기중)
  • stale : 데이터 패칭이 완료되어 만료된 쿼리 (컴포넌트가 마운트, 업데이트 되면 캐시된 데이터가 환횐다.)
  • inactive: 비활성화된 쿼리(5분뒤 가지비 콜렉터가 캐시를 제거한다.)

캐싱 라이프 사이클

  1. A라는 queryKey를 가진 A 쿼리 인스턴스가 mount 됨
  2. 네트워크를 통해 데이터를 가져오고(fetch),가져오는 데이터는 A라는 queryKey로 캐싱함
  3. 이 데이터는 신선한(fresh) 상태에서 staleTime(기본값 0) 이후 오래된 (stale)상태로 변경됨
  4. A쿼리 인스턴스가 unmout됨
  5. 캐시는 cacheTime(기본 5분)만큼 유지하다가 가비지 컬렉션 됨
  6. 만약 cacheTime이 지나기전, A 쿼리 인스턴스가 신선한(fresh)상태라면 새롭게 mount 되면 캐시된 데이터 보여줌

데이터 갱신

기본값으로 설정된 경우 오래된 쿼리는 자동으로 데이터를 다시 가져온다.

  • 쿼리를 사용한 컴포넌트가 마운트 되었을때 (refetchOnMout)
  • 윈도우가 다시 포커스 되었을 때 (refetchOnWindowFocus)
  • 네트워크가 다시 연결되었을 때(refetchOnReconnect)
  • refetchInterval 설정 하여 반복적으로 refetch 되도록 설정 했을 때

useQuery 기본 문법

// 방법1
const {data,isLoading,...} = useQuery(queryKey,queryFn,{
//options
}
                                      
//방법2
const result = userQuery({
	queryKey,
	queryFn,
	{// option
	}
})
result.data
result.isLoading


// 예제
const getAllSuperHero = async ()=>{
	return await axios.get('http://localhost:4000/superheros')
}

const {data, isLoading} = useQuery(['super-heros'],getAllSuperHeor)

1. queryKey

 const getSuperHero = async ({queryKey}:any) =>{
   const heroId = queryKey[1]
   return await axios.get(`http://localhost:4000/superheroes/${heroId}`)
 }
 
 const useSuperHeroData = (heroId:string)=>{
 	return useQuery(['super-hero', heroId] ,getSuperHero)
 }
  • ['super-heroes'] 배열 지정
  • querykey 기반으로 데이터를 캐싱 관리한다.
    • 쿼리가 특정 변수에 의존하면 배열에 이어서 넣어주면 된다. ['super-hero', heroId]
    • qeuryClient.setQueryData과 같이 특정 쿼리에 접근이 필요할때 초기에 설정해둔 포맷을 지켜줘야 제대로 쿼리에 접근 할수 있다.

2. queryFn

const getSuperHero = async (heroId: string) => {
  return await axios.get(`http://localhost:4000/superheroes/${heroId}`);
};

const useSuperHeroData = (heroId: string) => {
  return useQuery(["super-hero", heroId], () => getSuperHero(heroId));
};
  • Promise를 반환하는 함수를 넣어야 한다.
  • queryKey 와 queryFn 예제의 차이
    • queryKey 예제는 2번째 queryFn에 getSuperHero 함수를 바로 넘겨주고, getSuperHero에서 매개 변수로 객체를 받아와 해당 객체의 queryKey를 활용
    • queryFn 예제는 그냥 2번째 queryFn에 화살표 함수를 사용하고, getSuperHero의 인자로 heroId를 넘겨주고 있다.

주요 리턴 데이터

const { status, isLoading, isError, error, data, isFetching, ... } = useQuery(
  ["colors", pageNum],
  () => fetchColors(pageNum)
);
  • status : 상태를 표현하는 4가지 값
    • idle: 쿼리 데이터가 없고 비었을때
    • loading : 캐시된 데이터가 없고 로딩중
    • error: 요청 에러
    • success: 요청성공
  • data: 쿼리 함수가 리턴한 Promise에서 resolve된 데이터
  • isLoading: 캐싱 된 데이터가 없을때, 즉,처음 실행된 쿼리 일때 로딩 여부에 따라 true/false 로 반환
  • isFetching:캐싱 된 데이터가 있더라도 쿼리가 실행되면 로딩 여부에 따라 true/false로 반환된다.
  • error: 쿼리 함수에 오류가 발생한 경우, 쿼리에 대한 오류 객체
  • isError : 에러가 발생한 경우 true

주요 옵션

retry

const {isLoading, isFetching, data, isError, error} = useQuery(['super-hero'], getSuperHero,{
	retry:10
}

query/mutation 작업이 실패하면 자동으로 재시도를 한다. Query 기본값은 3번 이며, Mutation의 기본값은 0이다. retryDelay설정을 통해 간격을 설정 할 수 있다.

  • false: 실패한 쿼리는 다시 시도 하지 않는다.
  • true: 실패한 쿼리를 무한으로 재요청 시도
  • number: 숫자 만큼 시도
const {isLoading, isFetching, data, isError, error} = useQuery(['super-hero'], getSuperHero,{
  cacheTime: 5*60*1000  
  staleTime: 1*60*1000  
}

staleTime

데이터가 오래된 것으로 인식하게 되는 시간.(fresh -> stale) ms로 저장되는데 기본값 은 0 이며 데이터가 오래 되었다고 판단되면 다시 데이터를 가지고 온다.

  • fresh 상태일 때는 쿼리 인스턴스가 새롭게 mount 되어도 네트워크 요청(fetch)가 일어 나지 않는다.
  • 0: 데이터를 가져온 즉시 오래된 데이터로 인식하여 캐시 된 데이터를 우선 사용한 후 API를 다시 호출하여 새로운 데이터를 응답 받으면 데이터를 교체한다.
  • 5000 :
    • 5초 이전 데이터를 요청한 경우: 최신 데이터로 판단하여 API를 다시 호출하지 않고 캐시된 데이터를 사용한다.
    • 5초 이후 데이터를 요청한 경우 : 캐시 된 데이터를 오래된 데이터로 판단 하여 캐시 된 데이터를 우선 사용한 후, API를 다시 호출하여 새로운 데이터를 응답 받으면 데이터를 교체하고 응답 받은 데이터를 캐시한다.

cacheTime

데이터를 얼마나 오랫동안 보관 할 것인지 나타내는 시간. ms단위로 저장되는데 기본값 은 5분(5 x 60 x 1000). 쿼리 인스턴스가 unmount 되면 데이터는 비활성화(inactive) 상태가 되는데, 비활성화 된 데이터는 cacheTime에 설정된 시간이 지난후 가비지 컬렉션이된다.

  • cacheTime이 지나기 전에 쿼리 인스턴스가 다시 mount되면, 데이터를 fetch하는 동안 캐시 데이터를 보여준다.
  • 5000:
    • 비활성화 된 데이터를 5초 이전에 요청한 경우: 캐시 된 데이터를 우선 사용한 후 API를 호출하여 새로운 데이터를 응답 받으면 응답 받은 데이터를 다시 캐시한다.
    • 비활성화 된 데이터를 5초 이후에 요청한 경우: 데이터가 이미 가비지 컬렉션 되었기 때문에, 캐시 데이터를 사용하지 못하고 API 응답 데이터를 기다린 후 데이터를 응답 받은 데이터를 사용하고 캐시한다.

refetchOnMout

const {isLoading, isFetching, data, isError, error} = useQuery(['super-hero'],
getSuperHero,{
	refetchOnMount: true
}

데이터가 stale 상태일 경우, mount마다 refetch를 실행하는 옵션 기본값 true

  • aways : 마운트 시마다 매번 refetch 실행
  • false : 최초 fetch 이후 refetch 하지 않는다.

refetchOnWindowFocus

const {isLoading, isFetching, data, isError, error} = useQuery(['super-hero'],
 getSuperHero,{
	refetchOnWindowFocus: true
}

데이터가 stale 상태일 경우 윈도우 포커싱 될때마다 refetch 옵션 기본값 true

  • aways : 항상 윈도우 포커싱 될 때마다 refetch를 실행한다.

Polling

const {isLoading, isFetching, data, isError, error} = useQuery(['super-hero'],
 getSuperHero,{
	refetchInterval:2000,
  	refetchIntervalInbackground:true
}

리얼타임 웹을 위한 기법으로 일정한 주기(특정시간)를 가지고 서버와 응답을 주고 받는 방식

  • refetchInterval: ms(시간)을 값으로 넣어주면 일정 시간 마다 자동으로 refetch
  • refetchIntervalInBackground : refetchInterval과 함께 사용하는 옵션, 탭/창이 백그라운드에 있는 동안 refetch(브라우저에 focus되지 않아도 refetch 시켜준다.)

enabled refetch

const {isLoading, isFetching, data, isError, error, refetch} = useQuery(['super-hero']
,getSuperHero,{
  enabled:false
})

const handleClickRefetch = useCallback(()=>{
refetch()
},[refetch])

return(
	<div>
  	{data?.data.map((hero:Data)=>(
  		<div key={hero.id}>{hero.name}</div>
	))}
    <button onClick={handleClickRefetch}>Fetch</button>
  	</div>
)

쿼리가 자동으로 실행되지 않도록 할 때 설정. false를 주면 자동으로 실행되지 않는다. useQuery리턴 데이터 중 status가 idle 상태로 시작한다.

  • refetch는 쿼리를 수동으로 다시 요청하는 기능이다.
  • 버튼 클릭이나 특정 이벤트를 통해 요청을 시도할 때 같이 사용된다.
  • enabled:false : queryClient가 쿼리를 다시가져오는 방법 중 invalidateQueries와 refetchQueries를 무시한다.

select

const { isLoading, isFetching, data, isError, error, refetch } = useQuery(
  ["super-hero"],
  getSuperHero,
  {
    onSuccess,
    onError,
   select(data){
     const superHeroNames = data.data.map((hero:Data)=>hero.name)
     return superHeroNames;
  }
);

return (
  <div>
    <button onClick={handleClickRefetch}>Fetch Heroes</button>
    {data.map((heroName: string, idx: number) => (
      <div key={idx}>{heroName}</div>
    ))}
  </div>
);

쿼리 함수에서 반환된 데이터의 일부를 변환하거나 선택할 수 있다.

KeepPreviousData

const fetchColors = async (pageNum:number) =>{
	return await axios.get(`http://localhost:4000/colors_limit=2&_page=${pageNum}`)
}

const {isLoading, isError, error, data, isFetching, isPreviousData}= useQuery(['colors', pageNum],
                                                                              ()=>fetchColors(pageNum),{
	keepPreviousData: true
})
  • true: 쿼리 키가 변경되어서 새로운 데이터를 요청하는 동안에도 마지막 data값을 유지한다.
  • 페이지네이션 기능을 구현할 때 편리하다. (캐싱되지 않은 페이지를 가져올때 깜빡거리는 현상 방지)
  • isPreviousData 값으로 현재의 쿼리 키에 해당하는 값인지 확인할 수있다. (아직 새로운 데이터가 캐싱 되지 않았따면, 이전 데이터이므로 true를 반환하고 새로운 데이터가 정상적으로 받아져 왔다면 이전데이터가 아니므로 false 이다.)

useQueries

  • 병렬쿼리란 동시에 여러 쿼리를 요청하는 방법. (동시성 극대화)
const queryResults = useQueries({
  queries:[
    {
      querykey: ['super-hero',1],
      queryFn : () => fetchSuperHero(1),
      staleTime:Infinity,
    },
  {
    querykey:['super-hero',2]
    queryFn : () => fetchSuperHero(2),
    staleTime:0,
   }
  ]
})

QueryClient

QueryClient 인스턴스는 React query에 유용한 기능을 담고 있다.

import {useQueryClient} from '@tanstak/react-query'

const Todos = ()=>{
	const QueryClient = userQueryClient()
}

쿼리 무효화 (invalidateQueries)

쿼리를 무효화 하여 데이터를 다시 가져오게 할 수 있다.

import {useQueryClient} from '@tanstak/react-query'

const Todos = ()=>{
	const QueryClient = userQueryClient()
    
    const mutation = useMutation({
      mutationFn : addTodo,
     onSuccess(data) {
      queryClient.invalidateQueries(["todo"]); // 이 key에 해당하는 쿼리가 무효화!
      console.log(data);
    },
    onError(err) {
      console.log(err);
    },
 })
}
  • 화면을 최신 상태로 유지하는 가장 간단한 방법
  • Mutation과 함께 사용 되는 경우 많다.

쿼리 취소

const query = useQuery(["super-heroes"], {
  /* ...options */
});

const queryClient = useQueryClient();

const onCancelQuery = (e) => {
  e.preventDefault();

  queryClient.cancelQueries(["super-heroes"]);
};

return <button onClick={onCancelQuery}>Cancel</button>;
  • 쿼리를 수동으로 취소하고 싶은 경우
    • 요청 시간이 오래 걸리는 경우 사용자가 취소버튼으로 중지하는 경우
    • HTTP 요청이 끝나지 않았을 때, 페이지를 벗어날 경우 불필요한 네트워크 리소스 개선
  • queryFn promise도 취소한다.

수동 쿼리 업데이트

const queryClient = useQueryClient()

//예제1
useMutation(addSuperHero, {
    onSuccess(data) {
      queryClient.setQueryData(["super-heroes"], (oldData: any) => {
        return {
          ...oldData,
          data: [...oldData.data, data.data],
        };
      });
    },
    onError(err) {
      console.log(err);
    },
});

//예제2 낙관적 업데이트
useMutation({
  mutaitionFn: uptateTodo,
  onMutate : async(newTodo) =>{
    //optimistic update 한것이 덮어써지지 않도록 쿼리 취소
  	await queryClient.cancelQueries({querykey:['todo']})
    // 에러 발생시 복원을 위해 기존 데이터 저장
	const previousTodos = queryClient.getQueryData(['todos'])
    //예상되는 변경 값으로 업데이트
    queryClient.setQueryData(['todos'],(old)=>[...old, newTodo])
    return {previousTodos}                        
  },
  // mutation이 실패하면 onMutate에서 반환된 context를 사용하여 롤백 진행
  onError:(err, newTodo, context)=>{
    // context를 통해 기존 값 쿼리 업데이트
    	queryClient.setQueryData(['todos'], context.previousTodos)
  }
   // 오류 또는 성공 후에는 항상 리프레쉬
  onSettled:()=>{
	queryClient.invalidateQueries({queryKey:['todos']})
 }
})

때때로 수동으로 쿼리를 업데이트 해 주는 것이 더 좋은 사용성을 제공할 수 있다.
qeuryClient.setQueryData를 사용하면 된다.

  • 낙관적 업데이트(Optimistic Update): Mutation으로 상태를 변경후 데이터 노출이 늦어진다면 수동으로 먼저 쿼리를 업데이트 하여 사용자에게 빠르게 변경된 결과를 제공하는 것. (인터넷 속도가 느리거나 서버가 느릴 때이다. 유저가 행한 액션을 기다릴 필요 없이 바로 업데이트되는 것처럼 보이기 때문에 사용자 경험(UX) 측면에서 좋다.)

쿼리 미리 가져오기

const perfetchNextPosts = async (nextPage:number)=>{
	const queryClient = useQueryCleint();
   await queryClient.prefetchQuery(
     ['posts',nextPage],
     ()=>fetchPost(nextPage),
     {
     	//options...
     }
   )
}

useEffect(()=>{
	const nextPage = currentPage +1
    if(netxtPage < maxPage){
    	prefetchNextPosts(nextPage)
    }
},[currentPage])

미래에 사용 될 수 있는 쿼리를 미리 가져올 수 다. queryClient.prefetchQuery로 가져오는 데이터가 이미 캐싱되어 있다면 데이터를 가져오지 않는다.

  • 미리 가져온 쿼리를 캐시를 해두기 때문에 Query를 통해 데이터를 가져올 때 캐시된 데이터를 사용하여 빠른 결과를 얻을 수 있다,
  • ex: 1페이지에서 2페이지로 이동했을 때 3페이지의 데이터를 queryClient.prefetchQuery로 미리 캐시해 둔다면 3페이지로 전환할 때 빠른 결과 값을 가져올 수 있다(페이지네이션 - ux측면에서 좋다.)

useMutation

const CreateTodo = () => {
  const mutation = useMutation(createTodo, {
    onMutate() {
      /* ... */
    },
    onSuccess(data) {
      console.log(data);
    },
    onError(err) {
      console.log(err);
    },
    onSettled() {
      /* ... */
    },
  });

  const onCreateTodo = (e) => {
    e.preventDefault();
    mutation.mutate({ title });
  };

  return <>...</>;
};
  • 서버의 데이터 post,patch,pus,delete와 같이 수정하고자 할때 사용한다.
  • onMutate :mutation 함수가 실행되기 전에 실행되고, mutation 함수가 받을 동일한변수가 전달된다.
  • onSuccess, onError : 성공시, 실패시
  • onSettled : 성공하든 에러가 발생되든 상관없이 마지막 실행

    mutation 과 mutationAsync 차이 (mutation 사용이 좀더 유리함.)

    • mutation : onSuccess,onError,onSettled 와 같은 콜백 함수로 처리 할수 있다.
    • mutationAsync: Promise 반환하기 때문에 then,Async await로 처리해야 한다.(에러핸들링을 직접 다뤄야 한다.)

참고
https://beomy.github.io/tech/react/tanstack-query-v4/#errorboundary
https://github.com/ssi02014/react-query-tutorial#infinite-queries

profile
그냥하기.😎
post-custom-banner

1개의 댓글

comment-user-thumbnail
2023년 7월 28일

좋은 글이네요. 공유해주셔서 감사합니다.

답글 달기