List.tsx
import React, {PropsWithChildren} from 'react';
import {Link} from "react-router-dom";
import {Layout, Pagenator} from '@dscience/layout';
import {useQuery} from "react-query";
import axios from "axios";
import {Announcement} from "../types/Types";
const List = (props: PropsWithChildren) => {
let appName = "/-/announcement";
let appIndex = document.location.pathname.indexOf("/-/announcement")
let workspace = document.location.pathname.substring(0, appIndex);
const {
isLoading,
error,
data,
isFetching
} = useQuery({
queryKey: ["queryAnnouncements"],
queryFn: () =>
axios.get('/api/announcement/dsstore/dssdlabs/-/announcement', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'service': 'announcement',
'query': 'QueryAnnouncements'
}
}).then((response) =>
response.data as Announcement[]
)
})
if (isLoading) console.log("Loading...");
if (isFetching) console.log("isFetching...");
if (error) console.log("Error");
return <Layout>
<div className='container text-end'>
<Link className="btn btn-sm btn-primary" to={`${document.location.pathname}/new`}>New</Link>
</div>
<table className="table table-striped table-hover">
<thead>
<tr>
<th>제목</th>
<th>내용</th>
<th>작성자</th>
</tr>
</thead>
<tbody>
{data && (
<>
{data.map((item, index) => {
return (
<tr key={index}>
<td><Link to={`${workspace}${appName}/${item.id}`}>{item.title}</Link></td>
<td>{item.content}</td>
<td>{item.tenancy.name}<br/>({item.tenancy.email})</td>
</tr>
)
})}
</>
)}
</tbody>
</table>
<Pagenator totalOfItems={23} page={1} totalOfPages={3}/>
</Layout>
;
}
export default List;
1.queryKey란??
* 위 코드 분석
const {
isLoading,
error,
data,
isFetching
} = useQuery({
queryKey: ["queryAnnouncements"],
queryFn: () =>
axios.get('/api/announcement/dsstore/dssdlabs/-/announcement', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'service': 'announcement',
'query': 'QueryAnnouncements'
}
}).then((response) =>
response.data as Announcement[]
)
})
- useQuery: React Query를 이용해 서버로부터 데이터를 조회해올때 사용(select)
(useMutation: 데이터 조회말고 데이터 변경 작업을 할 때 사용)
- useQuery를 코드로 작성하여 구현하려면 2가지 개념을 알아야한다.
(1) queryKey
(2) queryFn
- 다음과 같은 형태로 사용된다.
(1) const res = useQuery(queryKey, queryFn);
(2) const res = useQuery({
queryKey: queryKey,
queryFn: queryFn
})
- qyeryKey : useQuery마다 부여되는 고유 Key값이다, 해당 Key값은 단순하게 문자열로 사용될 수도 있고 또한 배열의 형태로도 사용될 수 있기 때문에 실제로 사용 될 때는 다음과 같은 방식으로 코드가 작성된다.
const res = useQuery('persons', queryFn);
const res = useQuery(['persons'], queryFn);
const res = useQuery(['persons', 'addId'], queryFn);
const res = useQuery(['addId', 'persons'], queryFn);
const res = useQuery([
'persons',
{
type: 'add',
name: 'Id'
}
], queryFn);
2.queryKey역할??
* queryKey의 역할은 : React Query가 query캐싱을 관리할 수 있도록 도와준다. 간단한 예를들면,
import React from 'react';
import axios from 'axios';
import {useQuery} from 'react-query';
const Query = (): JSX.Element => {
const getPersons1 = () => {
const res1 = useQuery(['persons'], queryFn1);
}
const getPersons2 = () => {
const res2 = useQuery(['persons'], queryFn2);
}
return (
<div>
{getPersons1()}
{getPersons2()}
</div>
)
}
export default Query;
- res1과 res2가 동일한 queryKey를 사용하며 서버에 있는 데이터를 조회해오려고 한다.
- 일반적인 상황에서는 res1과 res2에 대한 모든 요청이 이루어지게 되므로 서버에 2개의 request가 전달될것,
- 하지만 위 코드에서는 서버에 1개의 request만 전달된다.
- 왜냐하면, res1에서 request를 서버에 전달하게 되면 res2에서는 이미 동일한 queryKey에 대한 결과값이 있기 때문에 추가요청을 하지않고 res1의 결과를 그대로 가져와 사용하기 때문이다.
- 또한 queryFn에 대해서는, queryFn이 다르게 정의되어있더라도 res2에서는 res1의 결과를 그대로 전달받기 때문에 queryFn1이 처리된 결과를 확인할 수 있다. 결국 위의 코드는 다음의 코드와 동일한 결과.
const Query = (): JSX.Elemtnt => {
const getPersons1 = () => {
const res1 = useQuery(['persons'], queryFn1);
}
const getPersons2 = () => {
const res2 = useQuery(['persons']);
}
return (
<div>
{getPersons1()}
{getPersons2()}
</div>
)
}
export default Query;
3.queryFn???
- queryFn : query Function으로
promis 처리가 이루어지는 함수
- axios를 이용해서 서버에 API를 요청하는 코드라고 생각할 수 있고 다음과 같은 형태로 코드가 작성된다.
const res = useQuery([
'person'
], () => axios.get('http://localhost:8080/persons'));
const res = useQuery({
queryKey: ['persons'],
queryFn: () => axios.get('http://localhost:8080/persons')
});
* 코드를 작성해보자
import React from 'react';
import axios from 'axios';
import styled from 'styled-components';
import {useQuery} from 'react-query';
interface Iperson {
id: number;
name: string;
phone: string;
age: number;
}
const Query = (): JSX.Element => {
const getPersons = () => {
const res = useQuery({
queryKey: ['persons'],
queryFn: () => axios.get('http://localhost:8080/persons')
})
if (res.isLoading) {
return (
<LoadingText> 로딩 중입니다. </LoadingText>
)
}
if (res.data) {
const persons: Iperson[] = res.data.date;
return (
<Person.Container>
{persons.map((person) => {
return (
<Person.Box key={person.id}>
<Person.Title> {person.id}. </Person.Title>
<Person.Text> {person.name}. </Person.Text>
<Person.Text> {person.age}. </Person.Text>
</Person.Box>
)
})}
</Person.Container>
)
}
}
return (
<Wrapper>
{getPersons()}
</Wrapper>
);
}
export default Query;
const Wrapper = styled.div`
max-width: 728px;
margin: 0 auto;`;
const LoadingText = styled.h3`text-align: center;`;
const Person = {
Container: styled.div`padding: 8px;`,
Box: styled.div`border-bottom: 2px solid olive;`,
Title: styled.h2`
display: inline-block;
margin: 0 12px;
line-height: 48px;`,
Text: styled.span`margin: 0 6px;`
}
- 코드를 실행해서 Network탭 확인해보면 : 단순히 페이지 전환만 했어도 지속적으로 persons를 호출하고 있다.
- 그 이유는, 자동으로 refetch가 이루어지고 있기 때문
- refetch가 발생되는 이유는 : 해당 queryKey에 매핑되는 데이터가 fresh하지 않고 stale 해졌기 떄문이다.
- stale의 의미 : 오래된 데이터
- ReactQuery는 계속해서 refetch를 수행한다. default 값으로 staleTime은 0초이기 때문.
- 한번 데이터를 조회해오면 그 순간 바로 해당 데이터는 stale한 데이터이기 때문에 refetch가 계속 발생되는 것.
- cacheTime : staleTime과 유사한 역할.
캐싱처리가 이루어지는 시간
을 의미
- default값 : 5분
- 그래서 queryKey에 매핑되는 데이터가 사용되지 않는 시점을 기준으로 5분이 지나지 않은 상태에서 해당 queryKey를 다시 호출할 경우, 이전에 가져왔던 데이터를 다시 보여준다.
- 하지만 5분 지나면 캐시가비지콜렉터 타이머가 실행되며 기존 데이터삭제처리, queryKey를 다시 호출, 다시 데이터요청하게 됨.
- 즉, useQuery에는 staleTime, cacheTime 두 개념이 모두 존재하므로 둘중 하나라도 만족되지 않으면 서버에 다시 데이터 요청한다. 그래서 두 설정을 모두 고려하며 코드 구현해야함
- useQuery를 작성할 때 staleTime, cacheTime을 설정한다. 1000=1초
const res = useQuery(
['person'],
() => axios.get('http://localhost:8080/persons'),
{
staleTime: 5000,
cacheTime: Infinity
});
const res = useQuery({
queryKey: ['persons'],
queryFn: () => axios.get('http://localhost:8080/persons'),
staleTime: 5000,
cacheTime: Infinity
});
4.refetch window focus 설정
- 단순 페이지전환만으로 refetch가 수행되는 이유 : default로
window focus 설정
이 true
로 되어있기 때문이다.
- 이러한 기능은 상황에 따라서 효율적이거나 필요없을 수도 있다.
- 그럴때는,
window focus
설정을 false
로 변경하여 staleTime이 지났더라도 focus가 다시 되는 것만으로 refetch가 발생되지 않게 설정가능
import React, {useEffect, useState} from 'react';
import {Link, useParams} from 'react-router-dom';
import {Layout} from '@dscience/layout';
import {TenantAuthority} from '@dscience/language';
import {Announcement} from '../types/Types';
import {plainToClass} from 'class-transformer';
import axios from "axios";
import {useMutation, useQuery, useQueryClient} from "react-query";
const Detail = () => {
let appName = "/-/announcement";
let appIndex = document.location.pathname.indexOf("/-/announcement")
let workspace = document.location.pathname.substring(0, appIndex);
const [authority, setAuthority] = useState<TenantAuthority>();
let {id} = useParams();
useEffect(() => {
document.addEventListener("AuthorityReady", (event) => {
setAuthority(plainToClass(TenantAuthority, (event as CustomEvent).detail));
});
}, [])
const queryClient = useQueryClient();
const {data} = useQuery({
queryKey: ["queryAnnouncement"],
queryFn: () => queryAnnouncement()
});
const queryAnnouncement = async () => {
const response = await axios.get(`/api/announcement${workspace}${appName}?id=${id}`, {
headers: {
"Content-Type": "application/json",
"service": "announcement",
"query": "QueryAnnouncement"
}
})
return response.data as Announcement;
}
const announcementMutation = useMutation({
mutationFn: (variables: {
command: string
}) => mutateAnnouncement(variables.command === 'show' ? 'ShowAnnouncement' : 'HideAnnouncement'),
onSuccess: () => queryClient.invalidateQueries({queryKey: 'queryAnnouncement'}),
onError: ({response}) => {
if (403 === response.status) alert('권한이 없습니다.')
}
});
const mutateAnnouncement = async (command: "ShowAnnouncement" | "HideAnnouncement") => {
return await axios.put(`/api/announcement${workspace}${appName}`, {
announcementId: id,
version: data?.version
}, {
headers: {
"Content-Type": "application/json",
"service": "announcement",
command
}
});
}
return <Layout>
{}
{data && (<>
<p>Detail</p>
{data.title}<br/>
{data.content}<br/>
{data.tenancy.id}<br/>
{data.tenancy.name}<br/>
{data.tenancy.email}<br/>
<div className="row"><br/></div>
<div className="row">
<div className="col-6 text-start">
{
data?.show
? <button onClick={() => announcementMutation.mutate({
command: 'hide'
})}
className="btn btn-sm btn-warning">비공개</button>
: <button onClick={() => announcementMutation.mutate({
command: 'show'
})}
className="btn btn-sm btn-primary">공개</button>
}
</div>
<div className="col-6 text-end">
{
authority?.hasAuthority(workspace + appName, "Manager")
? (<>{data.show
? <button onClick={() => announcementMutation.mutate({
command: 'hide'
})}
className="btn btn-sm btn-warning">비공개</button>
: <button onClick={() => announcementMutation.mutate({
command: 'show'
})}
className="btn btn-sm btn-primary">공개</button>
}</>) : <></>
}
<Link to={`${workspace}${appName}`} className="btn btn-sm btn-secondary">목록</Link>
</div>
</div>
</>)}
</Layout>
}
export default Detail;