The Traditional Way of Data Fetching
One of the common patterns that React developers use for data fetching involves cobbling together the component-level and side-effects via the useState() hook and the useEffect() hook. If you’re not familiar with this approach, here’s an example that elucidates it:
리액트 개발자가 데이터 가져오기에 사용하는 일반적인 패턴 중 하나가 useState와 useEffect를 통해 컴포넌트 레벨과 side-effects를 조금씩 결합하는 것이다.
다음과 같다.
import { useState, useEffect } from "react";
import "../../styles/Home.module.scss";
export default function Home() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
const fetchUsers = async () => {
setLoading(true);
try {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await res.json();
setData(users);
} catch (err) {
console.error(err);
if (err instanceof Error) {
setError(err.message);
}
}
setLoading(false);
};
fetchUsers();
}, []);
console.log("home");
return (
<div className="Home">
<h1>Data Fetching</h1>
<h2>The React Hooks Way</h2>
<div>
{loading ? (
<p>Loading...</p>
) : error ? (
<p>{error}</p>
) : (
data.map(({ id, name }) => <p key={id}>{name}</p>)
)}
</div>
</div>
);
}
TypeScript
복사
In the above example, we require 3 component-level states to keep track of the data, loading status, and the error message. On top of that, we call the asynchronous fetch() function inside the effect hook to make a network request to the API and manage the component-level state accordingly.
위의 예에서 우리는 3개의 컴포넌트 레벨 상태를 요구한다. 데이터를 추적하는 것을 유지하고, 로딩 상태를, 그리고 에러 메시지를 위해. 가장 앞에서 우리는 비동기 fetch 함수를 effect 훅안에서 호출한다. API 에 네트워크 요청을 보내고 그에 따라 컴포넌트 레벨 상태를 관리한다.
While there isn’t anything fundamentally wrong with this approach, this is a lot of business logic that clutters the component. Even with this bare minimum example, the lines of code and logic we had to write were significant. Not quite good in terms of reusability!
이 접근은 기본적으로 틀린 것은 없지만, 이것은 컴포넌트를 혼란스럽게 하는 비즈니스 로직이 매우 많다. 심지어 이 최소한의 예시에서, 우리가 써야만 하는 로직과 코드 라인은 상당했다. 재사용성 측면에서 꽤 좋지 않다!
The other popular approach usually seen in big applications is storing the data in the global state with state management tools like Context API, Redux, or other state management libraries. While this is great for avoiding issues like prop drilling and achieving data communication amongst isolated components, it still doesn’t address the problems that are exclusive to the server state.
보통 대규모 어플리케이션에서 보이는 다른 인기있는 접근법은 ContextAPI, Redux나 다른 상태 관리 라이브러리에 데이터를 전역 상태로 저장하는 것이다. 이것은 props drilling 같은 이슈를 피하고, 독립된 컴포넌트 사이의 데이터 통신을 할 수 있지만 서버 상태에 배제된다는 문제가 해결되지 않는다.
And this is where React Query shines. Let’s take a deep dive into it and figure out how we can use it to make data fetching painless.
여기서 리액트 쿼리가 빛을 발한다.
An Introduction to React Query
React Query is a data fetching library that can help you manage asynchronous operations between your server and client and efficiently fetch your data. But that’s not all. It brings a bunch of handy features to improve the user experience and make your React application faster and more responsive.
리액트 쿼리는 서버와 클라이언트 간의 비동기 작업을 관리하고 데이터를 효율적으로 가져올 수 있는 데이터 가져오기 라이브러리입니다. 하지만 그게 다가 아닙니다. 사용자 환경을 개선하고 리액트 응용 프로그램을 더 반응적이고 빠르게 응답할 수 있도록 여러 가지 편리한 기능을 제공합니다.
Before we discuss the features of React Query, let us first understand the term server state. Server state is the asynchronous data that is fetched from a server or an API. Unlike the client state, the server state has shared ownership and the potential to become stale as soon as you’re done fetching it.
리액트 쿼리의 기능에 대해 논의하기 전에 먼저 server state라는 용어에 대해 알아보겠습니다. 서버 상태는 서버 또는 API에서 가져오는 비동기 데이터입니다. 클라이언트 상태와 달리 서버 상태는 공유 소유권을 가지며 가져오기가 완료되는 즉시 오래된 상태가 될 수 있습니다.
Therefore, managing server state efficiently is a challenge in itself. Here are some of the major features that React Query offers to combat these challenges:
•
Caching the data for future use.
•
Deduping multiple requests for the same data into a single request.
•
Pagination and infinite scrolling.
•
Prefetching the data.
•
Knowing when the data gets outdated and updating it in the background.
And a lot more. Enough talk. Let’s check out how to integrate React Query in your React application.
따라서 서버 상태를 효율적으로 관리하는 것 자체가 어려운 과제입니다. 리액트 쿼리가 이러한 문제를 해결하기 위해 제공하는 몇 가지 주요 기능은 다음과 같습니다.
•
나중에 사용할 수 있도록 데이터를 캐싱합니다.
•
동일한 데이터에 대한 여러 요청을 단일 요청으로 이중화합니다.
•
페이지 이동 및 무한 스크롤
•
데이터를 미리 가져오는 중입니다.
•
데이터가 오래되면 백그라운드에서 업데이트하는 것입니다.
그리고 더 많은 것. 얘기 그만.
Adding React Query to Your React Project
To start with React Query, use the following command to install it as an NPM package:
npm install react-query
TypeScript
복사
# or, if you use yarn:
yarn add react-query
TypeScript
복사
Before you can start using the hooks from React Query, you will need to import QueryClient and QueryClientProvider from react-query and wrap it around the <App /> component in index.js. This will ensure that all the components in the React application will have access to the hooks and cache.
리액트 쿼리에서 후크를 사용하려면 먼저 리액트 쿼리에서 QueryClient 및 QueryClientProvider를 가져와 index.js의 <App /> 구성 요소를 래핑해야 합니다. 이렇게 하면 애플리케이션의 모든 컴포넌트가 후크 및 캐시에 액세스할 수 있습니다.
import { QueryClient, QueryClientProvider } from "react-query";
import ReactDOM from "react-dom";
import App from "./App";
const queryClient = new QueryClient();
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
document.getElementById("root")
);
TypeScript
복사
And voila, setting up React Query is that simple. Let’s start making actual network requests and fetch data from an API using React Query’s useQuery hook in the next section.
이봐, 리액트 쿼리 설정은 간단해 다음 섹션에서 React Query의 useQuery hook을 사용하여 API에서 데이터를 가져오고 실제 네트워크 요청을 시작하겠습니다.
The useQuery() Hook
For our data fetching needs, we’ll be using the useQuery hook from react-query to handle all the server state for us. This is going to be one of the most used hooks of this library. Let’s use the same example that we used in the traditional data fetching methods section but use React Query this time.
데이터 가져오기 요구를 위해 useQuery hook을 react-query에서 사용하여 모든 서버 상태를 처리할 것입니다. 이 라이브러리에서 가장 많이 사용되는 후크 중 하나가 될 것입니다. 기존 데이터 가져오기 방법 섹션에서 사용한 것과 동일한 예를 사용하지만 이번에는 react-query를 사용하겠습니다.
import { useQuery } from "react-query";
import "./styles.css";
export default function App() {
const fetchUsers = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await res.json();
return users;
};
const { data, isLoading, isError, error } = useQuery("users", fetchUsers);
return (
<div className="App">
<h1>Data Fetching</h1>
<h2>With React Query's useQuery Hook</h2>
{isLoading ? (
<p>Loading...</p>
) : isError ? (
<p>{error.message}</p>
) : (
data.map((user) => <p key={user.id}>{user.name}</p>)
)}
</div>
);
}
TypeScript
복사
The useQuery hook takes two required parameters: the query key (more on this in the upcoming section) and the fetcher function, which returns a promise. Apart from this, you can also pass in an optional object parameter to configure the cache time, stale time, and other properties.
In return, React Query provides an object full of useful information related to the request, such as the status, data, loading state, failure count, and a lot more. In the above example, I’ve destructured the data, isLoading, isError, and the error from the returned object to render the UI accordingly.
useQuery 훅은 두 개의 필수 매개 변수를 사용합니다: 쿼리 키(다음 섹션에서 자세히 설명)와 Promise을 반환하는 fetcher 함수. 이와 별도로 선택적 개체 매개 변수를 전달하여 cache time, stale time, and other properties을 구성할 수도 있습니다.
React query는 상태, 데이터, 로드 상태, 실패 횟수 등과 같은 요청과 관련된 유용한 정보로 가득 찬 객체를 제공합니다. 위의 예에서는 반환된 객체의 'data', 'isLoading', 'isError', 'Error'를 구조화하여 UI를 렌더링했습니다.
React Query Devtools
React Query comes with an in-built devtools that can be very handy for monitoring and understanding the lifecycles of a request. Here’s how you can import and use it in your React application:
React Query는 요청의 라이프사이클을 모니터링하고 이해하는 데 매우 유용할 수 있는 내장된 devtools와 함께 제공됩니다. 다음은 React 응용프로그램에서 가져와 사용할 수 있는 방법입니다.
import { QueryClient, QueryClientProvider } from "react-query";
// Importing the devtools from react-query/devtools
import { ReactQueryDevtools } from "react-query/devtools";
import ReactDOM from "react-dom";
import App from "./App";
const queryClient = new QueryClient();
ReactDOM.render(
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools />
</QueryClientProvider>,
document.getElementById("root")
);
TypeScript
복사
Debugging how React Query works is extremely easy as all the requests you make are logged to the devtools along with the complete information related to it. You can open it by clicking on the floating React Query logo on the bottom left side of your screen.
By default, the devtools is only enabled on the development environment and gets disabled on production, so you don’t have to worry about that. I would highly recommend you to use it for a better and visual understanding.
React Query의 작동 방식은 모든 요청이 devtools에 관련된 전체 정보와 함께 기록되기 때문에 디버깅이 매우 쉽습니다. 화면 왼쪽 하단에 떠 있는 React Query 로고를 클릭하여 열 수 있습니다.
기본적으로 devtools는 개발 환경에서만 활성화되고 운영 시 비활성화되므로 걱정하지 않아도 됩니다. 더 나은 시각적 이해를 위해 사용하기를 강력히 추천합니다.
Query Keys And Its Importance
To make the most out of React Query, it is essential that you understand the query keys and the role it plays in the useQuery() hook. Query keys are unique keys used to identify each request. Let’s understand this practically with a simple example.
Respect Query를 최대한 활용하려면 쿼리 키와 useQuery() 후크에서 쿼리 키가 수행하는 역할을 이해하는 것이 중요합니다. 쿼리 키는 각 요청을 식별하는 데 사용되는 고유 키입니다.
간단한 예시로 이것을 실질적으로 이해합니다.
import { useQuery } from "react-query";
import "./styles.css";
export default function App() {
return (
<div className="App">
<h1>Query Keys</h1>
<h2>Example 1</h2>
<h3>Users</h3>
<Users />
<h3>Posts</h3>
<Posts />
</div>
);
}
function Users() {
const { data, isLoading, isError, error } = useQuery("users", () =>
fetch("https://jsonplaceholder.typicode.com/users").then((res) =>
res.json()
)
);
if (isLoading) return <p>Loading...</p>;
if (isError) return <p>{error.message}</p>;
return (
<div>
{data.map((user) => (
<p key={user.id}>{user.name}</p>
))}
</div>
);
}
function Posts() {
const { data, isLoading, isError, error } = useQuery("posts", () =>
fetch("https://jsonplaceholder.typicode.com/posts?_limit=5").then((res) =>
res.json()
)
);
if (isLoading) return <p>Loading...</p>;
if (isError) return <p>{error.message}</p>;
return (
<div>
{data.map((post) => (
<p key={post.id}>{post.title}</p>
))}
</div>
);
}
TypeScript
복사
In the above example, we have 2 components: <Users /> and <Posts /> to display the users and posts, respectively. However, notice that the query key in the useQuery() hook in both the components are different. If you keep the same query key, both the requests will be considered the same even though we are requesting different resources.
위의 예에서는 <Users />와 <Posts />의 2개의 컴포넌트를 사용하여 각각 Users와 Posts를 표시합니다. 단, 양쪽 컴포넌트의 useQuery() 후크에 있는 쿼리 키가 다르다는 점에 주의해 주십시오. 동일한 쿼리 키를 유지하면 서로 다른 리소스를 요청하더라도 두 요청은 동일한 것으로 간주됩니다.
Let us take another example where the query key could be dynamic, such as a prop:
쿼리 키가 동적일 수 있는 또 다른 예를 들어 다음과 같습니다. 예를 들어 보겠습니다.
import { useState } from "react";
import { useQuery } from "react-query";
import "./styles.css";
export default function App() {
return (
<div className="App">
<h1>Query Keys</h1>
<h2>Example 2</h2>
<Users />
</div>
);
}
function Users() {
const [id, setId] = useState(null);
const { data, isLoading, isError, error } = useQuery("users", () =>
fetch("https://jsonplaceholder.typicode.com/users").then((res) =>
res.json()
)
);
if (isLoading) return <p>Loading...</p>;
if (isError) return <p>{error.message}</p>;
return (
<div>
{data.map((user) => (
<p key={user.id} onClick={() => setId(user.id)}>
{user.name}
</p>
))}
<br />
{id && <UserInfo id={id} />}
</div>
);
}
function UserInfo({ id }) {
const { data, isLoading, isError, error } = useQuery(["users", id], () =>
fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then((res) =>
res.json()
)
);
if (isLoading) return <p>Loading...</p>;
if (isError) return <p>{error.message}</p>;
return (
<div>
<h4>{data.name}</h4>
<h4>{data.email}</h4>
<h4>{data.phone}</h4>
<h4>{data.website}</h4>
</div>
);
}
TypeScript
복사
This example will help you truly understand the purpose of query keys and how it is used to cache the data. The <Users /> component renders a list of all the users and uses the query key “users” for the useQuery() hook. On clicking any of the users from the list, the <UserInfo /> component is rendered with the user’s ID being sent as a prop.
이 예에서는 쿼리 키의 목적과 데이터를 캐시하는 데 사용되는 방법을 이해하는 데 도움이 됩니다. <Users /> 컴포넌트는 모든 사용자의 목록을 렌더링하고 쿼리 키 "users""를 useQuery() 후크에 사용합니다. 목록에서 사용자를 클릭하면 "<UserInfo/>" 컴포넌트가 렌더링되고 사용자의 ID가 props으로 전송됩니다.
This <UserInfo /> component uses a dynamic query key of [“users”, id] to retrieve the user’s complete information. Since the id is unique for each user, React Query can use it to cache the data and re-fetch it whenever needed. This is a bit similar to the dependency array in the useEffect() hook.
이 <UserInfo /> 컴포넌트는 동적 쿼리 키 ["users", id] 를 사용하여 사용자의 완전한 정보를 가져옵니다. id는 사용자마다 고유하므로 React Query는 데이터를 캐시하고 필요할 때마다 데이터를 다시 가져올 수 있습니다. 이것은 'useEffect()' 후크의 의존관계 배열과 약간 유사합니다.
The first time you click on a user from the list, React Query will fetch the details of that user from the API and store it in the cache. Even if you click on some other user and click back on this user, React Query will render the cached data of the user and automatically re-fetch the data in the background.
목록에서 사용자를 처음 클릭하면 React Query가 API에서 해당 사용자의 세부 정보를 가져와 캐시에 저장합니다. 다른 사용자를 클릭하고 이 사용자를 다시 클릭해도 React Query는 캐시된 사용자의 데이터를 렌더링하고 백그라운드에서 데이터를 자동으로 다시 가져옵니다.
Therefore, the content is rendered instantaneously without any loading state. On re-fetching, if any content has been changed, the UI will be re-rendered automatically. This makes your website look performant and provides a better user experience.
따라서 콘텐츠는 로드 상태 없이 즉시 렌더링됩니다. 재취득 시 변경된 콘텐츠가 있는 경우 UI가 자동으로 재렌더됩니다. 이렇게 하면 웹 사이트가 성능이 향상되고 사용자 환경이 향상됩니다.
The useMutation() Hook
Till now, we have considered scenarios where we’re fetching the data from the server or API when the component is mounted. However, this may not always be the case. For example, we may need to make network request to create, update or delete resources on the server. And this is where the useMutation() hook from react-query comes into play.
지금까지는 컴포넌트를 마운트할 때 서버나 API에서 데이터를 가져오는 시나리오를 검토했습니다. 하지만 항상 그렇지는 않을 수 있습니다. 예를 들어 서버상의 자원을 작성, 갱신 또는 삭제하기 위해 네트워크 요구를 해야 하는 경우가 있습니다. 여기서 react-query의 useMutation() 훅이 작동합니다.
As the name suggests, this hook allows you to make requests to your server or API to mutate the state instead of just fetching them. Here’s an example that demonstrates the useMutation() hook being used to add a new todo:
이름에서 알 수 있듯이 이 훅을 사용하면 서버나 API에 요청하여 상태를 변환하는 대신 가져올 수 있습니다. 다음은 useMutation() 훅을 사용하여 새로운 작업을 추가하는 예를 보여드리겠습니다.
// newTodoPage.tsx
import { useState } from "react";
import { useMutation } from "react-query";
export default function newTodoPage() {
const [todo, setTodo] = useState("");
const mutation = useMutation(
() =>
fetch("https://jsonplaceholder.typicode.com/todos", {
method: "POST",
headers: {
"Content-type": "application/json; charset=UTF-8",
},
body: JSON.stringify({
userId: 1,
title: todo,
completed: false,
}),
}).then((res) => res.json()),
{
onSuccess(data) {
console.log("Successful", { data });
},
onError(error) {
console.log("Failed", { error });
},
onSettled() {
console.log("Mutation completed.");
},
}
);
async function addTodo(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
mutation.mutateAsync();
}
return (
<div className="newTodoPage">
<h1>useMutations() Hook</h1>
<h2>Create, update or delete data</h2>
<h3>Add a new todo</h3>
<form onSubmit={addTodo}>
<input
type="text"
value={todo}
onChange={(e) => setTodo(e.target.value)}
/>
<button>Add todo</button>
</form>
{mutation.isLoading && <p>Making request...</p>}
{mutation.isSuccess && <p>Todo added!</p>}
{mutation.isError && <p>There was an error!</p>}
</div>
);
}
TypeScript
복사
In the above example, the useMutation() hook takes a function that makes a POST request to the API endpoint to add a new todo. We can also add some side-effects such as onSuccess, onError, and onSettled to keep track of the mutation’s status.
위의 예에서 useMutation() 훅은 API 엔드포인트에 POST 요청하는 함수를 사용하여 새로운 ToDo를 추가합니다. 또한 변환(mutation) 상태를 추적하기 위해 onSuccess, onError, onSettled 등의 side-effects도 추가할 수 있습니다.
To run the mutation, call mutateAsync() or mutate() on the mutation object returned by the useMutation() hook. You can similarly make requests for updating and deleting the data on the server.
mutation을 실행하려면 useMutation() 훅에 의해 반환된 mutation 객체에서 mutateAsync() 또는 mutate()를 호출합니다. 마찬가지로 서버의 데이터 업데이트 및 삭제를 요청할 수 있습니다.
onSubmit 속성에 프로미스를 반환하는 함수를 넣어준 것에 대해 아래 블로그 참고
Axios를 이용한 요청 보내기(+ EventListener) - https://tothefullest08.github.io/javascript/2019/06/28/JS04_event_listener_axios/
Other Features to Check Out
But that’s not all. React Query has a lot more features that you can explore and use in your next projects, such as infinite scrolling, pagination, and pre-fetching. Be sure to check out the documentation. I would also highly recommend you to watch some talks from Tanner Linsley, the creator of React Query, to understand how it differs from other data fetching libraries such as SWR. You can also check out Applozic’s React Native Tutorial here.
하지만 그게 다가 아닙니다. Respect Query에는 무한 스크롤, 페이지 번호 매기기, 프리페치 등 다음 프로젝트에서 탐색하고 사용할 수 있는 기능이 많이 있습니다. 반드시 설명서를 확인해 주십시오. 또한 React Query의 제작자인 Tanner Linsley가 SWR과 같은 다른 데이터 가져오기 라이브러리와 어떻게 다른지 알아보는 것도 추천합니다. Applozic’s React Native Tutorial here.
That is all for this blog post. I hope you understood React Query and would consider using it in your future projects.
•
Status states for requests.
•
Dependent and parallel requests.
•
Caching and refetching.
•
Optimistic Updates.
•
Dedicated Dev tools.