본문 바로가기
부트캠프 개발일지 2023-2024/React 리액트

[10주차] 리액트심화: React Query 이론과 개념

by whereanna00 2023. 12. 6.

React Query 실습도 참고

 

[10주차] 리액트심화: React Query 실습 (초기세팅, 데이터가져오기, 데이터입력하기)

이론과 개념도 함께 참고! [10주차] 리액트심화: React Query 이론과 개념 React Query 1 react query 란, 서버 상태 관리를 쉽게 할 수 있도록 도와주는 라이브러리이다. 서버상태(server state)는 서버에 요청

whereannalee.tistory.com


React Query 1

react query 란, 서버 상태 관리를 쉽게 할 수 있도록 도와주는 라이브러리이다.

서버상태(server state)는 서버에 요청하고 응답받는 모든 과정과 연관된 데이터들을 의미한다.

 


 

서버상태관리는 무엇을 하는 것을 의미하는가?

What is server state management?

 

1. fetching : 서버에서 데이터를 받아오기

2. cachning : 서버에서 받아온 데이터브라우저 상에서 따로 보관하여, 동일한 데이터가 단 시간 내에 다시 클라이언트 쪽에서 요청이 왔을 때 또 한번의 서버 데이터 요청 없이 브라우저 상에서 보관된 데이터에서 꺼내 쓰기

3. synchronizing : db에 있는 데이터와 브라우저상에 보관되어 있는 캐시 데이터(서버상태 server state)를 동일하게 만드는 것 = 동기화

4. updating : db에 있는 데이터를 변경하는 것 (mutation & invalidateQueries)

 


 

리액트 쿼리(react-query) 를 쓰기 전과 후

BEFORE

// React Query 미사용 시
const [todos, setTodos] = useState([]);
const [isLoading, setIsLoading] = useState(false);

const getTodos = async () => {
  setIsLoading(true);
	const data = await axios.get(`${API_URL}/todos`).then(res => res.data);
	setTodos(data);
  setIsLoading(false);
}
useEffect(() => {
	getTodos();
}, []);

 

랜더링 순서: 코드 위~아래 return 까지 내려갔다가 (useEffect 제외) -> 최초 랜더링 완료 후, useEffect 실행 -> useEffect 안에 getTodos 호출 실행 -> getTodos 함수 실행 -> 동기적으로 axios를 이용해 db 로부터 데이터를 가져오기 -> todos 상태(state) 변경 -> ...

 

 

AFTER

// React Query 사용 시
const getTodos = () => axios.get(`${API_URL}/todos`).then(res => res.data);

const { data: todos, isLoading } = useQuery(["todos"], getTodos);

 

위 예시를 보면 결국, 리액트 쿼리를 사용하는 것이

  • 코드 가독성이 좋다
  • 코드가 간결하다

는 장점을 가진다는 것을 알 수 있다.

 


 

stale-while-revalidate (SWR) 전략

신규 데이터가 도착하기까지의 시간동안 기존 캐싱되어 있는 데이터를 사용하도록 하는 전략

 

 

이 전략은 서버의 헤더응답 설정 Cache-Control 에서 아이디어가 기원되었다.

클라이언트가 0~1초 사이에 다시 데이터를 요청하면, 서버 호출없이 캐시 데이터를 바로 사용





클라이언트가 1 ~ 60s 사이에 다시 데이터 요청하면, 일단 캐시 데이터를 사용하고 서버에서 신규데이터를 주면 그것으로 교체

 

Cache-Control: max-age=1, stale-while-revalidate=59

 

 


 

캐시 데이터가 보관되는 위치

 

캐시 데이터는 QueryClientProvider에 저장된다.

이 QueryClientProvider는 내부적으로 React Context API를 사용한다. 따라서 QueryClientProvider 아래에 있는 모든 자식 컴포넌트들은 캐시 데이터에 접근할 수 있다.

 

// App.jsx
const queryClient = new QueryClinet();

const App = () => {
	return (
		<QueryClientProvider client={queryClient}>
			<Router />
		</QueryClientProvider>
	);
}

 

 


 

리액트쿼리 데이터 흐름

 

A_Component -> todos 데이터 요청시

더보기

1. <A_Component> 에서 useQuery 실행

2. ["todos"] 라는 Query Key에 대한 context안에 있는 캐시 데이터를 달라고 요청

3. 하지만 현재 context에는 todos에 대한 데이터가 없는 상태.

{todos: 데이터없음}

 

4. 따라서 context -> A_Component에 undefined 반환

5. 그 다음에, useQuery의 두번째 인자, query function인 getTodos가 실행

6. DB 에게 todos에 대한 데이터를 달라고 요청

7. 그럼 DB (서버)에 있는 todos 데이터를 ["todos"]에 대한 데이터로 캐싱처리(새 데이터로 바꿔놓는다) *cachy context

8. 리랜더링 (=함수컴포넌트를 재 호출한다는 의미)을 하면서 새롭게 업데이트 된 데이터를 A_Component의 {data}로 넣어준다.

 

 

A_Component 에서 B_Component로 이동 후, todos 데이터 요청시

더보기

1. <B_Component>에서 useQuery 실행

2. ["todos"] 라는 Query Key에 대한 context안에 있는 캐시 데이터를 달라고 요청

3. context에서 B_Component로 캐시 데이터를 전달

(왜 이때 캐시 데이터가 있나? 이미 A 컴포넌트에서 getTodos(query function)을 실행해놨기 때문에)

4. 그 다음에, useQuery의 두번째 인자, query function인 getTodos가 실행

5. DB 에게 todos에 대한 데이터를 달라고 요청

6. 그럼 DB (서버)에 있는 todos 데이터를 ["todos"]에 대한 데이터로 캐싱처리(새 데이터로 바꿔놓는다) *cachy context

7. 리랜더링 (=함수컴포넌트를 재 호출한다는 의미)을 하면서 새롭게 업데이트 된 데이터를 B_Component의 {data}로 넣어준다.

 

 

C_Component에서  useMutation을 이용해, addTodo라는 mutation function 실행시

더보기

1. DB에 3개의 todo 요소가 있다고 가정한다면

2. addTodo 를 실행시키면 -> DB의 요소가 + 1 씩 늘어난다

3. 하지만 아직 이 데이터는 context(캐시 context에) 반영이 안된상태

=> 우리는 UI를 그릴때 캐시 context에 있는것만 구독을 하면서 그리기 때문에, 결국 캐시 context를 변경되어야 함.

4. 따라서 onSuccess를 쓴다.

=> 과정설명: addTodo가 성공적으로 DB에 반영이 되면 그 때 queryClient.invalidateQueries를 실행시킨다. *무효화라는 뜻

즉, 캐시 context의 query key가 "todos"인 것을 무효화 해라! 라는 명령.

그리고 새걸로 교체해라!

5. C_Component의 queryClient.invalidateQueries에서 인자로 넘기고 있는 ["todos"]에 해당하는 getTodos를 호출

6. getTodos를 호출하면, C_Component에서 DB에게 todos에 대한 데이터를 달라고 요청

7. 그럼 DB (서버)에 있는 todos 데이터를 ["todos"]에 대한 데이터로 캐싱처리(새 데이터로 바꿔놓는다) *cachy context

 


 

Tanstack Query

React Query v4 부터 라이브러리 이름이 Tanstack Query로 변경되었다. 리액트, Vue 등 다양한 SPA 프레임워크에도 적용할 계획이라고 전해진다.

 

설치

// "@tanstack/react-query": "^4.29.19"
yarn add @tanstack/react-query

 

유의사항 : query key 배열형태로 써주기

useQuery("todos", getTodos); ❌ 에러 발생

useQuery(["todos"], getTodos); ✅ 정상 동작

 


 

UseQuery의 첫번째 인자 : Query Key

Query Key 는 unique 해야 한다.

QK는 위 예제처럼 한 단어, 배열의 형태, nested 객체일 수도 있다. Key라는 말이 의미하듯, 모든 Query keys는 Unique해야 한다.

const query1 = useQuery('qk', api); // unique
const query2 = useQuery('qk2', api); // not unique
const query3 = useQuery('qk2', api); // not unique
  • 단어 한 개로 이루어진 Query Keys

useQuery('todos', ...) 는 queryKey === ['todos'] 와 같다.

 

  • 배열 형태의 Query Keys
// 💥주의! key는 표현이 그렇다는거지, api 로직과는 관련이 없다.

// ID가 5인 todo 아이템 1개
useQuery(['todo', 5], ...)
// queryKey === ['todo', 5]

// ID가 5인 todo 아이템 1개인데, preview 속성은 true야
useQuery(['todo', 5, { preview: true }], ...)
// queryKey === ['todo', 5, { preview: true }]

// todolist 전체인데, type은 done이야
useQuery(['todos', { type: 'done' }], ...)
// queryKey === ['todos', { type: 'done' }]

 

 

 

연습

1. useQuery(['todos', { status, page }], ...)
2. useQuery(['todos', { page, status }], ...)
3. useQuery(['todos', { page, status, other: undefined }], ...)

=> 1번과 2번은 query key가 같으므로 유니크하지 않다.

=> 3번은 1번과 2번으로부터 유니크하다. other라는 key를 새로 가지고 있기 때문

 

 

1. useQuery(['todos', status, page], ...)
2. useQuery(['todos', page, status], ...)
3. useQuery(['todos', undefined, page, status], ...)

=> 1,2, 3번 모두 각각으로부터 유니크하다.

=> 1번과 2번이 서로 다른 query key 인 이유는 배열로 이루어진 query key에는 요소의 순서가 중요하기 때문에 서로 다름을 가르는 기준이 되기때문이다.


 

UseQuery의 두번째 인자 : Query Functions

Query Functions은 promise 객체를 return 한다.

그리고 그 promise는 resolve 또는 error를 반환한다.

 

오류가 발생한 경우 -> 그에 맞는 적절한 오류 처리 관련 로직을 삽입해서 처리를 해야한다.  axios, fetch, graphql 중 어떤 방법을 이용하던지 적절한 오류 처리를 통해 사용자가 혼란에 빠지지 않도록 해줘야한다. try ~ catch


UseQuery의 결과물 (=반환값 return)

usequery의 반환값은 객체이다.

그 객체 안에는 isLoading, isError, isSuccess, data 등이 들어있고

구조분해할당을 통해 꺼내 쓰면 용이하다.

 

  1. 호출시작시, isLoading=true
  2. 조회 결과 오류시 isError=true, isLoading=false (error 객체를 통해 좀 더 상세한 오류 내용을 확인할수있음)
  3. 조회 결과 정상시 isSuccess=true, isLoading=false.  (data 객체를 통해 좀 더 상세한 조회 결과를 확인할 수 있음)

Mutations

- useMutation(비동기함수, { onSuccess : () => {}})

- mutation을 사용할 때는 mutation.mutate() 안에 들어가는 인자는 한 개의 변수 또는 한개의 객체만 들어갈 수 있다.

mutation.mutate(newTodo);

 

이 객체는 4 가지의 상태를 가진다.

{

isIdle: 

isLoading:

isError:

isSuccess: data {...}

}


 

React Query의 라이프사이클

 

 

캐시 데이터의 라이프사이클
active = 캐시 데이터가 화면에서 사용되고 있다는 뜻
inactive = 캐시 데이터가 화면에서 사용되지 않고 있다는 뜻

1. query function을 통해서 fetching으로 db로부터 데이터를 받아오면, 그 데이터는 fresh 또는 stale이라는 상태를 갖게 된다.
2. fresh = 더 이상 새거가 필요하지 않다. 이미 uptodate 되어 있는 상태. -> query function 실행 시킬 필요 없음.
3. stale = 헌 것. outdated 된 상태. -> 계속해서 query function 실행 시키면서 db로부터 데이터를 받아와야 한다.

inactive 상태가 어느정도 되면 cacheTIme 만료로 garbage collector에 의해 deleted (삭제)가 된다.

 

default config (기본설정)

** stale time : 유통기한과 같다. stale time이 0 이 되면 'stale' 상태가 된다. 보통 디폴트값은 0 이고, 만약 설정을 해주고 싶다면 db.json 에서 아래와 같이 설정해줄 수 있다. stale time 이 있다면, fresh 하기때문에 더 이상 서버에 요청하지 않는다.

**refetchOnMount: true => mount했을 때 refetch 해라! 디폴트는 true 이다. (예시: 다른페이지에 갔다가 다시 홈화면으로 왔을 때 다시 fetch를 해서 데이터를 가져오냐 아니냐)

**refetchOnWindowFocus: true => 브라우저를 클릭할 때 refetch가 되는 것(true), 안되게 막는것(false). 디폴트는 true.


** refetchOnReconnect: true => Network 가 끊겼다가 재연결 되었을 때 stale data를 refetch 자동 실행. 디폴트는 true.


** cacheTime: 5분 (1000 * 60 * 5 ms) => 사용하지 않는 캐시데이터를 언제 삭제할 것인지를 정해주는 것. 사용하지 않는다는 것, 상태가 inactive 상태! 디폴트는 5분.
5초로 설정한 예시 코드

**retry: 3 => 재시도를 총 3번까지 한다. 서버상태가 항상 정상적일 순 없다. 따라서 불안정한 상태일때 브라우저에 바로 에러를 띄우는게 아니라 3번까지는 서버에 재요청을 한다는 뜻. 디폴트는 3회.

 

 


 

staleTime VS cache time

 

staleTIme = 얼마의 시간이 흐른 뒤에 stale(헌 것으로) 취급할 건지 (default: 0)

cacheTime = 사용하지 않는 데이터를 언제 삭제 할 것인지, inactive 된 이후로 메모리에 얼마만큼 있을건지 (default: 5분, cacheTime 0되면 삭제처리)

 

 

 

staleTime 과 stale / fresh의 관계

  • staleTime > 0 이면, fresh data
  • staleTime = 0 이면, stale data

 

" 하나의 쿼리 인스턴스(=하나의 query key =각각의 캐시데이터)마다 위와 같은 Lifecycle을 가진다. "

 


 

isLoading vs isFetching

 

isLoading

- 캐시 context 안에 아무것도 없었을 때, isLoading이 true가 된다. (캐시데이터가 있을 경우 isLoading=false, isFetching =true)

- 새로운 캐시 데이터를 서버에서 받고 있는 지 여부.

 

isFetching

- 캐시 context 안에 데이터가 있던지 없던지 상관없이, 서버에서 데이터를 받고 있는지 여부. query function으로부터 서버에서 데이터를 받고 있는지의 여부. 즉, query funciton이 실행되는 지의 여부.

 

생각해보기

Q. 메인페이지와 상세페이지가 모두 useQuery(”todos”, getTodos) 를 가질 때, 메인페이지 → 상세페이지 → 메인페이지 순으로 이동할 때 메인페이지 재 마운트 시의 isLoading, isFetching 의 콘솔로그값은?

// MainPage.jsx
function MainPage() {
	const { isLoading, isFetching } = useQuery("todos", getTodos);
	console.log("isLoading: ",isLoading);
	console.log("isFetching: ", isFetching);
}​



정답!

A. isLoading = false, isFetching = true

 

 


 

useQuery 실행 시 cacheTime > 0 인 것과 queryFn 실행간의 관계

 

useQuery를 실행하면 queryFunction이 실행되는데 이는 cacheTime > 0 인 것과 전혀 상관이 없다.

대신 staleTime과 관계가 있다. 

**cacheTime = 사용하지 않는 캐시데이터를 언제 삭제할 것인지를 정해주는 것. 

**StaleTIme = 유통기한과 같다. stale time이 0 이 되면 'stale' 상태가 된다.

 

만약 cacheTIme > 0 이면 (cacheTime 숫자 동안만큼은) 캐시데이터가 존재하고

-> 이 경우 useQuery를 실행할 때 stale data(헌 데이터)를 우선 받고

-> staleTime이 0이면(그럼 그 데이터는 stale data가 되버린다) queryFunction을 실행한 리턴값으로 리렌더링 하면서 바꿔준다.

 

만약 cacheTime이 0이 되면 캐시 데이터가 삭제되기 때문에, 이 경우 useQuery로 data를 호출할때 undefined값을 우선 받고

-> queryFunction을 실행한 리턴값으로 리렌더링 하면서 바꿔준다.

 


useQuery 에서 자주 사용하는 옵션들

 

1. enabled

useQuery의 (첫번째 인자 : query key, 두번째 인자 : query function, 세번째 인자 : query option)

useQuery(["todos"], getTodos, { enabled: true })

 

- enabled는 boolean type(true or false)만 받을 수 있다.

- enabled = true일 때만 queryFunction이 실행된다.

- enabled 옵션이 없을 경우, 기본값으로 true 로 생각하고 useQuery가(=queryFunction인 getTodos가) 자동실행됨.

 

그런데 이렇게 자동적으로 실행시키고 싶지 않을 때가 있을 것이다.

그럴때는 아래 적용 예제 1 처럼 사용하면 된다.

 

적용 예제 (1) : Disabling/Pausing Queries (이벤트 발생 시에만 수동 실행하고 싶을 때)

사용자가 submit을 했을 때만 getTodos가 실행되었으면 좋겠을 때!

const { data, refetch } = useQuery(["todos"], getTodos, {
  enabled: false
});

return (
	<div>
    <button onClick={() => refetch()}>데이터 불러오기</button>
  </div>
);

 

 

useQuery가 2개이상이고 순서를 주고 싶을 때

적용 예제 (2)  : Dependent Queries (useQuery 2개 이상이며 실행순서 설정 필요할 때)

// Dependent Query 예제 (순차적 query 실행)
// Get the user
const { data: user } = useQuery({
  queryKey: ['user', email],
  queryFn: getUserByEmail,
})

const userId = user?.id // optional Chaining : user가 없으면 undefined, 있으면 id 까지 접근

// Then get the user's projects
const {
  status,
  fetchStatus,
  data: projects,
} = useQuery({
  queryKey: ['projects', userId],
  queryFn: getProjectsByUser,
  // The query will not execute until the userId exists
  enabled: !!userId //userId가 참이면 그때서야 queryFn를 실행시킨다는 뜻
})
// 여기서 !!userId 는 Boolean(userId)와 같습니다.
// 만약 userId가 undefined라면, !!userId는 false 이다.

 

userId가 참이면 그때서야 queryFn를 실행시킨다는 뜻

 

 

2. select

 

import { useQuery } from 'react-query'

function User() {
  const { data } = useQuery(['user'], fetchUser, {
    select: (user) => user.username,
  })
  return <div>Username: {data}</div>
}

 

cache context 안에 fetchuser에 의한 값을 select의 매개변수로 넣어줄 수 있다.

queryFn 에 의해 리턴된 값을 변형시킨 후에 useQuery 의 리턴 data로 넘겨줌.

그리고 난 다음, user.name을 {data}로 넘겨준다.

(단, cache data 는 queryFn 에서 리턴받은 값 그대로임)

 

 

 


 

 

Important Defaults | TanStack Query Docs

Query results by default are structurally shared to detect if data has actually changed and if not, the data reference remains unchanged to better help with value stabilization with regards to useMemo and useCallback. If this concept sounds foreign, then d

tanstack.com

 

 

Devtools | TanStack Query Docs

Wave your hands in the air and shout hooray because React Query comes with dedicated devtools! 🥳 When you begin your React Query journey, you'll want these devtools by your side. They help visualize all of the inner workings of React Query and will like

tanstack.com

 

728x90
반응형