일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- tanstack query
- 프로그래머스
- 타입스크립트
- JavaScript
- js
- REACT
- 리액트 프로젝트
- 파이썬 slice
- 파이썬 반복문
- 파이썬 replace
- 내일배움캠프 최종 프로젝트
- 파이썬 for in
- 자바스크립트
- 한글 공부 사이트
- 내일배움캠프 프로젝트
- 코딩테스트
- 내일배움캠프
- 타입스크립트 props
- 파이썬 딕셔너리
- 파이썬 enumerate
- 리액트 훅
- useState
- React Hooks
- 파이썬 list
- Next 팀 프로젝트
- typeScript
- 리액트
- 내배캠 프로젝트
- 파이썬 for
- 타입스크립트 리액트
- Today
- Total
sohyeon kim
[React] React Query : 미들웨어 대신 리액트 쿼리 본문
💡 리액트 쿼리에 대해 알아보자.
✳️ 기존 미들웨어의 한계
다른 서버와의 API 통신과 비동기 데이터 관리를 위해 Redux-thunk, Redux-saga 등
미들웨어를 채택해 사용할 수 있지만, 다음과 같은 문제가 있다.
- 보일러 플레이트 : 코드량이 너무 많다.
- 규격화 문제 : Redux 는 비동기 데이터 관리를 위한 전문 라이브러리가 아니다.
✳️ 리액트 쿼리의 강점 : 너무 쉽고, 책임에서 자유롭다.
- 보일러 플레이트 만들다가 오류날 일이 없다.
- 내가 만든 부분이 아니니 잘못이 일어나도 내 잘못이 아니다.
- 사용방법이 기존 thunk 대비 너무 쉽고, 직관적!
✳️ 주요 키워드
- Query : 어떤 데이터에 대한 요청을 의미 - axios 의 get
- Mutation : 어떤 데이터(데이터 그룹 자체)를 변경(추가, 수정 삭제)하는 것, CUD - axios 의 post, put, patch, delete
- Query Invalidation : query 를 invalidatio, 즉 무효화시킨다.
기존에 가져온 query 는 서버 데이터이기 때문에, 언제든지 변경이 있을 수 있다.
그러니 최신 상태가 아닐 수 있다. 그런 경우, 기존의 쿼리를 무효화 시킨 후 최신화 시켜야 한다.이 과정을 리액트 쿼리에선 알아서 해준다! 바로 Query Invalidation 기능으로!
🤓 이제 써보자!
프로젝트 파일 만들고
npm install axios or yarn add axios ~
npm install json-server or yarn add json-server ~
db.json 만들어 데이터 넣어준 뒤 json-server --watch db.json --port 4000
그리고 yarn add react-query
or npm i @tanstack/react-query 🔗 https://velog.io/@gkj8963/React-Query
✚ React Query VS Tanstack Query
리액트 쿼리가 버전 4부터 탄스택 쿼리로 변경됐다. 리액트뿐 아니라 Vue 등 다른 SPA 프레임워크에 적용할 계획 ~ ?
그래서 버전별 설치 명령어가 다른 것!
yarn add react-query "^3.39.3"
yarn add@tanstack/react-query "^4.29.19"
✚❗️ v4, v5부터는 query key 를 반드시 배열 형태로 써줘야 한다.
useQuery("todos", getTodos); // 에러
userQuery(["todos"], getTodos); // 성공
useQuery({queryKey:['todo'],queryFn: getTodos}) // v5 tanstack
db.json
{
"todos": [
{ "id": "1", "title": "todo1", "contents": "react", "isDone": false },
{ "id": "2", "title": "todo2", "contents": "thunk", "isDone": true },
{ "id": "3", "title": "todo3", "contents": "query", "isDone": false }
]
}
src/App.jsx
import { QueryClient, QueryClientProvider } from "react-query";
import Router from "./shared/Router";
const queryClient = new QueryClient(); // 쿼리클라이언트를 만들어
const App = () => {
return (
<QueryClientProvider client={queryClient}> // 기존에 Provider 로 감쌌던 것처럼 감싸준다.
<Router />
</QueryClientProvider>
);
};
export default App;
루트 폴더에 .env 생성
src/api/todos.js 생성
REACT_APP_SERVER_URL = http://localhost:4000
import axios from "axios";
export const getTodos = async () => {
const reponse = await axios.get(`${process.env.REACT_APP_SERVER_URL}/todos`);
console.log("reponse", reponse);
return reponse; // ** 반드시 리턴!
};
아래에서 useQuery 를 쓰기 전.. 잠시 알아보자.
❗️ useQuery 의 첫 번 째 인자 "todos" ➡️ 쿼리의 키! Query Keys
어느 컴포넌트에서든 이 키로 무효화 가능
이름이 꼭 유니크해야한다! 같은 이름으로 또 만들면 안됨!
// Query Keys 의 구조
useQuery('todos', ...)
// 위 코드는 내부적으로는 이렇게 해석된다. 배열 형태!
queryKey === ['todos']
// 💥주의! 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' }]
다음 Query Keys 는 유니크한가 ?
useQuery(['todos', { status, page }], ...) // 1
useQuery(['todos', { page, status }], ...) // 2
useQuery(['todos', { page, status, other: undefined }], ...) // 3
// 객체는 순서를 따지지 않으므로 3번만 유니크하다.
useQuery(['todos', status, page], ...)
useQuery(['todos', page, status], ...)
useQuery(['todos', undefined, page, status], ...)
// 배열은 순서를 따지므로 모두 유니크하다.
❗️ useQuery 의 두 번 째 인자 "fetchTodoList" ➡️ 쿼리 함수! Query Functio
쿼리 함수는 Promise 객체를 return
Promise 객체는 반드시 data 를 resolve 하거나 에러를 낸다.
에러 시 적절한 오류 처리 로직을 반드시 넣어줘야 한다.
useQuery 로 얻은 결과물은 객체다!
그 안에는 우리가 '조회' 를 요청한 결과에 대한 거의 모든 정보가 들어가 있고,
그 과정에 대한 정보도 다음과 같이 들어가 있다. ** thunk 처럼 세팅을 하지 않아도 된다.
- 시작하면 isLoading 이 true 가 된다.
- 조회 결과 오류 시 is Error :true, isLoading : false
error 개체를 통해 더 상세한 오류 내용을 확인할 수 있다. - 조회 결과 정상 처리 시 isSuccess : true, isLoading : false
data 객체를 통해 더 상세한 조회 결과를 확인할 수 있다.
src/redux/components/TodoList/TodoList.jsx
import React from "react";
import { useSelector } from "react-redux";
import { StyledDiv, StyledTodoListHeader, StyledTodoListBox } from "./styles";
import Todo from "../Todo";
import { getTodos } from "../../../api/todos"; // **
import { useQuery } from "react-query"; // **
function TodoList({ isActive }) {
const todos = useSelector((state) => state.todos); // 안 쓸 예정
const { isLoading, isError, data } = useQuery("todos", getTodos);
// useQuery 에는 isLoading, isError, data 이 원래 포함되어 있다!
return (
<StyledDiv>
<StyledTodoListHeader>
{isActive ? "해야 할 일 ⛱" : "완료한 일 ✅"}
</StyledTodoListHeader>
<StyledTodoListBox>
{todos
.filter((item) => item.isDone === !isActive)
.map((item) => {
return <Todo key={item.id} todo={item} isActive={isActive} />;
})}
</StyledTodoListBox>
</StyledDiv>
);
}
export default TodoList;
데이터를 잘 가져오고 있다.
src/api/todos.js
export const getTodos = async () => {
const response = await axios.get(`${process.env.REACT_APP_SERVER_URL}/todos`);
console.log("data", response.data); // [{…}, {…}, {…}]
return response.data; // response 중 데이터만 리턴할 수 있다.
};
💡 useQuery 의 아주 좋은 기능
useQuery 가 아직 진행 중이라면(완료되지 않았다면) isLoading 이 트루다!
App.jsx
const { isLoading, isError, data } = useQuery("todos", getTodos);
if (isLoading) {
return <h1>Loading</h1>;
}
데이터를 가져오는 동안 화면에 로딩 문구가 뜬다.
에러도 추가하면 아래와 같이 뜬다 ~
App.jsx
이제 기존에 useSelector 로 가져왔던 todos 가 아닌
useQuery 로 가져온 data 를 map 으로 돌려보자.
import React from "react";
import { StyledDiv, StyledTodoListHeader, StyledTodoListBox } from "./styles";
import Todo from "../Todo";
import { getTodos } from "../../../api/todos";
import { useQuery } from "react-query";
function TodoList({ isActive }) {
const { isLoading, isError, data } = useQuery("todos", getTodos);
if (isLoading) {
return <h1>Loading</h1>;
}
if (isError) {
return <h1>Error</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;
redux 에 저장해 놨던 todos / useQuery 로 가져온 data
🤓 이제 mutation 을 해보자. 그 중 추가하기!
todos.js 에 addTodo 함수 추가
export const addTodo = async (newTodo) => {
await axios.post(`${process.env.REACT_APP_SERVER_URL}/todos`, newTodo);
};
useMutation 은 인자 두 개를 받는다.
첫 번 째 인자 비동기 함수에 api 에 만들어 둔 addTodo 를 넣어준다.
두 번 째 인자는 결과물 객체! 이 객체는 항상 어느 상태 중 하나에 속한다.
- isldle
- isLoading
- isError - error 객체를 항상 품고 있음
- isSuccess(query 에만 있는게 아니다!) - data 객체를 항상 품고 있음
mutation.mutate(newTodo) 의 인자는 반드시 한 개의 변수나 객체여야 한다.
바뀐 Input.jsx
import { addTodo } from "../../../api/todos";
import { useMutation, useQueryClient } from "react-query";
function Input() {
const queryClient = useQueryClient(); // new QueryClient 를 상위에서 썼으니 아래에서 useQueryClient 를 쓸 수 있다.
const mutation = useMutation(addTodo, {
onSuccess: () => { // 성공시 실행 함수
queryClient.invalidateQueries(""); // 뭘 invalidate 할지
console.log("성공");
},
});
// ...
const handleSubmitButtonClick = (event) => {
// ...
mutation.mutate(newTodo); // dispatch 대신 mutation
기존 src/redux/components/Input/Input.jsx
import React, { useState } from "react";
import LabledInput from "../common/LabledInput";
import HeightBox from "../common/HeightBox";
import { StyledButton } from "./styles";
import { FlexDiv } from "./styles";
import RightMarginBox from "../common/RightMarginBox";
import "./styles";
import { StyledDiv } from "./styles";
import { useDispatch, useSelector } from "react-redux";
import { v4 as uuidv4 } from "uuid";
import { addTodo } from "../../modules/todos";
function Input() {
const dispatch = useDispatch();
// 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 함
// 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>
);
}
export default Input;
제출을 하고 새로고침을 하면 잘 들어가있다.
🤓 이제 바로 화면에 반영되도록 Query Invalidation 을 해보자.
TodoList.jsx
const { isLoading, isError, data } = useQuery("todos", getTodos); // todos 쿼리를 가져오고 있다.
Input.jsx
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries("todos"); // 가져오던 todos 를 무효화, 다시 실행하기 위해
console.log("성공");
},
});
이제 제출하면 바로 새 투두가 뜬다!
'React' 카테고리의 다른 글
[React] Lodash : debouncing 라이브러리 (0) | 2024.02.20 |
---|---|
[React] Throttling & Debouncing : 과도한 이벤트 핸들러 호출 방지 (0) | 2024.02.20 |
[React] Error : Cannot read properties of null (reading 'useMemo') (0) | 2024.02.20 |
[React] Custom Hooks : 훅 만들기 (0) | 2024.02.20 |
[React] Redux Thunk App (0) | 2024.02.20 |