[React] react-query 사용하기

참고

  • https://kyounghwan01.github.io/blog/React/react-query/basic/#%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%82%E1%85%B3%E1%86%AB-%E1%84%8B%E1%85%B5%E1%84%8B%E1%85%B2

개요

  • Vite 을 통해 테스트 진행할 리액트 프로젝트 하나를 생성합니다.
  • 생성된 프로젝트에 react-query 를 다운받고, CRUD 기능을 통해 react-query 사용 방법에 대해서 익힙니다.

사용 기술

  • react-query 테스트 프로젝트를 개발하기 위해, 사전에 필요한 라이브러리 정보는 다음과 같습니다.

    • vite
    • react-query
    • react-query-devtools
    • json-server
    • uuid
  • 테스트 프로젝트에서 사용될 라이브러리 목록은 위와 같습니다.

  • 각 라이브러리는 프로젝트 진행해 나가면서, 필요할 때 마다 설치 진행하겠습니다.


Vite 설치

  • 저는 Vite 를 사용해서 프로젝트를 진행해 나가려고 합니다.
  • Vite 는 빠르고 간결한 모던 웹 프로젝트 개발 경험에 맞춰 탄생항 빌드 도구입니다.
  • 자세한 내용은 https://vitejs-kr.github.io/guide/ 해당 사이트에서 확인하시면 됩니다.
  • 앞서 생성한 리액트 프로젝트에서 Vite 를 설치 해줍니다.
  • Vite 설치하는 방법은 아래와 같습니다.
npm create vite@latest ./

  • 위 명령어를 통해 Vite를 설치 합니다.

  • 정상적으로 설치가 되었다면, 아래와 같이 프로젝트가 생성된 것을 확인할 수 있습니다.

  • 다음으로, npm install 명령을 통해 package.json 파일의 의존이 되어있는 패키지들을 설치 진행해줍니다.
npm install
  • 여기까지 모두 완료 하였다면, 이제 정상적으로 실행 되는지 확인을 위해서 Vite 를 실행합니다.
  • Vite 를 실행하는 명령어는 다음과 같습니다.
npm run dev
  • npm run dev 명령을 실행하게 되면, 아래와 같이 로그가 출력됩니다.

  VITE v4.2.1  ready in 1015 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help
  • 위에서 http://localhost:5173 주소가 보이는데, 여기로 접속 합니다.
  • 문제 없이 설치가 되었다면, 아래와 같이 Vite + React 화면이 나오는 것을 확인할 수 있습니다.


불필요한 파일 삭제

  • 다음으로, 프로젝트 개발에 앞서 필요없는 파일 3가지를 우선 삭제 하도록 하겠습니다.
  • 삭제 진행할 파일 목록은 다음과 같습니다.
    • assets
    • App.css
    • index.css
  • 앞서 Vite 를 통해 생성했던 프로젝트에서 위 3가지 파일을 삭제합니다.

  • 위와 같이 삭제를 완료 하였다면, 남은 파일 목록은 아래와 같습니다.


Router 구성

  • 앞에서 불필요한 파일을 삭제 하였다면, 이제 라우터를 구성합니다.
  • 먼저, 라우터를 구성하기 위해 필요한 패키지인 react-router-dom 패키지를 설치 합니다.
  • 설치 명령어는 아래와 같습니다.
npm i react-router-dom
  • 설치가 완료 되었다면, main.jsxBrowserRouter 를 추가해 줍니다.
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <BrowserRouter>
            <App />
        </BrowserRouter>
    </React.StrictMode>
);
  • 위와 같이 BrowserRouter 를 추가해 주었습니다.

App.jsx 에 페이지 라우터 구성

  • React-Query 테스트를 위해, 총 3개의 페이지를 구성합니다.
  • 아직 페이지를 생성하지는 않았지만, 아래와 같이 PostLists, Post, EditPost 3개의 페이지 Router 를 구성해 줍니다.
import { useState } from "react";

function App() {
    return (
        <div className="App">
            <h1>Awesome blog</h1>
            <Routes>
                <Route path="/" element={<PostLists />} />
                <Route path="/post/:id" element={<Post />} />
                <Route path="/post/:id/edit" element={<EditPost />} />
            </Routes>
        </div>
    );
}

export default App;
  • 위에서 PostLists, Post, EditPost 페이지는 하나씩 구현해 나갈 예정입니다.

react-query 설치

  • App.jsx 에 Router 구성이 완료 되었다면, 페이지와 컴포넌트를 추가하기에 앞서 react-query 를 먼저 설치를 진행합니다.
  • react query 의 공식 사이트는 https://tanstack.com/query/v3/ 해당 링크를 통해 확인하실 수 있습니다.
  • react-query 설치하는 명령어는 아래와 같습니다.
npm i @tanstack/react-query
  • react-qeury 를 추가 완료 하였다면, react-query 전용 devtools 도 설치해줍니디다.
npm i @tanstack/react-query-devtools
  • 위와 같이 설치가 모두 완료 되었다면, react-query 를 사용할 준비는 모두 마쳤습니다.
  • react-query, react-query devtools 를 사용하려면, main.jsx 에 관련 속성을 추가해 주어야 합니다.
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

// create a client
const queryClient = new QueryClient();

ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <BrowserRouter>
            <QueryClientProvider client={queryClient}>
                <App />
                <ReactQueryDevtools initialIsOpen={false} />
            </QueryClientProvider>
        </BrowserRouter>
    </React.StrictMode>
);

json-server 설치 및 구성

  • react-query 를 사용하기에 앞서, client 에서 서버쪽으로 데이터를 요청해서 조회해야 할 서버 쪽 데이터가 필요합니다.
  • 하지만, 서버를 구성하기에는 워낙 시간적 비용이 많이 들기 떄문에 원활한 테스트를 위해 json-server 를 이용하여 가짜 서버 역학을 하려고 합니다.
  • json-server 설치는 다음 명령어를 통해 가능합니다.
npm install -g json-server
  • 설치가 완료 되었으면, 프로젝트 root 경로에 db.json 파일을 하나 생성해 줍니다.
  • 그리고 아래와 같이 테스트 데이터를 json 형식으로 추가합니다.
{
    "posts": [
        {
            "id": "1",
            "title": "title",
            "body": "body"
        }
    ]
}
  • 우선 posts 이름을 가진 데이터 Array 가 있고, 여기에는 1개의 item 을 가지고 있는 형태의 json 데이터를 하나 생성하였습니다.
  • 그리고 json-server 를 실행시켜 주면 됩니다.
  • json-server 실행 시켜주는 명령어는 아래와 같습니다.
json-server --watch db.json
  • 위 명령어를 통해, json-server 를 실행시킬 수 있습니다.
  • 정상적으로 실행 되었다면, 아래와 같이 로그가 출력됩니다.
\{^_^}/ hi!

  Loading db.json
  Done

  Resources
  http://localhost:3000/posts

  Home
  http://localhost:3000

  Type s + enter at any time to create a snapshot of the database
  Watching...

CRUD API 서비스 추가

  • 다음으로 react-query 를 이용하여 서버의 데이터를 CRUD 하기 위한 api 파일 하나를 생성합니다.
  • src/api/posts.jsx 경로에 posts.jsx 파일을 하나 생성 후, 서버의 데이터를 요청하는 로직을 추가해 줍니다.
  • 여기서 저는 fetch API 를 사용하였습니다.
export async function fetchPosts() {
    const response = await fetch("http://localhost:3000/posts");
    return response.json();
}

export async function fetchPost(id) {
    const response = await fetch(`http://localhost:3000/posts/${id}`);
    return response.json();
}

export async function createPost(newPost) {
    const response = await fetch(`http://localhost:3000/posts`, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(newPost),
    });
    return response.json();
}

export async function updatePost(updatedPost) {
    const response = await fetch(`http://localhost:3000/posts/${updatedPost.id}`, {
        method: "PUT",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify(updatedPost),
    });
    return response.json();
}

export async function deletePost(id) {
    const response = await fetch(`http://localhost:3000/posts/${id}`, {
        method: "DELETE",
    });
    return response.json();
}
  • 위에서 작성한 함수들을 바로 뒤에서 만들 페이지 및 컴포넌트에서 사용할 예정입니다.

페이지 및 컴포넌트 추가

  • 앞서, react-query 와 react-query devtools 설정을 완료 하였습니다.
  • 또한, App.jsx 에 3개의 페이지 전환을 위한 라우터 구성도 완료하였습니다.
  • 이제 앞서 추가했던, PostLists, Post, EditPost 3개의 페이지 및 관련 컴포넌트들을 추가하도록 하겠습니다.

Post 페이지 생성

  • src/pages/Post.jsx 경로로 해서 Post 페이지를 추가하였습니다.
  • Post.jsx 코드는 다음과 같습니다.
import { useQuery } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { fetchPost } from "../api/posts";
import { useNavigate } from "react-router-dom";

export default function Post() {
    const navigate = useNavigate();
    const { id } = useParams();
    const {
        isLoading,
        isError,
        data: post,
        error,
    } = useQuery({
        queryKey: ["posts", id],
        queryFn: () => fetchPost(id),
    });

    if (isLoading) return "loading...";
    if (isError) return `Error: ${error.message}`;

    return (
        <div>
            <button
                onClick={() => {
                    navigate("/");
                }}
            >
                back to list posts
            </button>
            <h1>{post.title}</h1>
            <p>{post.author}</p>
        </div>
    );
}
  • Post.jsx 페이지 코드를 보시면, useQuery 를 이용하여 데이터를 불러오는 것을 확인하실 수 있습니다.
  • useQuery 는 데이터를 get 하기 위한 api 입니다.
  • post, update, delete 는 useMutation 을 사용합니다.
  • 첫 번째 파라미터로 unique key 가 들어가고, 두 번째 파라미터로 비동기 함수(api 호출 함수) 가 들어갑니다. (두 번째 파라미터는 promise 가 들어가야 합니다.)
  • 첫 번째 파라미터로 설정한 unique Key 는 다른 컴포넌트에서도 해당 키를 사용하면 호출 가능합니다. unique key 는 string 배열을 받습니다. 배열로 넘기면 0번 값은 string 값으로 다른 컴포넌트에서 부를 값이 들어가고 두 번째 값을 넣으면 query 함수 내부에 파리미터로 해당 값이 전달됩니다.
  • return 값은 api의 성공, 실패여부, api return 값을 포함한 객체입니다.
  • useQuery는 비동기로 작동합니다. 즉, 한 컴포넌트에 여러개의 useQuery가 있다면 하나가 끝나고 다음 useQuery가 실행되는 것이 아닌 두개의 useQuery가 동시에 실행됩니다. 여러개의 비동기 query가 있다면 useQuery보다는 밑에 설명해 드릴 useQueries를 권유드립니다.
  • enabled를 사용하면 useQuery를 동기적으로 사용 가능합니다.

PostList 페이지 생성

  • src/pages/PostList.jsx 경로로 해서 PostList 페이지를 추가하였습니다.
  • PostList.jsx 코드는 다음과 같습니다.
import AddPost from "../components/AddPost";
import { QueryClient, useQueryClient, useMutation, useQuery } from "@tanstack/react-query";
import { deletePost, fetchPosts } from "../api/posts";
import { useNavigate } from "react-router-dom";
import { useEffect, useMemo, useState } from "react";

export default function PostLists() {
    const queryClient = useQueryClient();
    const navigate = useNavigate();
    const {
        isLoading,
        isError,
        data: posts,
        error,
    } = useQuery({
        queryKey: ["posts"],
        queryFn: fetchPosts,
    });

    const deletePostMutation = useMutation({
        mutationFn: deletePost,
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: ["posts"] });
        },
    });

    const handleDelete = (id) => {
        deletePostMutation.mutate(id);
    };

    if (isLoading) return "loading...";
    if (isError) return `Error: ${error.message}`;

    return (
        <div>
            <AddPost />
            {posts.map((post) => (
                <div key={post.id} style={{ background: "#777" }}>
                    <h4 style={{ cursor: "pointer" }} onClick={() => navigate(`/post/${post.id}`)}>
                        {post.title}
                    </h4>
                    <button onClick={() => navigate(`/post/${post.id}/edit`)}>Edit</button>
                    <button onClick={() => handleDelete(post.id)}>Delete</button>
                </div>
            ))}
        </div>
    );
}

EditPost 페이지 생성

  • src/pages/EditPost.jsx 경로로 해서 EditPost 페이지를 추가하였습니다.
  • EditPost.jsx 코드는 다음과 같습니다.
import PostForm from "../components/PostForm";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useParams } from "react-router-dom";
import { fetchPost, updatePost } from "../api/posts";
import { useNavigate } from "react-router-dom";

export default function EditPost() {
    const queryClient = useQueryClient();
    const navigate = useNavigate();
    const { id } = useParams();
    const {
        isLoading,
        isError,
        data: post,
        error,
    } = useQuery({
        queryKey: ["posts", id],
        queryFn: () => fetchPost(id),
    });
    const updatePostMutation = useMutation({
        mutationFn: updatePost,
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: ["posts"] });
            navigate("/");
        },
    });

    if (isLoading) return "loading...";
    if (isError) return `Error: ${error.message}`;

    const handleSubmit = (updatePost) => {
        updatePostMutation.mutate({ id, ...updatePost });
    };

    return (
        <div>
            <PostForm onSubmit={handleSubmit} initialValue={post} />
        </div>
    );
}

AddPost.jsx 컴포넌트 생성

  • 앞서 페이지에서 사용하는 공통 컴포넌트 중 하나인 AddPost.jsx 컴포넌트 코드입니다.
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { createPost } from "../api/posts";
import PostForm from "./PostForm";
import { v4 as uuidv4 } from "uuid";

export default function AddPost() {
    const queryClient = useQueryClient();

    const createPostMutation = useMutation({
        mutationFn: createPost,
        onSuccess: () => {
            queryClient.invalidateQueries({ queryKey: ["posts"] });
            console.log("success bro!");
        },
    });

    const handleAddPost = (post) => {
        createPostMutation.mutate({
            id: uuidv4(),
            ...post,
        });
    };

    return (
        <div>
            <h2>Add new post</h2>
            <PostForm onSubmit={handleAddPost} initialValue={{}} />
        </div>
    );
}
  • 위 코드에서 핵심은 useMutation react-query 에서 제공해주는 hook 을 이용하여 데이터를 추가해주고 있는 부분입니다.
  • useMutation 을 값을 바꿀때 사용하는 api 입니다.
  • 앞서, src/api/posts.jsx API 에서 만든 createPost 함수를 mutationFn 에 넣어 주었습니다.

PostForm.jsx 컴포넌트 생성

  • 앞서 페이지에서 사용하는 공통 컴포넌트 중 하나인 PostForm.jsx 컴포넌트 코드입니다.
import { useState } from "react";

export default function PostForm({ onSubmit, initialValue }) {
    const [post, setPost] = useState({
        title: initialValue.title || "",
        body: initialValue.body || "",
    });

    const handleChangeInput = (e) => {
        setPost({
            ...post,
            [e.target.name]: e.target.value,
        });
    };

    const handleSubmit = (e) => {
        e.preventDefault();
        onSubmit(post);
        setPost({
            title: "",
            body: "",
        });
    };

    const renderField = (label) => (
        <div>
            <label>{label}</label>
            <input onChange={handleChangeInput} type="text" name={label.toLowerCase()} value={post[label.toLowerCase()]} />
        </div>
    );

    return (
        <form onSubmit={handleSubmit}>
            {renderField("Title")}
            {renderField("Body")}
            <button type="submit">Submit</button>
        </form>
    );
}

전체 프로젝트 구조

  • 이제 코드는 모두 작성 완료 하였습니다.
  • 완성된 프로젝트의 폴더 구조는 아래와 같습니다.


실행 결과

728x90

'웹 프로그래밍 > React' 카테고리의 다른 글

[react] Button 클릭 시, 데이터 추가하기  (0) 2023.04.20
[React] react-query 정리  (0) 2023.04.03
[React] React Query 란?  (0) 2023.03.30
[react] 리액트 버튼 아이콘 사용하기  (0) 2023.01.13
[React] JSX 란?  (0) 2022.12.11

이 글을 공유하기

댓글

Designed by JB FACTORY