230711 React Query

๋‚˜์œค๋นˆยท2023๋…„ 7์›” 11์ผ
0

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
18/55

๐Ÿ“Œ 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;
profile
ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž๋ฅผ ๊ฟˆ๊พธ๋Š”

0๊ฐœ์˜ ๋Œ“๊ธ€