๐ React Query๋?
1) ๋ฑ์ฅ ๋ฐฐ๊ฒฝ
Redux-thunk, Redux-Saga์ ๊ฐ์ ๊ธฐ์กด์ ๋ฏธ๋ค์จ์ด๋ ์ฝ๋๋์ด ๋๋ฌด ๋ง๊ณ , Redux๊ฐ ๋น๋๊ธฐ ๋ฐ์ดํฐ ๊ด๋ฆฌ๋ฅผ ์ํ ์ ๋ฌธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๋๋ผ๋ ๋ฌธ์ ๊ฐ ์๋ค. ์ด๋ฌํ ๋ฌธ์ ์ ์ ๋ณด์ํ๊ธฐ ์ํด React Query๊ฐ ๋ฑ์ฅํ๋ค. React Query๋ ๋ณด์ผ๋ฌ ํ๋ ์ดํธ๋ฅผ ๋ง๋ค๋ค๊ฐ ์ค๋ฅ๊ฐ ๋ ์ผ์ด ์๊ณ , ์ฌ์ฉ๋ฐฉ๋ฒ์ด thunk์ ๋นํด ์ฝ๊ณ ์ง๊ด์ ์ด๋ค.
2) ์ฃผ์ ํค์๋
Query : ๋ฐ์ดํฐ์ ๋ํ ์์ฒญ์ ์๋ฏธ
Mutation : ๋ฐ์ดํฐ์ ๋ํ ๋ณ๊ฒฝ์ ์๋ฏธ (์ถ๊ฐ/์์ /์ญ์ , ์ฆ CUD)
Query Invaildation : Query๋ฅผ ๋ฌดํจํ ์ํจ๋ค๋ ์๋ฏธ
๋ฌดํจํ ์ํจ๋ค๋ ๋ฌด์จ ์๋ฏธ? ๊ธฐ์กด์ ๊ฐ์ ธ์จ Query๋ ์๋ฒ ๋ฐ์ดํฐ์ด๊ธฐ ๋๋ฌธ์ ์ธ์ ๋ ์ง ๋ณ๊ฒฝ์ด ์์ ์ ์๋ค. ์ฆ ์ต์ ์ํ๊ฐ ์๋ ์ ์๋ค. ์ด๋ฐ ๊ฒฝ์ฐ ๊ธฐ์กด์ ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํจํ ์ํจ ํ ์ต์ ํ ์์ผ์ผ ํ๋ค.
๐ React Query ์ฌ์ฉํ๊ธฐ
1) ํจํค์ง ์ค์นํ๊ธฐ
yarn add react-query
2) App.jsx(์์์ปดํฌ๋ํธ)์์ React Query ๊ด๋ จ ์ค์ ํ๊ธฐ
import React from "react";
import Router from "./shared/Router";
import { QueryClient, QueryClientProvider } from "react-query";
const queryClient = new QueryClient();
const App = () => {
return (
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
);
};
export default App;
3) .env ์์ฑ (ํ๊ฒฝ์ ๋ณด ๊ด๋ฆฌ)
REACT_APP_SERVER_URL = http://localhost:4000
4) src > api > todos.js ์์ฑ
// axios ์์ฒญ์ด ๋ค์ด๊ฐ๋ ๋ชจ๋ ๋ชจ๋
import axios from "axios";
// ์กฐํ
const getTodos = async () => {
const response = await axios.get(`${process.env.REACT_APP_SERVER_URL}/todos`);
return response.data;
};
export { getTodos };
5) Todolist.jsx ์ปดํฌ๋ํธ ๋ด์์ ๊ฐ์ ธ์จ ๋ฐ์ดํฐ ์กฐํํ๊ธฐ
import React from "react";
import { StyledDiv, StyledTodoListHeader, StyledTodoListBox } from "./styles";
import Todo from "../Todo";
import { getTodos } from "../../../api/todos";
import { useQuery } from "react-query";
/**
* ์ปดํฌ๋ํธ ๊ฐ์ : ๋ฉ์ธ > TODOLIST. ํ ์ผ์ ๋ชฉ๋ก์ ๊ฐ์ง๊ณ ์๋ ์ปดํฌ๋ํธ
* 2022.12.16 : ์ต์ด ์์ฑ
*
* @returns TodoList ์ปดํฌ๋ํธ
*/
function TodoList({ isActive }) {
// const todos = useSelector((state) => state.todos);
// ๋ฐ์ดํฐ ์กฐํ
// useQuery์ ์ฒซ๋ฒ์งธ ์ธ์๋ ์ฟผ๋ฆฌ์ ์ด๋ฆ(์ฟผ๋ฆฌํค!!), ์ฟผ๋ฆฌํจ์!!(์กฐํ๋ฅผ ํ๋ ๋น๋๊ธฐ ํจ์,api)
// ๋ฆฌ์กํธ ์ฟผ๋ฆฌ์์๋ useQuery์ ๊ฒฐ๊ณผ๊ฐ(๊ฐ์ฒด)์ผ๋ก isLoding, isError, data๋ฅผ ๋ชจ๋ ์ ๊ณต
// ๊ตฌ์กฐ๋ถํดํ ๋น์ผ๋ก ๋ฐ์์ค๊ธฐ
const { isLoding, isError, data } = useQuery("todos", getTodos);
if (isLoding) {
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;
6) src > api > todos.js์์ ๋ฐ์ดํฐ ์ถ๊ฐ ์์ฒญํ๊ธฐ
// axios ์์ฒญ์ด ๋ค์ด๊ฐ๋ ๋ชจ๋ ๋ชจ๋
import axios from "axios";
// ์กฐํ
const getTodos = async () => {
const response = await axios.get(`${process.env.REACT_APP_SERVER_URL}/todos`);
return response.data;
};
// ์ถ๊ฐ
const addTodo = async (newTodo) => {
await axios.post(`${process.env.REACT_APP_SERVER_URL}/todos`, newTodo);
};
export { getTodos, addTodo };
7) Input.jsx ์ปดํฌ๋ํธ ๋ด์์ ์ถ๊ฐํ๋ ๊ธฐ๋ฅ์ด ๋ฆฌ์กํธ ์ฟผ๋ฆฌ๋ฅผ ํตํด ๋์ํ๋๋ก ํ๊ธฐ
// (1) ๋ง๋ค์ด ๋์ api import ํ๊ธฐ
import { addTodo } from "../../../api/todos";
// (2) useQueryClient() ์ด์ฉํ๊ธฐ
// useQueryClient๋ฅผ ํตํด ์์์ปดํฌ๋ํธ์์ ๋ง๋ ๊ฒ์ ์ด์ฉํด์
// ํ๋์ ํ๋ฆ์ผ๋ก์ ์ฟผ๋ฆฌ ํด๋ผ์ด์ธํธ๋ฅผ ์ด์ฉํ ์ ์์
const queryClient = useQueryClient();
// (3) Mutation
// useMutation์ ์ฒซ๋ฒ์งธ ์ธ์๋ก๋ ๋ง๋ค์ด ๋์ api, ๋๋ฒ์งธ ์ธ์๋ก๋ ๊ฐ์ฒด๊ฐ ๋ค์ด๊ฐ
// ๊ฐ์ฒด์๋ ์ฑ๊ณต๊ณผ ์คํจ์ ๋ํ ํค๋ฒจ๋ฅ๊ฐ ๋ค์ด๊ฐ
const mutation = useMutation(addTodo, {
// (4) invalidate (๋ฐ๋ก ๊ฐฑ์ !)
onSuccess: () => {
// [์ฑ๊ณต] "todos"๋ก ์ฝ์ด ์จ ๋ฐ์ดํฐ๋ฅผ ๋ฌดํจํํ๊ณ ๋ค์ ๋ถ๋ฌ์ด!
queryClient.invalidateQueries("todos");
console.log("์ฑ๊ณตํ์์ต๋๋ค!");
},
});
// (5) dispatch๋ก ํธ์ถํ๋ ๋ถ๋ถ์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑ
// api์ ๋ํ ์ธ์๋ฅผ ๋ฃ์ด์ค
mutation.mutate(newTodo);
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";
// (1) ๋ง๋ค์ด ๋์ api import ํ๊ธฐ
import { addTodo } from "../../../api/todos";
import { useMutation, useQueries, useQueryClient } from "react-query";
/**
* ์ปดํฌ๋ํธ ๊ฐ์ : Todo ๋ฉ์ธ ํ์ด์ง์์ ์ ๋ชฉ๊ณผ ๋ด์ฉ์ ์
๋ ฅํ๋ ์์ญ
* 2022.12.16 : ์ต์ด ์์ฑ
*
* @returns Input ์ปดํฌ๋ํธ
*/
function Input() {
const dispatch = useDispatch();
// (2) useQueryClient() ์ด์ฉํ๊ธฐ
// useQueryClient๋ฅผ ํตํด ์์์ปดํฌ๋ํธ์์ ๋ง๋ ๊ฒ์ ์ด์ฉํด์
// ํ๋์ ํ๋ฆ์ผ๋ก์ ์ฟผ๋ฆฌ ํด๋ผ์ด์ธํธ๋ฅผ ์ด์ฉํ ์ ์์
const queryClient = useQueryClient();
// (3) Mutation
// useMutation์ ์ฒซ๋ฒ์งธ ์ธ์๋ก๋ ๋ง๋ค์ด ๋์ api, ๋๋ฒ์งธ ์ธ์๋ก๋ ๊ฐ์ฒด๊ฐ ๋ค์ด๊ฐ
// ๊ฐ์ฒด์๋ ์ฑ๊ณต๊ณผ ์คํจ์ ๋ํ ํค๋ฒจ๋ฅ๊ฐ ๋ค์ด๊ฐ
const mutation = useMutation(addTodo, {
// (4) invalidate (๋ฐ๋ก ๊ฐฑ์ !)
onSuccess: () => {
// [์ฑ๊ณต] "todos"๋ก ์ฝ์ด ์จ ๋ฐ์ดํฐ๋ฅผ ๋ฌดํจํํ๊ณ ๋ค์ ๋ถ๋ฌ์ด!
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));
// (5) dispatch๋ก ํธ์ถํ๋ ๋ถ๋ถ์ ๋ค์๊ณผ ๊ฐ์ด ์์ฑ
// api์ ๋ํ ์ธ์๋ฅผ ๋ฃ์ด์ค
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>
);
}
export default Input;