안녕하세요. 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 Option
enable
👉 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
와 인자는 동일, 함수를 호출했을 때 반환 값이Primse
staus
👉 요청 상태를 문자열로 변환(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,
},
})