1. 리액트 쿼리의 엄청난 내장 기능
isLoading, isError의 기능이 내장되어 있어 자동으로 처리해준다!!! 👉 편리하다
return 문에 도착하기 전에 isLoading 또는 isError에 따라 별도의 처리를 해주기 때문에 대기 상태 처리 / 오류 처리에 대한 부분이 아주 쉽게 해결이 된다.
기존 미들웨어 코드들에 비해서 코드량이 적고 규격화되어 있기 때문에 책임 소재가 분명해지게 된다.

2. 리액트 쿼리 사용 방법
2-0. 리액트 쿼리 설치
yarn add react-query
2-1. 모든 루트를 QueryClientProvider 로 감싼다.
QueryClientProvider는 데이터를 읽어오는 기능(QueryClient)을 애플리케이션 전체에 주입하도록 하는 API 이다.
import React from "react";
import Router from "./shared/Router";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
const App = () => {
// QueryClientProvider 주입.
// QueryClientProvider : 데이터를 읽어오는 기능(QueryClient)을 애플리케이션 전체에 주입하도록 하는 API
return (
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
);
};
export default App;
2-2. api 폴더에 api 파일 만들기.
[.env]

환경변수 파일을 root 폴더에 만든다.
REACT_APP_SERVER_URL=http://localhost:4000
[api.js]
api를 만든다.
// axios 요청이 들어가는 모든 모듈
import axios from "axios";
// 조회 REACT_APP_SERVER_URL
// .env 환경변수 만들 때는 REACT_APP 를 식별자 명 앞에 꼭 붙여줘야 한다.
const getTodos = async () => {
// 환경정보 바꾸면 재시작해야 함.
const response = await axios.get(
`${process.env.REACT_APP_SERVER_URL}/todos`
);
// console.log(response);
console.log(response.data);
return response.data;
};
// 추가
// 이 함수는 어떤 투두를 추가해야 하는지 알아야 하기 때문에 newTodo를 인자로 받음.
const addTodo = async (newTodo) => {
await axios.post(`${process.env.REACT_APP_SERVER_URL}/todos`, newTodo);
};
export { getTodos, addTodo };
2-3. 컴포넌트에서 사용하기
기존의 Global state를 읽어올 때 썼던 useSelector를 대체한다.
[TodoList.jsx : 카드 뿌리는 곳]
⭐ useQuery의 형태 볼 것. 첫 번째 인자로 query keys, 두 번째 인자로 query (callback)function을 가진다.
function TodoList({ isActive }) {
// const todos = useSelector((state) => state.todos);
// useQuery의 첫 번째 인자는 쿼리의 키[이름]. 두 번째 인자는 조회를 해오는 비동기 함수.
// 첫 번째 인자는 애플리케이션 전체 맥락에서 이 쿼리를 공유하는 방법으로 쓰임. 그 어느 컴포넌트에 뿌려져 있어도 같은 key면 같은 쿼리 및 데이터 보장!!
// 그렇기 때문에 querykey는 unique 해야 함.
const { isLoading, isError, data } = useQuery("todos", getTodos);
if (isLoading) {
return <h1>로딩중입니다...!</h1>;
}
if (isError) {
return <h1>오류가 발생하였습니다...!</h1>;
}
return (
<StyledDiv>
<StyledTodoListHeader>
{isActive ? "해야 할 일 ⛱" : "완료한 일 ✅"}
</StyledTodoListHeader>
<StyledTodoListBox>
{data
.filter((item) => item.isDone === !isActive)
.map((item) => {
return (
<Todo
key={item.id}
todo={item}
isActive={isActive}
/>
);
})}
</StyledTodoListBox>
</StyledDiv>
);
}
export default TodoList;
query keys
1. refetching 하는 데에 쓰임.(fetch : 구해오다, 가져오다 => refetch: 다시 불러오다.)(= invalidate랑 관련 있는 개념)
2. 캐싱(caching) 처리를 하는 데에도 쓰인다.
3. 애플리케이션 전체 맥락에서 이 쿼리를 공유하기 위한 방법으로 쓰인다.
같은 query key면 같은 쿼리 및 데이터를 보장한다. 그렇기 때문에 unique 해야 한다. 아래와 예시와 같이 겹치면 절대 안 된다.
const query1 = useQuery('qk', api); // unique
const query2 = useQuery('qk2', api); // not unique
const query3 = useQuery('qk2', api); // not unique
세분화하고 unique 함을 지키기 위해 다음과 같이 여러가지 형태가 들어갈 수 있다.
// 💥주의! key는 표현이 그렇다는거지, api 로직과는 관련이 없어요!
// ID가 5인 todo 아이템 1개
useQuery(['todo', 5], ...)
// queryKey === ['todo', 5]
// ID가 5인 todo 아이템 1개인데, preview 속성은 true야
useQuery(['todo', 5, { preview: true }], ...)
// queryKey === ['todo', 5, { preview: true }]
// todolist 전체인데, type은 done이야
useQuery(['todos', { type: 'done' }], ...)
// queryKey === ['todos', { type: 'done' }]
⭐ Unique 한 쿼리 키인지 아닌지 판별해보기.
[객체는 주소를 프로퍼티 명으로 따지기 때문에 순서는 상관없어서 유니크하지 않다.]
똑같은 키이다. 하지만 세 번째는 또 다른 프로퍼티를 가지고 있기 때문에 위에 두 줄과는 다른 유니크한 키를 가지고 있는 것이다.
useQuery(['todos', { status, page }], ...)
useQuery(['todos', { page, status }], ...)
// useQuery(['todos', { page, status, other: undefined }], ...)
[배열은 주소를 인덱스로 따지기 때문에 유니크하다.]
유니크한 키이다.
useQuery(['todos', status, page], ...)
useQuery(['todos', page, status], ...)
// useQuery(['todos', undefined, page, status], ...)
query function
1. useQuery의 두 번째 인자 쿼리 함수는 promise 객체를 return 함.
promise 객체는 반드시 data를 resolve 하거나 에러를 내야 함.
에러가 발생하는 경우에는 그에 맞는 적젏한 오류 처리 관련 로직을 삽입해서 처리를 해줘야 함.
axios, fetch, graphql 중 어떤 방법을 이용하던지 적절한 오류 처리를 통해 사용자를 혼란에 빠뜨리지 않게 해야 한다.
useQuery를 통해 얻은 결과물
1. useQuery를 통해 얻은 결과물은은 객체(Object)임.
안에 isLoading, isError, data 등이 있음. 이런 것들을 사용해서 컴포넌트 내부에서 유용하게 사용할 수 있음.
useMutation
1. query와는 다르게 mutation은 CRUD 중 CUD에서 사용된다.
2. mutatoin.mutate(인자) 로 사용하게 될 때
인자는 반드시 한 개의 변수 또는 객체여야 한다.
ex) mutation.mutate(인자1, 인자2) 👉 오류
3. 결과를 객체(object 형태로) 갖고 있다.
4. 그 결과물 객체는 항상 어느 상태 중 하나에 속한다.
✔ isidle(: 게으른, 나태한, 가동되지 않는 이라는 뜻)
✔ isLoading
✔ isError : error 객체를 항상 품고 있음!
✔ isSuccess(query에만 있는 것이 아님): data 객체를 항상 품고 있음!
성공이 일어났을 때는 반드시 onSuccess가 필요한지 판단해야 함.
변경이 일어난 경우 혹시나 갱신해줘야 하는 데이터가 없는지를 머릿 속으로 생각을 해줘야 함.
그런 게 있다 하면 해당 쿼리 키를 반드시 invalidate 해줘야 한다.
예시로 이해하기
[Input.jsx : 카드에 입력할 내용 쓰는 곳]
⭐ useQueryClient 사용, TodoList에서 호출한 useQuery에 썼었던 queryKey를 어떻게 사용하는 지 주석 설명 볼 것.
⭐ 기존 dispatch로 실어나르던 값을 mutation.mutate(newTodo)가 대체하는 부분 볼 것.
⭐ useMutation API를 사용한 invalidate 과정 볼 것.
💬 [invalidate의 과정]
Input.jsx에서 값 입력으로 인해 서버 데이터가 변경됨
→ onSuccess가 일어나면 기존의 Query인 “todos”는 무효화
→ 새로운 데이터를 가져와서 “todos”를 최신화시킴
→ TodoList.jsx를 갱신함
따라서 계속해서 리액트 앱은 최신 상태의 서버 데이터를 유지할 수 있게 됨. => refresh 기능이라고 보면 됨.
function Input() {
// const dispatch = useDispatch();
// 리액트 쿼리 관련 코드
// useQueryClient : 상위 컴포넌트(App.jsx에 보면 QueryClientProvider를 주입해놨음.) 에서 만든 것을 이용해서 하나의 흐름으로서 쿼리 클라이언트를 이용
// useMutation : 변형[변경]할 수 있는 리액트 쿼리의 방법임. 첫 번째 인자로 우리가 만들어놓은 api, 두 번째 인자로 객체 성공했을 때의 실패했을 때의 키,value가 들어감.
const queryClient = useQueryClient();
const mutation = useMutation(addTodo, {
onSuccess: () => {
// invalidateQueries 어떤 것을 invalid 하게 만드는 역할을 함.
// invalid 하게 만든다는 것은 그 데이터가 이제는 (싱크가) 맞지 않는다고 선언을 하는 것입니다.
// 이거 예시 기억나죠?
// 20시에 데이터를 받아왔는데 누군가 21시에 데이터를 더 넣으면 데이터가 달라진 상태라고 예시 들었던 거.
// 싱크가 안 맞는 상태를 해결하는 것입니다.
// query key를 넣어주면 그 query key를 가지는 쿼리를 무효화시키고 다시 불러오는 기능을 합니다.
// 간단하게 말하자면 refresh를 해줍니다.
queryClient.invalidateQueries("todos");
console.log("성공하였습니다.");
},
});
// useSelector를 통한, store의 값 접근
const todos = useSelector((state) => state.todos);
// 컴포넌트 내부에서 사용할 state 2개(제목, 내용) 정의
const [title, setTitle] = useState("");
const [contents, setContents] = useState("");
// 에러 메시지 발생 함수
const getErrorMsg = (errorCode, params) => {
switch (errorCode) {
case "01":
return alert(
`[필수 입력 값 검증 실패 안내]\n\n제목과 내용은 모두 입력돼야 합니다. 입력값을 확인해주세요.\n입력된 값(제목 : '${params.title}', 내용 : '${params.contents}')`
);
case "02":
return alert(
`[내용 중복 안내]\n\n입력하신 제목('${params.title}')및 내용('${params.contents}')과 일치하는 TODO는 이미 TODO LIST에 등록되어 있습니다.\n기 등록한 TODO ITEM의 수정을 원하시면 해당 아이템의 [상세보기]-[수정]을 이용해주세요.`
);
default:
return `시스템 내부 오류가 발생하였습니다. 고객센터로 연락주세요.`;
}
};
// title의 변경을 감지하는 함수
const handleTitleChange = (event) => {
setTitle(event.target.value);
};
// contents의 변경을 감지하는 함수
const handleContentsChange = (event) => {
setContents(event.target.value);
};
// form 태그 내부에서의 submit이 실행된 경우 호출되는 함수
const handleSubmitButtonClick = (event) => {
// submit의 고유 기능인, 새로고침(refresh)을 막아주는 역함
event.preventDefault();
// 제목과 내용이 모두 존재해야만 정상처리(하나라도 없는 경우 오류 발생)
// "01" : 필수 입력값 검증 실패 안내
if (!title || !contents) {
return getErrorMsg("01", { title, contents });
}
// 이미 존재하는 todo 항목이면 오류
const validationArr = todos.filter(
(item) => item.title === title && item.contents === contents
);
// "02" : 내용 중복 안내
if (validationArr.length > 0) {
return getErrorMsg("02", { title, contents });
}
// 추가하려는 todo를 newTodo라는 객체로 세로 만듦
const newTodo = {
title,
contents,
isDone: false,
id: uuidv4(),
};
// todo를 추가하는 reducer 호출
// 인자 : payload
// dispatch(addTodo(newTodo));
// ⭐ 기존 dispatch문 대신 사용.
mutation.mutate(newTodo);
// state 두 개를 초기화
setTitle("");
setContents("");
};
return (
<StyledDiv>
<form onSubmit={handleSubmitButtonClick}>
<FlexDiv>
<RightMarginBox margin={10}>
<LabledInput
id="title"
label="제목"
placeholder="제목을 입력해주세요."
value={title}
onChange={handleTitleChange}
/>
<HeightBox height={10} />
<LabledInput
id="contents"
label="내용"
placeholder="내용을 입력해주세요."
value={contents}
onChange={handleContentsChange}
/>
</RightMarginBox>
<StyledButton type="submit">제출</StyledButton>
</FlexDiv>
</form>
</StyledDiv>
);
}