안녕하세요. Narvis2 입니다.
이번 시간에는 서버의 값을 client 에 가져오거나 캐싱, 값 업데이트, 에러 핸들링 등 비동기 과정을 더욱 편하게 하는데 사용하는 react-query에 대하여 알아보겠습니다.
🚩 React-Query
- 🍬 서버의 값을 client 에 가져오거나 Caching, value updating, error handling 등 비동기 과정을 더욱 편하게 하는데 사용됨
- 서버와 클라이언트를 분리합니다.
🍀 1. React-Query 장점
- 캐싱
get을 한 데이터에 대해update를 하면 자동으로get을 다시 수행한다.- Ex 👉 게시판의 글을 가져왔을 때 게시판의 글을 생성하면 게시판 글을
get하는api를 자동으로 실행할 수 있음 - 데이터가 오래되었다고 판단되면 다시
get(invalidateQueries) - 동일 데이터를 여러번 요청하면 한번만 요청한다. (Option 에 따라 중복 호출, 허용 시간 조절 가능)
- 무한 스크롤
Infinite Queries를 사용하여 pagination 처리 가능
- 비동기 과정을 선언적으로 관리가 가능하다.
- react 의
Hook과 사용하는 구조가 비슷하다.
🍀 2. useQuery
- 데이터를
get하기 위한api(post, update 는useMutation을 사용) - 첫 번째 파라미터
Unique Key가 들어감, 요청의 결과물이 특정 변수에 따라 달라진다면 꼭Unique key에 포함해야 함Unique key는 String 과 []을 받는다. 배열로 넘기면 0번 값은 String 값으로 다른 Component에서 부를 값이 들어가고 두 번째 값을 넣으면 Query 함수 내부에 파라미터로 해당 값이 전달됨
- 두 번째 파라미터
- 비동기 함수(api 호출 함수)가 들어감 (
promise가 들어가야 함) - 이 Component 가 랜더링될 때 해당 함수를 호출하고, 이에 대한 상태가 관리됨
- 비동기 함수(api 호출 함수)가 들어감 (
- 첫 번째 파라미터로 설정한
Unique Key는 다른 Component 에서도 해당key를 사용하면 호출 가능 Return값- Api 요청의 성공, 실패 여부 및 Api 결과 값을 포함한 객체
status👉 API의 요청 상태를 문자열로 나타냄loading👉 아직 데이터를 받아오지 않고, 현재 데이터를 요청 중error👉 오류 발생success👉 데이터 요청 성공idle👉 비활성화된 상태(따로 설정해 비활성화한 경우)
isLoading👉 status === ‘loading’ 과 같음isError👉 status === ‘error’ 와 같음isSuccess👉 staus === ‘success’ 와 같음isIdle👉 staus === ‘idle’ 과 같음error👉 오류가 발생했을 때 오류 정보를 가지고 있음data👉 요청 성공한 데이터를 가리킴isFetching👉 데이터가 요청 중일 때 true가 됨데이터가 이미 존재하는 상태에서 재요청할 때
isLoading은 false 이지만,isFetching은 true 임
refetch👉 다시 요청을 시작하는 함수
- 비동기로 작동
- 여러개의 비동기
Query가 있다면useQuery보다는useQueries를 사용하는 것이 좋음
- 여러개의 비동기
enabled를 사용하면useQuery를 동기적으로 사용 가능useQuery의 3번째 인자로Option값이 들어가는데 그Option의enabled에 값을 넣으면enabled값이true일때useQuery를 실행함
useQuery Optionenable👉 boolean 타입의 값을 설정, 이 값이 false 이면 Component 가 마운트될 때 자동으로 요청하지 않음. refetch 함수로만 요청이 시작됨retry👉 boolean or number or (failureCount: number, error: TError) => boolean 타입의 값을 설정- 요청이 실패했을 때 재요청할지 설정할 수 있음
이 값을
true로 했을때 👉 실패했을 때 성공할 때까지 계속 반복 요청이 값을
false로 했을때 👉 실패했을 떄 재요청하지 않음이 값을
3으로 하면 👉 3번까지만 재요청이 값을
함수 타입으로 설정하면 👉 실패 횟수와 오류 타입에 따라 재요청할지 함수 내에서 결정할 수 있음
retryDelay👉 number or (retryAttempt: number, error: TError) => number 타입 값을 설정- 시간 단위는 ms(밀리세컨드 0.001초)임
기본값 👉 (retryAttempt) => Math.min(1000 * 2 ** failureCount, 30000)
실패 횟수 n 에 따라 2의 n 제곱 초만큼 기다렸다가 재요청하고 최대 30초까지 기다림
staleTime👉 데이터의 유효 시간을 ms 단위로 설정, 기본값 0 (데이터를 조회한 순간 데이터는 바로 유효하지 않게 됨)cacheTime👉 데이터의 캐시 시간을 ms 단위로 설정, 기본값은 5분, 캐시 시간은 Hook을 사용하는 Component 가 언마운트되고 나서 해당 데이터를 얼마나 유지할지 결정함🍀 참고 🍀
staleTime과cacheTime의 차이 👇useQuery를 사용할 때staleTime옵션은 기본값이 0 임. 즉, 데이터를 조회한 순간 데이터는 바로 유효하지 않게됨. 데이터가 유효하지 않다면 기회가 주어졌을 때 다시 요청하여 데이터를 최신화 해야함- 재요청 기회가 주어지는 시점 : 똑같은
Cache key를 사용하는useQuery를 사용중인 Component 가 마운트될 때 cacheTime은useQuery Hook을 사용한 Component가 언마운트되고 나서 해당 데이터를 얼마 동안 유지할지에 대한 설정, 기본값은 5- 만약
useQuery를 사용한 Component 가 언마운트되고 나서 5분안에 다시 마운트된다면 isLoading 값이 true로 되지 않고, 처음 렌더링하는 시점부터 data 값이 이전에 불러온 데이터로 채워져 있게 된다. 그리고staleTime에 따라 해당 데이터가 유효하다면 재요청하지 않고, 유효하지 않다면 재요청한다.
refetchInterval👉 false or number 타입값을 설정- 이 설정으로 n초마다 데이터를 새로고침하도록 설정할 수 있음
- 시간 단위는 ms 임
refetchOnmount👉 boolean or ‘always’ 타입의 값을 설정- 이 설정으로 Component가 마운트될 때 재요청하는 방식을 설정할 수 있음
기본 값: true
true일 때는 데이터가 유효하지 않을 때 재요청함
false일 때는 Component가 다시 마운트되어도 재요청하지 않음
‘always’일 때는 데이터의 유효 여부와 관계없이 무조건 재요청함
onSuccess👉 (data: Data) => void 타입의 함수를 설정, 데이터 요청이 성공하고 나서 특정 함수를 호출하고 싶을 때 사용onError👉 (error: Error) => void 타입의 함수를 설정, 데이터 요청이 실패하고 나서 특정 함수를 호출하고 싶을 때 사용onSettled👉 (data?: Data, error?: Error) => void 타입의 함수를 설정, 데이터 요청의 성공 여부와 관계없이 요청이 끝나면 특정 함수를 호출하도록 설정initialData👉 Data() => Data 타입의 값을 설정, Hook에서 사용할 데이터의 초깃값을 지정하고 싶을 때 사용refetchOnWindowFocus👉React-Query는 사용자가 사용하는 윈도우가 다른 곳을 갔다가 다시 화면으로 돌아오면 이 함수를 재실행함, 그 재실행 여부 옵션
예제 👇 기본 설정 > index.js 에 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(documnet.getElementById("root"));
root.Render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={true} />
<App />
</QueryClientProvider>
</React.StrictMode>
);
예제 👇
useQuery사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const Todo = () => {
const { isLoading, isError, data, error } = useQuery("todos", fetchTodoList, {
refetchOnWindowFocus: false,
retry: 0,
onSuccess: (data) => {
console.log(data);
},
onError: (e) => {
console.log(e.message);
},
});
if (isLoading) {
return (
<View>
<Text>{"Loading"}</Text>
</View>
);
}
if (isError) {
return (
<View>
<Text>{"에러"}</Text>
</View>
);
}
// Something...
};
🍀 3. useQueries
useQuery를 비동기로 여러개 실행할 경우 사용promise.all과 마찬가지로 하나의 배열에 각Query에 대한 상태 값이 객체로 들어옴
예제 👇
useQueries사용 (lol 룬과 스펠을 받아오는 예시)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const result = useQueries([
/**
* getRune -> Unique Key
* riot.version -> 파라미터
*/
{
queryKey: ["getRune", riot.version],
queryFn: () => api.getRunInfo(riot.version),
},
{
queryKey: ["getSpell", riot.version],
queryFn: () => api.getSpellInfo(riot.version),
},
]);
useEffect(() => {
console.log(result);
const loadingFinishAll = result.some((result) => result.isLoading);
console.log(loadingFinishAll);
}, [result]);
예제 👇
useQueries사용 >Unique Key활용 >Unique Key를 배열로 넣으면queryFn(쿼리 함수)내부에서 변수로 사용 가능
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const result = useQueries([
{
queryKey: ["getRune", riot.version],
queryFn: (params) => {
// 결과 -> {queryKey: ['getRune', '12.1.1'], pageParam: undifined, meta: undifined}
console.log(params);
return api.getRunInfo(riot.version);
},
},
{
queryKey: ["getSpell", riot.version],
queryFn: () => api.getSpellInfo(riot.version),
},
]);
🍀 4. useMutation
- 값을 바꿀 때 사용하는 api
- 데이터를 생성(
POST), 수정(UPDATE), 삭제(DELETE)할 떄 사용하는Hook - 특정 함수에서 우리가 원하는 때에 직접 요청을 시작하는 형태
- 요청 관련 상태의 관리와 요청 처리 전/후로 실행할 작업을 쉽게 설정할 수 있음
- 첫 번쨰 인자 👉
Promise를 반환하는 함수 - 두 번째 인자 👉 이 작업이 처리되기 전후로 실행할 함수를 넣음(생략가능)
- onMutate 👉 요청 직전 처리, 여기서 반환하는 값은 하단 함수들의 context로 사용
- onError 👉 데이터 요청이 실패하고 나서 특정 함수를 호출하고 싶을 때 사용
- onSuccess 👉 데이터 요청이 성공하고 나서 특정 함수를 호출하고 싶을 때 사용
- onSettled 👉 데이터 요청의 성공 여부와 관계없이 요청이 끝나면 특정 함수를 호출하도록 설정
return값 👉useQuery와 동일mutate👇- 요청을 시작하는 함수
- 첫 번째 인자 👉 API 함수에서 사용할 인자
- 두 번쨰 인자
- {onSuccess, onSettled, onError} 객체, (생략가능)
- option 에 설정된 함수가 먼저 호출되고, mutate 두 번째 파라미터에 넣은 함수가 호출됨
mutateAsync👉mutate와 인자는 동일, 함수를 호출했을 때 반환 값이Primsestaus👉 요청 상태를 문자열로 변환(idle, loading, error, success)error👉 오류 정보data👉 요청 성공 시 데이터가 담겨있음reset👉 상태를 모두 초기화하는 함수
예제 👇
useMutation사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const Index = () => {
const [id, setId] = useState('');
const [password, setPassword] = useState('');
const loginMutation = useMutation(loginApi, {
// variable -> {loginId: xxx, password: xxx}
onMutate: variable => {
console.log("onMutate", variable);
},
onError: (error, variable, context) => {
// TODO: Error 핸들링
},
onSuccess: (data, variables, context) => {
console.log("success", data, varibles, context);
},
onSettled: () => {
console.log('end);
},
});
// API 호출에 사용될 인자
const onSubmit = () => {
loginMutation.mutate({loginId: id, password: password});
}
}
예제 👇
invalidateQueries를 사용하여 UPDATE 후 GET 함수를 간단하게 실행
mutation함수가 성공할 때,unique key로 맵핑된GET함수를invalidateQueries에 넣어주면 됨
만약mutation에서return된 값을 이용해서GET함수의 파라미터를 변경해야 할 경우setQueryData를 사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const mutation = useMutation(postTodo, {
onSuccess: () => {
// postTodo 가 성공하면 todos 로 맵핑된 useQuery API 함수를 실행
queryClient.invalidateQueries("todos");
},
});
// 만약 mutation에서 return된 값을 이용해서 GET 함수의 파라미터를 변경해야할 경우 setQueryData를 사용
const queryClient = useQueryClient();
// data 가 fetchTodoById 로 들어감
const mutation = useMutation(editTodo, {
onSuccess: (data) => {
queryClient.setQueryData(["todo", { id: 5 }], data);
},
});
const { status, data, error } = useQuery(["todo", { id: 5 }], fetchTodoById);
mutation.mutate({
id: 5,
name: "nkh",
});
🍀 5. react-suspense 와 react-query 사용
- 비동기를 좀 더 선언적으로 사용할 수 있음.
suspense를 사용하여loading을,Error bundary를 사용하여error handling을 더욱 직관적으로 할 수 있음.suspense를 사용하기 위해QueryClient에option을 하나 추가해야 함
예제 👇 Global 하게
suspense를 사용한다고 정의 > src/index.js 에 선언
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 0
suspense: true
}
}
})
ReactDom.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
...
)
예제 👇 함수마다
suspense를 사용하는 예시
1
2
3
4
5
6
7
8
9
const { data } = useQuery("test", testApi, { suspense: ture });
return (
<Suspense fallback={<div> loading... </div>}>
<ErrorBoundary fallback={<div>에러발생</div>}>
<div>{data}</div>
</ErrorBoundary>
</Suspense>
);
🍀 6. React-Query Cache 데이터 새로고침
React-Query에는useQuery를 사용할 때 입력한Unique Key를 통해 데이터를 만료시키고, 새로 불러올 수 있도록 처리할 수 있다.invalidateQueries👇- 캐시를 만료시켜 데이터를 새로 고침
Unique Key를 사용하여 데이터를 만료시키고, API를 재요청하는 방식
useQueryClient👉 이Hook은 이전에App Component에서QueryClientProvider에 넣었던queryClient를 사용할 수 있게 해줌getQueryData👇Unique Key를 사용하여 Cache Data 를 조회할 수 있음- 데이터가
undifined일 수 있으니, 만약 데이터가 존재하지 않는다면빈 배열을 사용하도록 준비 TypeScript환경에서는Generic을 지정하면 반환값의 데이터 type 을 설정할 수 있음
setQueryData👇- Cache Data 를 업데이트하는 메서드
- 데이터를 두 번째 인자에 넣어도되고,
업데이트 함수형태의 값을 인자로 넣을 수 있음 - 만약
업데이트 함수형태로 넣는다면getQueryData는 생략 가능
예제 👇
QueryClient를 사용하여 데이터 새로고침
1
2
3
4
5
6
7
8
const queryClient = useQueryClient();
const { mutate: write } = useMutation(writerArticle, {
onSuccess: () => {
queryClient.invalidateQueries("articles");
navigation.goBack();
},
});
예제 👇
QueryClient로Cache Data를 직접UPDATE하기
API를 재요청하지 않고Cache Data를UPDATE
1
2
3
4
5
6
7
8
const queryClient = useQueryClient();
const { mutate: write } = useMutation(writeArticle, {
onSuccess: (article) => {
const articles = queryClient.getQueryData<Article[]>("articles") ?? [];
queryClient.setQueryData("articles", articles.concat(article));
},
});
예제 👇
getQueryData생략,UPDATE함수 형태의 값을 인자로 넣음
Unique Key로 데이터 조회 후 그 데이터를UPDATE함수를 사용하여UPDATE
1
2
3
4
5
6
7
8
9
const queryClient = useQueryClient();
const { mutate: write } = useMutation(writeArticle, {
onSuccess: (article) => {
queryClient.setQueryData<Article[]>("articles", (articles) =>
(articles ?? []).concat(article)
);
},
});
🍀 7. useInfiniteQuery
React-Query에서Pagenation을 구현할 때 사용- 함수 부분에서
pageParam을 사용하고,option부분에getNextPageParam을 설정해줘야 함 getNextPageParam👉 (lastPage, allPages) => unknown | undefined 타입 함수- 이 함수에서는
pageParam으로 사용할 값을 결정 getNextPageParam에서 더 이상 조회할 수 있는 데이터가 없는 경우undefined를 반환해야 함allPages👉 지금까지 불러온 모든 페이지를 가리킴, 배열로 이루어진 배열 ex) Article[][]lastPage👉 가장 마지막으로 불러온 페이지, 현재 data type ex) Article[]
- 이 함수에서는
return값data👉 {pageParams, pages} 타입을 가지고 있음pageParams👉 각 페이지에서 사용된 파라미터 배열pages👉 각 페이지들을 배열 타입으로 나타냄 ex) Article[][]fetchNextPage👉 다음 페이지를 불러오는 함수hasNextPage👉 다음 페이지의 존재 유무를 알려줌 만약getNextPageParam에서undefined를 반환했다면 이 값은false가 되고 그렇지 않으면true가 된다.isFetchingNextPage👉 다음 페이지를 불러오고 있는지 여부를 알려줌- 그 외에
useQuery에서 반환되는 모든 필드들이 존재
예제 👇
useInfiniteQuery사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const ArticlesScreen = () => {
const {data, isFetchingNextPage, fetchNextPage} = useInfiniteQuery(
'articles',
({pageParam}) => getArticle({cursor: pageParam}),
{
getNextPageParam: (lastPage) =>
lastPage.length === 10 ? lastPage[lastPage.length - 1].id : undefined,
},
);
/**
* [][] => [] 으로 변경
* [] as Aritlce[] 이라고 입력하여 해당 배열이 Article의 배열이란 것을 명시하고, concat을 해줌
* concat 에는 배열 타입을 넣으면 해당 배열을 해체해서 앞부분의 배열에 붙여주기 때문에, 해당 방식으로
* 하면 배열들이 하나의 배열로 합쳐짐
*/
const item = useMemo(() => {
if (!data) return null;
return ([] as Article[]).concat(...data.length)
}, [data]);
const [user] = useUserState();
if (!items) {
return (
<ActivityIndicator size="large" style={styles.spinner} color="black">
);
}
return (
<Articles
articles={items}
showWriteButton={!!user}
isFetchingNextPage={isFetchingNextPage}
fetchNextPage={fetchNextPage}
>
);
}
예제 👇
useInfiniteQuery사용 >FlatList연결
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
interface ArticlesProps {
articles: Article[];
showWriteButton: boolean;
isFetchingNextPage: boolean;
fetchNextPage(): void;
}
const Articles({
articles,
showWriteButton,
isFetchingNextPage,
fetchNextPage
}: ArticlesProps) {
return (
<FlatList
data={articles}
renderItem={({item}) => (
<ArticleItem
id={item.id}
title={item.title}
publishedAt={item.published_at}
username={item.user.username}
/>
)}
keyExtractor={(item) => item.id.toString()}
style={styles.list}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListHeaderComponent={() => (showWriteButton ? <WriteButton /> : null)}
ListFooterComponent={() => (
<>
{articles.length > 0 ? <View style={styles.separator} /> : null}
{isFetchingNextPage && (
<ActivityIndicator
size="small"
color="black"
style={styles.spinner}
/>
)}
</>
)}
onEndReachedThrehold={0.5}
onEndReached={fetchNextPage}
/>
);
}
const styles = StyleSheet.create({
list: {
flex: 1,
},
separator: {
width: '100%',
height: 1,
backgroundColor: '#cfd8dc',
},
spinner: {
backgroundColor: 'white',
paddingTop: 32,
paddingBottom: 32,
},
})