처음 v1(Angularjs) 프로젝트에서 정적페이지로만 이루어진 부분 먼저 v2(React)로 마이그레이션을 진행하던 중 상태관리 및 query fetching에 대한 효율적인 부분을 고민하던 중 ReactQuery가 좋은 대안이 될 수 있겠다고 판단했고 관련된 내용을 찾아보던 중 배민 주문팀 프로젝트에 성공적인 ReactQuery 도입과정을 세미나로 풀어낸 내용이 있어 해당 내용을 정리하였다.
당시 바쁘던 팀원들의 상태를 위해 그나마 빠르게 정리할 수 있도록 내용을 정리하고 공유했다.
유튜브 배민 세미나 영상 출처
https://www.youtube.com/watch?v=MArE6Hy371c
프론트 개발자로서 오너쉽을 가지고 관리를 해야하는 데이터들이다.
UI/UX 의 중요성과 함께 프로덕트 규모가 커짐에 따라 FE가 해야할 일들이 많아짐. → 관리할 상태가 많아짐
상태관리가 프로덕트 커짐에 따라 어려움도 커짐
상태는 시간에 따라 변화함 → 유저 반응
리액트의 단방향 특성으로 Props Drilling 문제도 존재
Redux와 MobX 등의 라이브러리로 해결하기도 함
배민 앱위에서 메인 프로덕트들이 돌아가고 있음.
FE프로덕트가 어디에서 돌아가고 있는지 궁금할 수 있음.
→ 장바구니부터 주문쪽, 결제 등등에서 많은 부분에서 웹뷰를 활용 중
이에 따라 자연스럽게 상태관리에 대한 고민을 함 → 우리와 비슷한 과정
Store에 수많은 API통신 코드, isFetching, isError 등 API 관련 상태, 반복되는 구조의 API 통신 코드 → 리덕스의 전형적인 단점. 수많은 코드량
지금에와서 생각해봐도 상태관리를 위한 적정한 기술인가? 에 대한 의문은 있음.
만약 배민앱으로 주문이 들어온 상태
기능도 좋고, 파워풀하다. 뭔가 자신감 개쩜;
써본 입장에서 생각해보니까 어느정도 동의하는 바.
지가 알아서 백그라운드에서 잘해주고 훅기반의 심플하게 사용 가능하고 꽤 강력하고 괜찮은 옵션이 많다.
하나하나 살펴보자
React Query는 zero-config로 즉시 사용가능, But 원하면 언제든 config도 커스텀 가능!
첫인상은 뭔가 간당해보이기는 함. 딱히 config도 없고 코드도 훅스 같고??
React에서 쓰려면 QueryClientProvider 필수!
공식 문서에서 짚은 3가지 핵심 개념
추후 공식문서 참고하여 자세하게 더 공부해보자!
CRUD 중 Reading만 사용할거입니다.
예제.
import { useQuery } from 'react-query' function App() { const info = useQuery('todos', fetchTodoList) } // 'todos' => Query Key // fetchTodoList => Query Function
Key, Value 맵핑구조를 생각하면 된다.
Key가 관리되는 형태는 두가지 String, Array
// A list of todos useQuery('todos', ...) // queryKey === ['todos'] // Something else, Whatever! useQuery('somethingSpecial', ...) // queryKey === ['somethingSpecial']
// An individual todo useQuery(['todo'], 5], ...) // queryKey === ['todo', 5] //An individual todo in a "preview" format useQuery(['todo'], 5, { preview: true }], ...) // queryKey === ['todo', 5, { preview: true }] // A list of todos that are "done" useQuery(['todo'], { type: 'done' }], ...) // queryKey === ['todo', { type: 'done' }]
이 녀석은 쉽게 얘기해서 우리도 현재 data fetching 할 때 Promise 함수로 만들어서 쓰죠?? 그런 녀석입니다.
Promise를 반환! → 데이터 resolve하거나 error를 throw
useQuery('fetchOrder', () => fetchOrder(orderNo), options) export const fetchOrder = (orderNo: string): Promise<...> => orderHistoryApiRequester .get(`url`) .then(res => res.data);
자 이제 그럼 어떻게 쓰는지는 알겠고,
data: 마지막으로 성공한 resoloved된 데이터 (response)
error: 에러가 발생했을 때 반환되는 객체
isFetching: Request기 in-flight 중일 때 true
status, isLoading, isSuccess, isError 등등 : 모두 현재 query의 상태
→ 이거 redux 써봤으면 얼마나 중간중간 액션 상태로 만들어주는게 극혐인지 알고 있을 것…ㅠ
refetch: 해당 query refetch 하는 함수 제공
→ ex) 뭐 이미 리액트 쿼리가 잘 알아서 가져와주겠지만 특정 버튼을 눌렀을 때 새로운 쿼리를 가져왔으면 할 때? 사용하면 됨. 현재 이모밥줘 사이트에 메뉴를 제출하면 refetch 해주고 있음.
remove: 해당 query cache에서 지우는 함수 제공
자 그래서 이제 뭐가 편한지도 알겠고 뭐 반환하는지도 알겠고 어느정도 뭔지 알겠다.
근데 아까 config 커스텀 된다면서??
아까 슬쩍 지나간 코드
useQuery('fetchOrder', () => fetchOrder(orderNo), **options**)
options에 들어가는 녀석들
역시 많다.
쓸만한 것들을 정리해보자.
onSuccess, onError, onSettled: query fetching 성공/실패/완료 시 실행할 Side Effect 정의
enabled: 자동으로 query를 실행시킬지 말지 여부 → false 시 컴포넌트 마운트단계에서 실행x
retry: query 동작 실패 시, 자동으로 retry할지 결정하는 옵션 → 기본값 3번을 자동으로 실행
select: 성공 시 가져온 data를 가공해서 전달 가능 → data.data.name 등을 방지
keepPreviousData: 새롭게 fetching 시 이전 데이터 유지 여부
refetchInterval: 주기적으로 refetch 할지 결정하는 옵션 → polling 구현 시 엄청 스무스하게 자동으로 처리됨.
react-query 공식문서의 내용은 아니지만 배민에서 직접 사용하다보니 해당 부분 처리를 별도의 파일로 관리하는 것이 컴포넌트의 사용성이 더 좋았다고 생각하고 있어 해당 부분을 현재 분리하여 관리 중이라고 함.
알아서 잘 된다! (동적으로 하려면 다른 방법이 있음.)
function App () { // 아래의 쿼리들을 병렬적으로 알아서 잘 처리될 것이다! 걱정 ㄴㄴ const usersQuery = useQuery('users', fetchUsers) const teamsQuery = useQuery('teams', fetchTeams) const projectQuery = useQuery('projects', fetchProjects) ... }
기술 블로그에 달렸던 질문들중에 답변을 해보려고 함.
1번의 경우 매우 간단한 내용이고 2번으로 처리하는 경우 코드의 복잡도가 너무 많이 올라가게되고 이 부분을 최대한 지양하려고 함.
그래서 간단한 부분이라면 1번으로 처리.
조금 복잡한 컨디션의 조건을 갖고 있는 경우에서의 처리는 2번으로 추천. → 다만 해당 부분으로 처리 시 부가적인 상태들을 관리해야한다는 것은 좋은 부분은 아닌 듯함.
2번 질문은 리덕스에서 혼용하여 사용 시에 대한 질문이였기에 생략.
const mutation = useMutation(newTodo => { return axios.post('/todos', newTodo) })
useQuery 보다 더 심플하게 Promise 반환 함수만 있어도 된다!
→ 단, Query Key 를 넣어주면 devtools에서 볼 수 있기 때문에 실무에선 키값을 넣어주는 것을 개인적으로 추천함.
mutate: mutation을 실행하는 함수
mutateAsync: mutate와 비슷. But Promise를 반환
reset: mutation 내부 상태를 clean 하게 만듦
나머진 특별히 설명 필요x useQuery랑 비슷하게 동작하고 오히려 반환하는 객체 안의 내용은 더 적음.
onMutate: 본격적인 Mutation 동작 전에 먼저 동작하는 함수, Optimistic update 적용할 때 유용
<aside> ❓ <strong>Optimistic update?</strong>페이스북의 좋아요 기능을 사용한다 했을 때, 유저가 좋아요 버튼을 눌렀을 때 client는 해당 글의 좋아요 api가 성공했을 것이라고 예상하고 미리 파란색으로 좋아요 버튼에 대한 UI를 업데이트함. 다음과 같은 작업을 말하고 이후 api가 성공적으로 동작했다면 UI를 유지하고 실패한 경우에 한해 UI를 롤백함. 이 또한 기능으로 처리가 가능하다.
</aside>간단히 queryClient를 통해 invalidate 메소드를 호출하면 끝!
// Invalidate every query in the cache queryClient.invalidateQueries() // Invalidate every query with a key that starts with 'todos' queryClient.invalidateQueries('todos')
이러면 해당 Key를 가진 query는 stale 취급되고, 현재 rendering 되고 있는 query 들은 백그라운드에서 refetch 된다.
2부 시작.
우리들 모르는 사이에 등장한 옵션들
사실 아까 예제에서 잠깐 등장한 Option에 cacheTime, staleTime도 있었고,
refetchOnWindowFocus, refetchOnMount 같은 것도 있었음.
얘네들을 리액트쿼리가 어떻게 처리하고 있을까? 아래와 같은 아이디어를 차용했다고 생각하면 좋겠다.
HTTP Cache-Control Extensions for Stale Content
stale-while-revalidate
백그라운드에서 stale response를 revalidate 하는 동안 캐시가 가진 stale response 를 반환
위와 같은 아이디어대로 동작하게 된다면 서버요청응답으로 인한 Latency가 숨겨질 것!
이렇게하여 나온 것이 react-query, swr, etc 등등…
cacheTime: 메모리에 얼마만큼 있을건지 결정하는 요소(해당 시간 이후 GC에 의해 처리, default 5분)
staleTime: 얼마의 시간이 흐른 후에 데이터를 stale 취급할 것인지(default 0)
refetchOnMount / refetchOnWindowFocus / refetchOnReconnect → true 이면 Mount / window focus / reconnect 시점에 data 가 stale 이라고 판단되면 모두 refetch (모두 default true)
화면에 있다가 사라지는 query
알아서 하는 것들이 있어서 좋지만 주의도 해야함.
어떻게 Server State들을 전역상태처럼 관리할까??
QueryClient 내부적으로 Context를 사용하고 있음. 깃허브 코드 확인.
느껴진 바뀐 점으로.
최신 npm 동향
트렌드는 나쁘지 않아보임.
이런분들에게 추천합니다.