본문 바로가기
개발 기록/front - react

React Query로 서버 상태 관리하기 : 데이터 패칭과 캐싱

by jeong11 2024. 12. 17.
반응형

※ 체크리스트

① React Query의 개념과 필요성

② React Query의 주요 기능(데이터 패칭, 캐싱, 뮤테이션) 익히기

③ React Query를 활용한 서버 상태 관리 방법의 실습 

 

 

1. React Query 

1-1. React Query의 정의 

React 애플리케이션에서 서버 상태를 관리하고 데이터 패칭과 캐싱을 간단하게 만들어주는 라이브러리 
☞ 서버 상태(server state) 관리에 특화된 라이브러리 

 

1-2. React Query 주요 특징 

① 데이터 동기화 : React Query가 클라이언트 상태와 서버 상태를 동기화하는 능력 

● React Query는 자동으로 데이터의 **신선도(freshness)**를 추적하고 필요에 따라 백그라운드에서 데이터를 다시 가져와 UI를 업데이트한다 

● 이를 통해 사용자가 보고 있는 데이터가 항상 최신 상태인지 확인할 수 있다 

● ex) 사용자가 페이지를 다시 방문하거나 창을 포커스할 때, React Query는 데이터가 오래되었는지 검사하고 최신 상태로 동기화한다 

 

② 자동 캐싱 : 데이터가 변경되지 않으면 캐시된 데이터를 사용하여 네트워크 요청 감소 

● React Query가 API 호출 결과를 메모리 내에서 관리하여, 동일한 요청이 있는 경우 캐시된 데이터를 제공하는 기능 

● 캐시된 데이터는 일정 시간 동안 유지되며, 캐시 지속 시간(stale time)이 지나면 데이터를 리패치하여 업데이트할 수 있다 → 네트워크 요청 횟수를 줄이고 애플리케이션 성능을 최적화할 수 있다 

● ex) useQuery 훅을 사용하면 데이터를 가져올 때 자동으로 캐시된다 

 

③ 리트라이(Retry)와 리패치(Refetch) :  실패한 요청을 자동으로 재시도하거나 데이터가 오래된 경우 자동으로 새로고침 

● 리트라이(Retry) : React Query는 API 요청이 실패할 경우 자동으로 요청을 다시 시도할 수 있는 기능을 재공 

기본적으로 실패 시 세번까지 재시도하며, 설정에 따라 조정할 수 있다 

● 리패치(Refetch) :  일정 조건이 충족되는 경우 React Query는 데이터를 다시 가져온다 

사용자가 페이지를 다시 불러오거나, 네트워크 상태가 변경되었을 때 등 데이터를 리패치할 수 있다 

● ☞ 리트라이와 리패치는 앱의 신뢰성과 유연성을 높여, 네트워크 오류가 있을 때도 안정적인 데이터 제공을 보장한다 

 

④ 개발자 도구 : 상태 디버깅을 위한 React Query Devtools 제공 

브라우저 확장 또는 React 애플리케이션 내 사용할 수 있는 도구 

애플리케이션의 쿼리 상태, 캐시 데이터, 리패치 시점 등을 실시간으로 모니터링하고 디버깅할 수 있는 인터페이스를 제공한다 

 

1-3. 구성 요소 

▶ Query : 데이터를 가져오는 작업

▶ Mutation : 데이터를 생성, 수정, 삭제하는 작업 

▶ Query Client : React Query의 중앙 관리 객체 

 

1-4. 동작 흐름 

1) 데이터 가져오기 (Query)

● useQuery 훅을 사용해 데이터를 가져온다 

● 자동으로 캐시되며 동일한 키를 가진 요청은 캐시를 재활용한다 

import { useQuery } from '@tanstack/react-query';

const fetchPosts = async () => {
	const res = await fetch('/api/posts');
    return res.json();
};

const Post \ () => {
	const { data, isLoading, error } = useQuery(['posts'], fetchPosts);
    
    if (isLoading) return <p>Loading...</p>;
    if (error) return <p>Error: {error.message}</p>;
    
    return (
    	<ul>
        	{data.map(post => (
            	<li key={post.id}>{post.title}</li>
            ))}
    	</ul>
    );
};

 

2) 데이터 갱신 (Mutation)

● useMutation 훅을 사용해 데이터를 업데이트하거나 삭제할 수 있습니다 

● 성공적으로 요청을 처리하면 자동으로 해당 데이터와 연관된 쿼리가 갱신됩니다 

import { useMutation, useQueryClient } from '@tanstack/react-query';

const deletePost = async (postId) => {
	await fetch('/api/posts/${postId}', { method: 'DELETE' });
};

const PostList = () => {
	const queryClient = useQueryClient();
	const mutation = useMutation(deletePost, {
    	onSuccess: () => {
        	queryClient.invalidateQueries(['posts']);	//'posts'쿼리 갱신
        },
    });
    
    const handleDelete = (id) => mutation.mutate(id);
    
    // ...
};

 

 

2. React Query 설치 및 기본 설정 

React Query 설치 

npm install @tanstack/react-query

 

Query Client 설정 

애플리케이션의 최상위 컴포넌트에 Query Client를 설정한다 

import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';

const queryClient = new QueryClient();

function Root () {
	return (
    	<QueryClientProvider client={queryClient}>
        	<App />
        </QueryClientProvider>
    );
}

export default Root;

 

 

3. React Query 주요 기능 및 구현 

3-1. 프로젝트 생성 

npx create-react-app react-query-demo

 

3-2. React Query 설치 

npm install @tanstack/react-query

 

3-3. React Query 개발자 도구 설치 (필수 아님)

npm install @tanstack/react-query-devtools

 

3-4. 구현 

1단계 : 데이터 패칭 

▶ 서버에서 데이터를 가져오는 기본 예제 

> Posts.js 생성 

JSONPlaceholder API를 사용해 posts 데이터를 패칭한다 

Post.js의 코드 내용

// src/Posts.js
import React from 'react';
import { useQuery } from '@tanstack/react-query';

function fetchPosts() {
	return fetch('https://jsonplaceholder.typicode.com/posts').then((res) => 
    res.json()
    );
}

function Posts() {
    // 기존
    // const { data, isLoading, isError } = useQuery(['posts'], fetchPosts);
    
    // Uncaught Error: Bad argument type. Starting with v5, only the "Object" form is allowed when calling query related functions. 
    // React Query v5에서는 모든 쿼리 관련 함수(useQuery, useMutation, useInfiniteQuery 등)가 객체 형태로 인자를 전달해야 합니다
    const { data, isLoading, isError } = useQuery({
    	queryKey: ['posts'], // 쿼리 키
        queryFn: fetchPosts, // 데이터 패칭 함수
        staleTime: 5000,	// 캐싱된 데이터가 신선한 상태로 유지되는 시간(ms)
    });
    
    if(isLoading) return <p>Loading...</p>;
    if(isError) return <p>Error fetching data.</p>;
    
    return (
    	<ul>
        	{data.map((post) => (
            	<li key={post.id}>{post.title}</li>
            ))}
        </ul>
    );
}

export default Posts;

① fetchPosts 함수는 JSONPlaceholder API에서 게시글 데이터를 가져온다 

② useQuery 훅을 통해 fetchPosts 호출을 관리하며 데이터(data), 로딩상태(isLoading), 오류 상태(isError)를 다룬다 

③ data는 fetchPosts 함수에서 반환된 결과값(게시글 배열)을 저장한다 

④ 화면에서는 data 배열의 각 항목에서 post.title 값을 <li>태그 안에 렌더링한다 

⑤ useQuery와 useMutation이 React Query v5부터 객체 형태로만 인자를 받도록 변경되었다  

 

 

2단계 : 데이터 뮤테이션(Mutation)

▶ 데이터를 생성, 수정, 삭제하는 작업 

> CreatePost.js 생성 

포스트 생성

import { useMutation } from '@tanstack/react-query';

function createPost(newPost) {
	return fetch('https://jsonplaceholder.typicode.com/posts', {
		method: 'POST',
		headers: {
        	'Content-Type': 'application/json', 
		}, 
        body: JSON.stringify(newPost),
    }).then((res) => res.json());
}

function CreatePost() {
	// const mutation = useMutation(createPost);
    
    const mutation = useMutation({
    	mutationFn: createPost, // 데이터 생성 함수
    });
    
	const handleCreate = () => {
		mutation.mutate({ title: 'New Post', body: 'This is a new post' });
	};
    
    return (
	<div>
		<button onClick={handleCreate}>Create Post</button>
		{mutation.isLoading && <p>Creating post...</p>}
		{mutation.isSuccess && <p>Post created!</p>}
	</div>
    );
}

export default CreatePost;

① createPost 함수는 JSONPlaceholder API에 POST 요청을 보내 새로운 게시글을 생성한다

② useMutation 훅을 사용해 createPost 함수 호출을 관리하며, 생성중(isLoading)과 성공 상태(isSuccess)를 확인할 수 있다 

③ 버튼 클릭 시 mutation.mutate가 호출되어 새로운 게시글이 만들어진다 

 

 

3단계 : 캐싱 및 리패치

▶캐싱된 데이터를 사용하는 경우 

> Post.js 파일의 staleTime을 추가하는 옵션을 넣어준다 

const { data } = useQuery(['posts'], fetchPosts, {
	staleTime: 5000, // 데이터가 5초 동안 신선한 상태로 유지 
});

staleTime : React Query가 데이터를 "신선한 상태(fresh)"로 간주하는 기간을 정의한다 

● staleTime:500은 데이터가 로드된 후 5초 동안은 백그라운드에서 리패치(refetching)를 하지 않는다는 의미 

● 5초가 지나면 데이터를 오래된 상태(stale)로 간주하고 필요 시 다시 서버에서 데이터를 가져온다  

 

☞ 페이지를 다시 방문하거나 사용자가 새로고침 하지 않아도 데이터를 효율적으로 관리할 수 있다 

 

3-5. React Query Devtool 사용 및 실행 

넣어줄 코드 

import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function Root() {
	return (
    	<QueryClientProvider client={queryClient}>
        	<App />
            <ReactQueryDevtools initiallsOpen={false} />
        </QueryClientProvider>
    );
}

 

> App.js 수정 

Devtools 추가

import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';	// Devtools 임포트
import Posts from './Post';
import CreatePost from './CreatePost';

const queryClient = new QueryClient();

function App() {
	return (
    	<QueryClientProvider client={queryClient}>
		<div>
			<h1>React Query 실습</h1>
			<Post />
			<CreatePost />
		</div>
        	<ReactQueryDevtools initialIsOpen={false} /> {/* Devtools 추가 */}
        </QueryClientProvider>
    );
}

export default App;

① QueryClient와 QueryClientProvider는 React Query의 전역 상태 관리 및 캐싱을 활성화한다 

② <Post />는 게시글 목록을 보여주고, <CreatePost />는 새 게시글 생성 버튼과 상태 메시지를 관리한다 

③ React Query DevTools를 포함해 쿼리를 시각적으로 디버깅할 수 있다 

 

3-6. 실행

npm start

 

실행화면, 오른쪽 아래엔 React Query Devtools
React Query Devtool 클릭 시 실행

 

4. 실습 과제 

① 서버에서 데이터를 패칭하여 화면에 표시하세요

② 데이터를 생성하거나 수정하는 버튼을 구현하세요 

③ React Query Devtools를 사용해 상태를 확인하세요 

 

상단에 게시글 목록 표시

하단에 게시글 제목과 본문을 입력 

☞ JSONPlaceholder의 API 사용해 데이터 생성/수정을 처리한다 

 

4-1. 설정

1) 프로젝트 생성 

npx create-react-app react-query-practice

 

2) React Query 설치 

npm install @tanstack/react-query

 

3) React Query 개발자 도구 설치 

npm install @tanstack/react-query-devtools

 

4) 프로젝트 구조 

src/

App.js

Posts.js

CreateUpdatePost.js

 

4-2. 구현

1) App.js

> App.js 수정

구현 목표 1과 2 표시

import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import Posts from './Posts';
import CreateUpdatePost from './CreateUpdatePost';

// React Query Client 생성
const queryClient = new QueryClient();

function App() {
	return (
    <QueryClientProvider client={queryClient}>
    	<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
			<h1>React Query 과제</h1>
			<h2>1. 서버에서 데이터를 패칭하여 화면에 표시</h2>
			<Posts />
            
			<h2>2. 데이터를 생성하거나 수정하는 버튼</h2>
			<CreateUpdatePost />
            
			<ReactQueryDevtools initialIsOpen={false} />
        </div>
    </QueryClientProvider>
	)
}

export default App;

③ ReactQueryDevtools를 추가해 React Query의 상태를 시각적으로 확인 가능 

 

2) Post.js

> Post.js 생성 

서버에서 데이터를 가져와 화면에 게시한다 

테이블로 화면에 데이터 보여주기

import React from 'react';
import { useQuery } from '@tanstack/react-query';

function fetchPosts() {
	return fetch('https://jsonplaceholder.typicode.com/posts').then((res) => 
		res.json()
	);
}

function Posts() {
	const { data, isLoading, isError } = useQuery({
    	queryKey: ['posts'],	// 쿼리 키
        queryFn: fetchPosts, 	// 서버 데이터 패칭 함수
	});
    
	if (isLoading) return <p>Loading posts...</p>;
	if (isError) return <p>Error fetching posts</p>;
    
	return (
		<table
		style={{
		width: '100%',
		borderCollapse: 'collapse',
		marginTop: '20px',
		textAlign: 'left',
		}}
		>
				<thead>
					<tr style={{ backgroundColor: '#f4f4f4' }}>
						<th style={{ border: '1px solid #ddd', padding: '8px' }}>ID</th>
						<th style={{ border: '1px solid #ddd', padding: '8px' }}>Title</th>
						<th style={{ border: '1px solid #ddd', padding: '8px'}}>Body</th>
					</tr>
				</thead>
                <tbody>
					{data.slice(0, 10).map((post) => ( 
					<tr key={post.id}>
						<td style={{ border: '1px solid #ddd', padding: '8px' }}>{post.id}</td>
						<td style={{ border: '1px solid #ddd', padding: '8px' }}>{post.title}</td>
						<td style={{ border: '1px solid #ddd', padding: '8px' }}>{post.body}</td>
					</tr>
					))}
                </tbody>
		</table>
	);
}

export default Posts;

① 서버에서 데이터를 패칭하여 화면에 표시 

● Post.js에서 useQuery 훅을 사용해 JSONPlaceholder API에서 데이터를 패칭

● 게시글 목록을 테이블 형식으로 렌더링 

 

3) CreateUpdatePost.js

> 생성 

데이터를 생성하는 버튼, 수정하는 버튼 구현 

생성버튼 수정버튼 한번에

import React, { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';

// 새 게시글 생성 함수
function createPost(newPost) {
	return fetch('https://jsonplaceholder.typicode.com/posts', {
		method: 'POST', 
		headers: { 'Content-Type': 'application/json' },
		body: JSON.stringify(newPost),
	}).then((res) => res.json());
}

// 게시글 수정 함수
function updatePost(updatedPost) {
	return fetch('https://jsonplaceholder.typicode.com/posts/${updatedPost.id}', {
		method: 'PUT',
		headers: { 'Content-Type': 'application/json' },
		body: JSON.stringify(updatedPost),
    }).then((res) => res.json());
}

function CreateUpdatePost() {
	const [postId, setPostId] = useState('');
	const [title, setTitle] = useState('');
	const [body, setBody] = useState('');
	const queryClient = useQueryClient();
    
	// 생성/수정 Mutation
    const createMutation = useMutation({
        mutationFn: createPost, 
        onSuccess: () => {
			queryClient.invalidateQueries(['posts']);	// 게시글 목록 새로고침
        },
    });
    
    const updateMutation = useMutation({
		mutationFn: updatePost,
        onSuccess: () => {
			queryClient.invalidateQueries(['posts']);
        },
    });
    
    const handleCreate = () => {
		createMutation.mutate({ title, body });
    };
    
    const handleUpdate = () => {
    	updateMutation.mutate({ id: Number(postId), title, body });
    };
    
	return(
		<div>
			<input 
			type="text"
			placeholder="Post ID (for update)"
			value={postId}
			onchange={(e) => setPostId(e.target.value)}
			style={{ marginRight: '10px' }}
			/>
            
			<input
			type="text"
			placeholder="Title"
			value={title}
			onChenge={(e) => setTitle(e.target.value)}
			style={{ marginRight: '10px' }}
			/>
            
			<input
			type="text"
			placeholder="Body"
			value={body}
			onChange={(e) => setBody(e.target.value)}
			style={{ marginRight: '10px' }}
			/>
            
			<button onClick={handleCreate} style={{ marginRight: '10px' }}>
			Create Post
			</button>
			<button onClick={handleUpdate}>Update Post</button>
        
			<div style={{ marginTop: '10px' }}>
				{createMutation.isLoading && <p>Creating post...</p>}
				{createMutation.isSuccess && <p>Post created successfully!</p>}
                
                {updateMutation.isLoading && <p>Updating post...</p>}
                {updateMutation.isSuccess && <p>Post updated successfully!</p>}
			</div>
        </div>
	);
}

export default CreateUpdatePost;

② 데이터를 생성하거나 수정하는 버튼 구현 

● 두 개의 Mutation을 사용 

createPost : 새 게시글을 생성 / updatePost : 기존 게시글을 수정

● 각각의 버튼으로 데이터 조작 가능 

● 성공 시 queryClient.invalidateQueries를 호출해 게시글 목록 갱신 

 

☞ 이 코드는 JSONPlaceholder의 샘플 API를 사용하므로, 데이터 생성/수정이 실제로 저장되지는 않지만 요청 응답은 성공적으로 처리된다 

 

4) 실행 

npm start

 

테이블로 서버에서 받아온 데이터 표시, 데이터 생성과 수정버튼

데이터 생성 버튼 실행해보기 → 글 작성

버튼을 누르면 Post created successfully! 등장

데이터 수정 버튼 실행해보기 

Post updated successfully! 문구


※ 정리

Q&A

① React Query와 Redux의 차이점은 무엇일까? 

→ 둘 다 상태관리를 위한 도구지만 사용목적과 상태 관리 방식에 차이가 있다 

→ React Query는 서버 상태 관리에 초점이 맞춰져 있어 API 기반 데이터 패칭 및 캐싱을 효율적으로 처리하는 경우에 적합하고,  Redux는 클라이언트에서 애플리케이션의 상태를 한 곳에서 중앙 집중적으로 관리할 때 강력한 도구로 복잡한 상태 관리가 필요한 경우 유리하다 

→ 사용 방식 : 

▶ React Query :

useQuery와 useMutation 훅을 통해 비동기 데이터 요청을 수행, 

기본적으로 API 응답 데이터를 캐싱해 필요시 재사용하거나 자동으로 최신 상태를 동기화함 

데이터의 staleTime, 폴링(Polling), Retry 같은 서버 데이터 관련 기능이 내장 

▶ Redux :

데이터의 상태를 전역적으로 관리하기 위해 **스토어(store)**와 액션(action) 및 **리듀서(reducer)**를 사용

상태변경은 명시적으로 dispatch를 통해 이루어짐 

상태 관리의 유연성과 확장성이 뛰어나지만 초기 설정과 보일러플레이트 코드가 많을 수 있다 

특징 React Query Redux
상태 유형 서버 상태(Server State) 전역 상태 (Global State)
주요 사용 사례 API 응답 데이터, 서버 동기화 데이터 사용자 인증 상태, UI 상태, 클라이언트 상태
데이터 캐싱 내장된 캐싱, 자동 재요청 지원 직접 구현 필요
비동기 작업 내장된 훅으로 쉽게 관리 가능(useQuerry, useMutation) Redux Thunk 또는 Saga로 관리 가능

 

② React Query는 어떤 상황에서 적합한 상태 관리 도구일까? 

→ 서버 데이터 관리가 주된 목적일 때 적합하다 

주요 사용 사례는 API 응답 데이터와 서버 동기화 데이터

▶API 응답 데이터 : 클라이언트 애플리케이션이 서버로부터 가져오는 모든 데이터

ex) REST API, GraphQL API로부터 JSON 데이터를 가져와 화면에 표시하거나 내부 로직에서 사용

▶서버 동기화 데이터 : 클라이언트에서 처리된 변경 사항이 서버에 반영되고 그 결과로 다시 가져온 데이터가 최신 상태로 유지되는 것을 의미한다 

 

 

 

반응형