아래 내용은 코드로 배우는 React with 스프링부트 API서버 강의와 함께 설명이 부족한 부분에 대해서 조사하여 추가한 내용입니다.
React Query 는 React 애플리케이션에서 데이터를 관리하고 처리하기 위한 라이브러리입니다. 주로 웹 애플리케이션에서 서버로부터 데이터를 가져오거나 업데이트하는 작업을 단순하고 효과적으로 처리할 수 있도록 도와줍니다.
Query 는 데이터를 가져오는 작업을 나타내는 객체입니다. React Query 는 useQuery
훅을 통해 데이터를 가져오는 데 사용됩니다. 쿼리 객체에는 데이터를 가져오는 함수, 캐시 키 등이 포함됩니다.
Mutation 은 데이터를 업데이트하거나 변경하는 작업을 나타내는 객체입니다. useMutation
훅을 사용하여 데이터를 업데이트하는 데 사용됩니다.
Infinite Queries 는 무한 스크롤 또는 페이징과 같은 상황에서 데이터를 가져오는 작업을 처리하기 위한 기능이 있습니다.
useQuery 는 React Query 라이브러리에서 제공하는 훅 중 하나로, 비동기적으로 데이터를 가져오는 데(
GET
) 사용됩니다. React Query는 데이터 가져오기, 캐싱, 상태 관리 및 갱신을 용이하게 하기 위해 사용합니다.
const {data, isFetching} = useQuery({
queryKey : ['queryName', parameter],
queryFn : () => {...},
staleTime : 1000
});
1. queryKey
쿼리의 고유한 식별자로 사용되는 키입니다. 배열 형태로 제공되며, 첫 번째 요소는 쿼리 이름이나 키, 나머지 요소는 해당 쿼리에 필요한 매개변수 등을 나타냅니다. 이 키를 기반으로 데이터가 캐싱되고 업데이트됩니다.
2. queryFn
비동기 함수로, 실제로 데이터를 가져오는 함수를 제공합니다. 이 함수는 쿼리 키에 따라 호출되어 데이터를 반환합니다. React Query는 이 함수의 실행 결과를 자동으로 캐싱하고 관리합니다.
3. options
옵션 객체로, 쿼리에 대한 추가 설정을 제공할 수 있습니다. 예를 들어, staleTime 은 데이터가 만료되기 전까지 사용할 수 있는 시간을 설정합니다.
useQuery 의 queryKey 는 React Query 에서 각 쿼리를 고유하게 식별하는 데 사용되는 키입니다. 이 키를 기반으로 캐싱되는 데이터가 구분되고, 데이터를 다시 가져올 때도 이 키를 사용하여 어떤 쿼리를 실행해야 하는지 식별합니다.
queryKey 는 배열 형태로 제공되며 첫 번째 요소는 쿼리의 식별자( 이름 또는 키 )이고, 나머지 요소는 해당 쿼리에 필요한 매개변수 등을 나타냅니다.
const { data, error, isFetching } = useQuery({
queryKey : ['exampleQuery', useId]
});
'exampleQuery' 는 해당 쿼리의 이름 또는 식별자입니다. React Query 는 이를 기반으로 캐싱된 데이터를 관리합니다.
userId
는 이 쿼리에 필요한 추가 매개변수입니다. 쿼리를 실행할 때 이 매개변수를 사용하여 데이터를 가져옵니다.
이렇게 함으로써, 서로 다른 쿼리에 대해 다른 이름과 매개변수를 가진 queryKey 를 사용하여 각각의 쿼리를 식별하고 구분할 수 있습니다. 쉽게 말해서 파라미터로 지정된 값이 변경되면 React Query는 이를 새로운 쿼리로 처리하고 데이터를 다시 가져옵니다.
function ReadComponent({ itemId }) {
const [item, setItem] = useState(initState);
const [fetching, setFetching] = useState(false);
...
useEffect(() => {
setFetching(true);
getItem(itemId).then(data => {
setItem(data);
setFetching(false);
console.log("data = ", data);
})
}, [itemId]);
...
return (
{fetching ? <FetchingModal /> : <></>}
);
}
기존에 ReadComponent 에서 useState
를 통해 데이터를 선언하고, useEffect
를 통해 비동기로 처리한 데이터를 변경해주었습니다.
또한 데이터를 가져오는 중인지를 확인하기 위해 fetching 을 선언하고 이를 상태로 관리하여 true 이면 FetchingModal 을 띄우도록 하였습니다.
React Query 의 useQuery
를 사용하여 위의 과정을 단순하게 처리할 수 있습니다.
function ReadComponent({ itemId }) {
// isFetching 이 fetching 대체
// data 가 useState 대체
// useEffect 에서 데이터를 가져오는 것을 queryFn 에 선언한 함수가 대체
const {data, isFetching} = useQuery({
queryKey : ['items', itemId],
queryFn : () => getItem(itemId),
staleTime : 1000 * 10
});
...
const item = data || initState;
...
return (
{isFetching ? <FetchingModal /> : <></>}
);
}
data
는 쿼리 결과로 얻은( API 호출 후 얻은 ) 데이터를 나타내는 변수입니다.
isFetching
은 현재 데이터를 가져오고 있는지 여부를 나타내는 변수입니다.
staleTime
는 데이터가 만료되기 전까지 사용할 수 있는 시간( ms )을 나타냅니다. 데이터가 만료되면 React Query 는 새로운 데이터를 가져와 갱신합니다.
데이터를 가져왔을 때 Fresh 상태이고, 지정한 시간이 지나면 Stale 이 됩니다. 데이터가 Fresh 상태일 때는 API 를 다시 호출하지 않지만, Stale 상태일 때는 다시 호출하게 됩니다.
function ReadComponent({ itemId }) {
const {data, isFetching} = useQuery({
...
});
if(isFetching) {
return <FetchingModal />
}
...
const item = data;
...
return (...);
}
return 내부에서 삼항 연산자를 사용해서 FetchingModal 을 띄우지 않고, useQuery 의 isFetching 를 조건문으로 사용해서 FetchingModal 을 바로 반환할 수 있습니다.
이러한 방식을 사용하면 item 이라는 변수에 ||
연산자를 사용하지 않고 바로 data 를 할당할 수 있습니다.
페이지에서 동일한 페이지를 눌렀을 때 API 를 다시 호출하지 않습니다. 쇼핑몰 같이 데이터가 실시간으로 변하지 않는 애플리케이션은 상관없지만 SNS 와 같이 실시간으로 데이터가 변경되는 애플리케이션에서는 다시 호출을 해야 합니다.
이전에는 useMove 훅에 refresh 라는 상태를 만들어서 동일한 페이지라도 호출하도록 하였는데 React Query 에서 하는 방법을 살펴보겠습니다.
Query Invalidate 는 쿼리로 보관하고 있는 데이터를 지우는, 무효화 시키는 것을 의미합니다. 주로 데이터 업데이트, 생성, 삭제 등의 이벤트 후에 새로운 데이터를 다시 불러와 UI를 업데이트하는 데 사용됩니다.
function ListComponent() {
...
const queryClient = useQueryClient();
const handleClickPage = (pageParam) => {
if(pageParam.page === parseInt(page)) {
queryClient.invalidateQueries("items/list");
}
moveToList(pageParam);
}
...
return (
...
<PageComponent serverData={serverData} movePage={handleClickPage}></PageComponent>
);
}
useQueryClient
훅을 호출하여 쿼리 클라이언트 객체를 가져옵니다. 이 객체를 통해 현재 애플리케이션의 쿼리 상태를 관리하고, 쿼리에 대한 여러 작업을 수행할 수 있습니다.
invalidateQueries
는 특정 쿼리 또는 쿼리 그룹을 무효화하여 해당 데이터를 다시 불러오도록 유도합니다. 메서드를 호출할 때 식별자를 넘겨주면, 해당 식별자를 가진 쿼리를 무효화합니다. 무효화된 쿼리는 다음에 해당 쿼리에 대한 요청이 있을 때, 다시 서버에서 데이터를 가져와 갱신됩니다.
function ListComponent() {
const {data, isFetching, isError} = useQuery({
queryKey : ['items/list', {page, size, refresh}],
queryFn : () => getList({page, size}),
staleTime : 1000 * 5
});
}
이전에는 useMove 에 선언한 refresh 상태를 가져와서 useEffect 의 의존성 배열에 추가하여, 해당 값이 true, false 가 변경될 때마다 호출할 수 있도록 하였습니다.
위의 코드에서 refresh 를 queryKey 에 넘겨줌으로써 동일 페이지를 클릭해도 다시 호출하도록 설정하면서 staleTime 을 5초로 지정했기 때문에 처음 한 번은 다시 호출되지만 5초가 지나기 전까지는 다시 호출되지 않도록 합니다.
staleTime 으로 사용했을 때, 상품 등록을 한 후에 페이지가 이동되어도 아직 staleTime 이 남았다면 다시 불러오지 않기 때문에 추가된 데이터가 보이지 않는 경우도 발생합니다.
이런 경우에는 위의 invalidateQueries
를 사용하는 것이 더 좋습니다.
useMutation
은 React Query 에서 제공하는 훅 중 하나로, 데이터를 변경하는 작업( 생성, 수정, 삭제 )을 처리하고 해당 작업의 결과를 업데이트할 때 사용됩니다. 주로 폼 제출, 사용자 입력에 대한 서버 측 변경 등에서 활용됩니다.
const mutation = useMutation({
mutaionFn : () =>{...} // 실제 데이터 업데이트를 수행하는 비동기 함수
}
);
// mutation 함수를 호출하여 데이터 변경 작업 실행
mutation.mutate(formData);
// 성공 시 true
if(mutation.isSuccess) {
...
}
// 처리 중일 때 true
if(mutation.isPending) {
...
}
1. mutaionFn
mutaionFn 은 데이터 변경 작업을 수행하고 Promise 를 반환해야 합니다.
2. Mutate 함수
mutate
함수는 실제로 데이터 변경 작업을 시작하는 함수입니다. 인자로 데이터를 전달하면, Mutation
함수가 실행됩니다.
3. isSuccess, isPending
생성된 mutation 객체에서 여러 속성을 사용할 수 있는데 isSuccuess
는 성공했는지를 나타내고, isPending
은 처리 중인지 상태를 나타냅니다.
function AddComponent() {
const [fetching, setFetching] = useState(false);
const [result, setResult] = useState(null);
...
const handleClickSave = () => {
if (window.confirm("저장하시겠습니까?")) {
...
setFetching(true);
...
addItem(formData).then(data => {
setFetching(false);
setResult(data.RESULT);
});
}
}
const closeModal = () => {
setResult(null);
moveToList();
}
return (
{fetching ? <FetchingModal /> : <></>}
{result ? <ResultModal
title={'Success'}
content={`${result}번 상품 등록 완료!`}
callbackFn={closeModal} />
: <></>}
...
);
}
저장 버튼 클릭 시, formData 생성 후 직접 저장 API 를 호출합니다.
이때 처리 중인지를 표현하기 위해 fetching 상태를 만들고, 호출 전에 상태 변경, 호출 후에 상태가 변경되도록 하였습니다.
result 라는 상태를 만들어 성공했을 때 set 메서드를 이용하여 호출 후에 Modal 에 메세지를 띄우기 위해 결과값으로 변경하였고, 모달 창 닫을 때 다시 상태 변경을 하도록 하였습니다.
function AddComponent() {
const addMutation = useMutation({
mutationFn: (item) => addItem(item),
});
const queryClient = useQueryClient();;
...
const handleClickSave = () => {
if (window.confirm("저장하시겠습니까?")) {
addMutation.mutate(formData);
}
}
const closeModal = () => {
queryClient.invalidateQueries("items/list");
moveToList();
}
return (
{addMutation.isPending ? <FetchingModal /> : <></>} {/* 저장이 처리 중이라면 처리 중이라는 모달을 띄운다 */}
{addMutation.isSuccess ? <ResultModal
title={'Success'}
content={`${addMutation.data.RESULT}번 상품 등록 완료!`}
callbackFn={closeModal} />
: <></>}
...
);
}
데이터를 신규 등록하기 위해 useMutation
을 사용해서 객체를 생성하는데 이때 API 호출하는 함수를 mutationFn
에 지정합니다. 저장 버튼을 누를 때 직접 함수를 호출하지 않고, mutate()
함수를 통해 호출하도록 합니다.
처리 중인지를 표현하기 위한 상태 대신, mutation 객체의 isPending
을 사용할 수 있고, 성공했을 때 결과 데이터를 넣는 result 에 isSuccess
를 사용할 수 있습니다.
이때 mutation객체.data
를 통해 전달 받은 데이터에 접근할 수 있습니다.
모달을 닫을 때 staleTime 이 지나지 않았어도 list 목록을 다시 호출하기 위해 invalidateQueries
를 사용해 저장된 값을 제거하고 다시 호출하도록 합니다.
function ReadComponent({ itemId }) {
const [result, setResult] = useState(null);
const handleClickDelete = () => {
if(window.confirm("삭제하시겠습니까?")) {
setResult(item.itemId);
deleteItem(item.itemId);
}
}
const closeModal = () => {
setResult(null); // null 로 변경하면 결과 Modal 이 사라진다
moveToList();
}
return (
{isFetching ? <FetchingModal /> : <></>}
{result ? <ResultModal
title={'Success'}
content={`${result}번 상품이 삭제되었습니다`}
callbackFn={closeModal} />
: <></>}
);
}
ReadComponent 이기 때문에 이전에 적용한 useQuery 가 남아있는 형태입니다.
삭제 시에는 직접 API 를 호출하고, result 를 통해, 삭제 후 데이터를 가져와 ResultModal 에 띄우게 됩니다.
ResultModal 의 callbackFn 에 closeModal 을 두고 함수 내부에서 result 의 상태를 변경하고 list 로 이동하게 됩니다.
function ReadComponent({ itemId }) {
const delMutation = useMutation({
mutationFn : (itemId) => deleteItem(itemId)
});
const queryClient = useQueryClient();
const handleClickDelete = () => {
if(window.confirm("삭제하시겠습니까?")) {
delMutation.mutate(itemId);
}
}
const closeModal = () => {
if(delMutation.isSuccess) {
queryClient.invalidateQueries(['items', itemId]);
queryClient.invalidateQueries('items/list');
}
moveToList();
}
return(
{isFetching || delMutation.isPending ? <FetchingModal /> : <></>}
{delMutation.isSuccess ? <ResultModal
title={'Delete'}
content={'삭제되었습니다'}
callbackFn={closeModal}/>
: <></>}
);
}
삭제 시에는 useMutation 을 사용합니다. 이때 API 를 호출 시에 직접 호출하지 않고 mutate()
를 사용하여 호출하게 됩니다. 이때 처리 중인지 보여주기 위해 isPending
을 사용합니다.
ResultModal 을 띄우는 것도 result 상태에서 isSuccess
를 사용하게 되고, 삭제 후에 list 로 이동하는데 새로운 결과를 가져오기 위해 invalidateQueries
를 사용하여 쿼리의 데이터를 제거합니다.
function ModifyComponent({ itemId }) {
const [result, setResult] = useState(null);
const [fetching, setFetching] = useState(false);
useEffect(() => {
setFetching(true);
getItem(itemId).then(data => {
setItem(data);
setFetching(false);
})
}, [itemId]);
const handleClickSave = () => {
...
setFetching(true);
modifyItem(itemId, formData).then(() => {
setFetching(false);
setResult(itemId);
setItem({...initState})
})
}
const closeModal = () => {
setResult(null);
moveToRead(itemId);
}
return (
{fetching ? <FetchingModal /> : <></>}
{result ? <ResultModal
title={'Success'}
content={`${result}번 상품 수정 완료!`}
callbackFn={closeModal} />
: <></>}
);
}
useEffect 를 통해 itemId 가 변경되면 데이터를 가져오는 API 를 호출하여 해당 상품 번호에 맞는 데이터를 불러오도록 합니다.
데이터 수정 전 fetching 상태를 변경하고, 버튼을 누르면 수정 API 를 호출하고 fetching 과 result 의 상태를 변경하도록 합니다.
ResultModal 에서 closeModal 이 호출되면 다시 result 상태를 변경하고 상세 페이지로 이동하도록 합니다.
function ModifyComponent({ itemId }) {
const queryClient = useQueryClient();
// 수정 화면에서 데이터를 불러오는 역할
const query = useQuery({
queryKey : ['items', itemId],
queryFn : () => getItem(itemId),
staleTime : Infinity
})
// 수정 API 를 호출하는 역할
const modifyMutation = useMutation({
mutationFn : (item) => modifyItem(itemId, item)
})
// 호출 결과를 item 에 세팅
useEffect(() => {
if(query.isSuccess) {
setItem(query.data);
}
}, [itemId, query.data, query.isSuccess])
// 수정 후 저장 호출
const handleClickSave = () => {
modifyMutation.mutate(formData);
}
const closeModal = () => {
if(modifyMutation.isSuccess) {
queryClient.invalidateQueries(['items', itemId]);
queryClient.invalidateQueries('items/list');
}
moveToRead(itemId);
}
return (
{query.isFetching || modifyMutation.isPending ? <FetchingModal /> : <></>}
{modifyMutation.isSuccess ? <ResultModal
title={'Modify'}
content={'수정되었습니다'}
callbackFn={closeModal}/>
: <></>
}
);
}
수정 페이지로 들어갔을 때 데이터가 필요하기 때문에 useQuery
를 통해 데이터를 가져오도록 합니다.
이때 staleTime : Infinity
를 사용하여 데이터를 다시 가져오지 않고 계속 Fresh 한 상태를 유지하도록하고 useEffect
에서 item 의 상태를 변경하도록 합니다.
수정에서는 useMutation
이 필요하기 때문에 수정을 호출하는 함수를 가진 mutation 객체를 생성하고, 수정 버튼을 클릭하면 mutate()
를 호출하도록 합니다.
처리 상태와 처리 중인지를 나타내는 result 와 fetching 이 사라지고, isFetching
과 isSuccess
가 사용됩니다.
modal 이 뜬 이후에 닫기를 눌러 closeModal 이 호출되면 쿼리에 저장되어 있던 데이터를 invalidateQueries
를 이용해 초기화하고, 다시 가져올 수 있도록 합니다.