
React로 개발할 때, 데이터를 서버에서 가져와 화면에 보여주는 작업은 매우 중요합니다. 예를 들어, 웹 사이트에서 검색을 하거나, 게시글 목록을 불러오거나 할 때 모두 서버와 데이터를 주고받게 됩니다.
이러한 데이터를 서버 상태라고 합니다. 그런데 이 서버 상태를 효율적으로 관리하는 것이 쉽지 않기 때문에 Tanstack Query(구 React Query)라는 라이브러리가 등장하게 되었습니다.
React에는 기본적으로 useState와 useEffect라는 기능을 이용해 데이터를 가져오고 상태를 저장할 수 있습니다. 하지만 이는 다음과 같은 문제점이 존재합니다.
같은 데이터를 여러 번 요청하면 불필요한 네트워크 요청이 많아져 성능이 저하됩니다. 예를 들어, 사용자가 "뒤로 가기" 버튼을 눌렀을 때 동일한 데이터를 다시 불러와야 하는 상황이 발생할 수 있습니다.
서버에서 데이터를 가져오는 동안 "로딩 중..." 표시를 해야 하고, 만약 에러가 발생하면 사용자에게 적절히 알려줘야합니다. 이러한 로딩 및 에러 처리를 수동으로 구현해야 하므로 코드가 복잡해지고 유지보수가 어려워집니다.
React의 기본 상태 관리만으로는 가져온 데이터를 캐싱하지 않기 때문에, 검색을 여러 번 반복할 경우 동일한 요청이 매번 서버로 전송됩니다. 이로 인해 불필요한 네트워크 트래픽이 발생하고 성능이 저하됩니다.
사용자가 페이지를 새로고침하거나, 브라우저의 다른 탭을 열었다가 다시 돌아왔을 때, 기존 데이터를 유지하고 최신 데이터를 자동으로 불러오는 기능이 부족합니다.
이를 직접 구현하려면 많은 추가적인 코드가 필요로 합니다.
Tanstack Query는 서버에서 데이터를 가져올 때 발생하는 여러 문제를 자동으로 해결해 줍니다.
다음과 같은 핵심 기능을 통해 보다 효율적인 데이터 관리를 지원합니다.
한 번 가져온 데이터를 내부적으로 저장(캐싱)하여 동일한 요청이 들어오면 서버가 아닌 캐시에서 데이터를 가져옵니다.
이를 통해, 불필요한 네트워크 요청을 방지하고 페이지 이동 후에도 데이터를 빠르게 보여 줄 수 있습니다.
데이터를 가져올 때 Tanstack Query가 자동으로 로딩 상태, 성공, 실패 상태를 관리합니다. 이를 통해, 개발자는 상태 관리를 하지 않고도 데이터 상태를 쉽게 확인할 수 있습니다.
Tanstack Query는 페이지 포커스 또는 일정 시간 이후 자동으로 데이터를 새로고침(fetch)하여 항상 최신 상태를 유지합니다.
이 기능을 통해 사용자는 새로고ㄹ침 버튼을 누르지 않아도 최신 데이터를 확인할 수 있습니다.
서버에서 데이터를 불러오는 작업과 상태 관리를 하나의 훅(useQuery)를 통해 간단하게 처리할 수 있습니다.
React의 useState, useEffect를 사용할 때보다 코드가 훨씬 직관적이고 간결해집니다.
이 프로젝트는 Tanstack Query를 활용하여 서버 데이터를 효율적으로 가져오고 관리하는 방법을 학습하기 위해 진행되었습니다.
jsonplaceholder의 데이터를 활용하여 검색, 페이지네이션, 상세 페이지 유지 기능을 구현하는 것이 주요 목표입니다.
📂 searchbar/
├── 📂 src/ # 애플리케이션의 소스 코드
│ ├── 📂 Api/ # API 호출 관련 파일 (데이터 요청)
│ │ └── placeApi.js # JSONPlaceholder API 호출 함수
│ │
│ ├── 📂 Components/ # 재사용 가능한 UI 컴포넌트 모음
│ │ ├── Pagination.jsx # 페이지네이션 컴포넌트
│ │ └── SearchBar.jsx # 검색 바 컴포넌트
│ │
│ ├── 📂 Hooks/ # 커스텀 훅 (데이터 페칭 및 상태 관리)
│ │ └── usePlaces.js # TanStack Query 활용 훅
│ │
│ ├── 📂 Pages/ # 주요 페이지 컴포넌트 (라우팅 포함)
│ │ ├── Home.jsx # 홈 페이지 (리스트 및 검색)
│ │ └── Detail.jsx # 상세 페이지
│ │
│ ├── App.jsx # 최상위 루트 컴포넌트
│ ├── main.jsx # React 앱 진입점 (렌더링)
│ ├── App.css # 전역 스타일링
│ └── index.css # 기본 스타일
│
├── 📂 public/ # 정적 리소스 (이미지, 아이콘 등)
│ └── react.svg # React 로고 이미지
│
├── .gitignore # Git 저장소에서 제외할 파일 목록
├── eslint.config.js # ESLint 설정 파일 (코드 품질 유지)
├── index.html # 애플리케이션의 기본 HTML 파일
├── package.json # 프로젝트 설정 및 의존성 목록
├── package-lock.json # 의존성 버전 고정 파일
├── README.md # 프로젝트 설명 문서
└── vite.config.js # Vite 빌드 도구 설정
import { createRoot } from "react-dom/client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
const queryClient = new QueryClient();
createRoot(document.getElementById("root")).render(
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
);
웹 애플리케이션이 실행되기 위해서는 특정한 시작점이 필요합니다.
React에서 이 역할을 main.jsx 파일이 담당하며 이 파일은 웹 페이지에서 React 애플리케이션을 실행하고, 필요한 기능(데이터 관리, 페이지 이동 등)을 설정하는 중요한 역할을 합니다.
1. React 앱을 웹 페이지에 렌더링
2. Tanstack Query 설정을 통해 전역 데이터 관리 제공
3. React Router를 설정하여 페이지 간 이동을 가능하게 함
4. 앱의 주요 기능에 포함된 App.jsx를 실행하도록 설정
import { Routes, Route } from "react-router-dom";
import Home from "./Pages/Home";
import Detail from "./Pages/Detail";
const App = () => {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/detail/:id" element={<Detail />} />
</Routes>
);
};
export default App;
1. 라우팅 설정
- Routes로 전체 경로를 감싸고 Route를 이용해 페이지를 지정
- / -> Home 페이지, /detail/:id -> Detail 페이지 이동
2. React Router 사용 이유
- 싱글 페이지 애플리케이션(SPA)에서 페이지 이동 기능을 구현하기 위해
- URL 변경하면서도 전체 페이지 새로고침 없이 화면을 업데이트
3. URL 파라미터
- 특정 항목의 정보를 동적으로 전달하고 상세 페이지에서 활용
import axios from "axios";
export const fetchPlaces = async (query, page) => {
try {
console.log(`검색어: ${query}, 페이지: ${page}로 할 일 목록을 불러오는 중...`);
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos",
{
params: {
_page: page, // 페이지네이션을 위한 현재 페이지 번호
_limit: 10, // 한 페이지당 최대 10개의 항목을 가져오기
},
}
);
console.log("가져온 데이터:", response.data);
return {
results: response.data, // 가져온 데이터를 results로 반환
totalPages: Math.ceil(200 / 10), // 총 200개 항목 기준, 10개씩 페이지 나누기 (20페이지)
};
} catch (error) {
console.error("API 요청 중 오류 발생:", error.message);
throw new Error("데이터를 불러오는 데 문제가 발생했습니다.");
}
};
React 애플리케이션에서 서버에서 데이터를 가져오는 역할을 담당
fetchPlaces라는 함수를 통해 JSONPlaceholder의 todos 데이터를 가져오며, 페이지네이션을 처리!
export const fetchPlaces = async (query, page) => { ... }
비동기 함수로 데이터를 요청하고 응답을 받을 때까지 기다림
const response = await axios.get(
"https://jsonplaceholder.typicode.com/todos",
{
params: {
_page: page, // 요청할 페이지 번호
_limit: 10, // 페이지당 최대 항목 수
},
}
);
https://jsonplaceholder.typicode.com/todos?_page=2&_limit=10import { useQuery } from "@tanstack/react-query";
import { fetchPlaces } from "../Api/placeApi";
export const usePlaces = (query, page) => {
return useQuery({
queryKey: ["todos", query, page],
queryFn: () => fetchPlaces(query, page),
keepPreviousData: true, // 페이지네이션 시 이전 데이터 유지
});
};
React 애플리케이션에서 서버 데이터를 효율적으로 가져오고 캐싱하는 역할
이 파일에서는 Tanstack Query의 useQuery 훅을 사용하여 데이터를 요청하고 상태를 관리
1. 사용자가 검색어나 페이지 번호를 입력하면 usePlaces(query, page)가 호출
2. useQuery는 queryKey(["todos", query, page])를 확인하여 캐시에 해당 데이터가 있으면 캐시된 데이터를 반환하고, 없으면 fetchPlaces를 호출하여 새로운 데이터를 가져옴
3. 데이터가 로딩 중이면 로딩 상태를 자동으로 관리
4. 데이터 성공적으로 받아오면 화면에 표시
5. 페이지 이동할 때 이전 데이터를 유지하면서 새로운 데이터를 가져옴
import { useSearchParams, Link } from "react-router-dom";
import { usePlaces } from "../Hooks/usePlaces";
import Pagination from "../Components/Pagination";
import SearchBar from "../Components/SearchBar";
const Home = () => {
const [searchParams] = useSearchParams();
const query = searchParams.get("query") || "";
const page = parseInt(searchParams.get("page")) || 1;
const { data, isLoading, error } = usePlaces(query, page);
if (isLoading) return <p>로딩 중...</p>;
if (error) return <p>오류 발생: {error.message}</p>;
return (
<div>
<SearchBar />
<ul>
{data?.results.map((todo) => (
<li key={todo.id}>
<Link to={`/detail/${todo.id}`}>{todo.title}</Link>
</li>
))}
</ul>
<Pagination total={data?.totalPages || 1} />
</div>
);
};
export default Home;
이 파일은 애플리케이션의 메인 페이지로, 사용자가 할 일 목록을 검색하고 페이지네이션을 통해 데이터를 탐색할 수 있도록 구현된 컴포넌트!
1. useSearchParams로 URL에서 query(검색어)와 page(페이지) 값 가져오기
2. usePlaces 훅을 통해 검색어 및 페이지 번호에 따른 데이터 불러오기
3. 로딩 및 에러 상태 처리
4. 할 일 목록을 화면에 렌더링하고, 클릭 시 상세 페이지 이동
5. Pagination 컴포넌트를 사용하여 페이지 이동 처리
import { useSearchParams } from "react-router-dom";
const SearchBar = () => {
const [searchParams, setSearchParams] = useSearchParams();
const handleSearch = (event) => {
setSearchParams({ query: event.target.value, page: 1 });
};
return (
<div>
<input
type="text"
value={searchParams.get("query") || ""}
onChange={handleSearch}
/>
<button
onClick={() =>
setSearchParams({ query: searchParams.get("query") || "", page: 1 })
}
>
검색
</button>
</div>
);
};
export default SearchBar;
사용자가 검색어를 입력하면 URL 쿼리 파라미터(query, page)를 업데이트 하는 역할
React의 useSearchParams 훅을 사용하여 상태를 URL을 통해 관리하고, 사용자가 검색 버튼을 클릭하면 페이지를 1로 설정하여 초기화
1. 사용자가 검색어를 입력 → setSearchParams를 통해 URL 업데이트 (?query=검색어&page=1)
2. URL이 변경됨 → Home.jsx에서 URL을 기반으로 API 요청 실행
3. API에서 검색어에 해당하는 데이터를 가져옴
4. 결과가 화면에 표시
import { useNavigate, useSearchParams } from "react-router-dom";
import PropTypes from "prop-types";
const Pagination = ({ total }) => {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const currentPage = parseInt(searchParams.get("page")) || 1;
const maxVisiblePages = 5; // 한 번에 표시할 페이지 수
const currentGroup = Math.ceil(currentPage / maxVisiblePages);
const startPage = (currentGroup - 1) * maxVisiblePages + 1;
const endPage = Math.min(startPage + maxVisiblePages - 1, total);
// 페이지 변경 핸들러
const handlePageChange = (newPage) => {
searchParams.set("page", newPage);
navigate(`/?${searchParams.toString()}`);
};
return (
<div style={{ display: "flex", gap: "10px", alignItems: "center" }}>
<button
onClick={() => handlePageChange(startPage - 1)}
disabled={startPage === 1}
>
<
</button>
{Array.from(
{ length: endPage - startPage + 1 },
(_, i) => startPage + i
).map((page) => (
<button
key={page}
onClick={() => handlePageChange(page)}
disabled={page === currentPage}
style={{
fontWeight: page === currentPage ? "bold" : "normal",
cursor: page === currentPage ? "default" : "pointer",
}}
>
{page}
</button>
))}
<button
onClick={() => handlePageChange(endPage + 1)}
disabled={endPage >= total}
>
>
</button>
</div>
);
};
// PropTypes를 사용한 prop 검증 추가
Pagination.propTypes = {
total: PropTypes.number.isRequired, // total prop을 필수 숫자로 설정
};
export default Pagination;
이 컴포넌트는 애플리케이션의 페이지네이션 기능을 제공하며, URL page 쿼리 파라미터를 업데이트하여 특정 페이지로 이동하도록 구현
1. URL에서 현재 page 값을 가져와 현재 페이지와 그룹을 계산
2. 시작 및 끝 페이지 범위를 설정
3. 사용자가 페이지 버튼 클릭 → URL 변경 → 새 페이지 데이터 로드
4. 이전(<) / 다음(>) 버튼 클릭 시 페이지 그룹 이동
import { useParams, useNavigate } from "react-router-dom";
const Detail = () => {
const { id } = useParams();
const navigate = useNavigate();
return (
<div>
<h1>상세 페이지: {id}</h1>
<button onClick={() => navigate(-1)}>뒤로 가기</button>
</div>
);
};
export default Detail;
이 컴포넌트는 할 일 목록의 개별 항목 상세 정보를 보여주는 페이지이며, React Router의 useParams를 활용하여 URL에서 동적으로 데이터를 받아와 상세 정보를 표시하며 useNavigate를 사용하여 이전 페이지로 돌아가는 기능을 제공

이번 프로젝트를 통해 Tanstack Query를 활용한 서버 상태 관리를 보다 효과적으로 구현하는 방법을 배울 수 있었습니다.
1) 효율적인 서버 상태 관리
2) URL 기반 상태 관리
3) 사용자 경험 향상
이 프로젝트에서는 JSONPlaceholder API를 이용해 할 일 목록을 가져오고, 이를 검색 및 페이지네이션 기능과 결합하여 다음과 같은 성과를 얻었습니다.
1) 데이터 페칭 최적화 → TanStack Query의 캐싱 기능 활용
2) 페이지 간 이동 시 상태 유지 → React Router를 통한 상태 관리
3) 간결한 코드 유지 → useQuery 훅을 사용하여 데이터 로딩 및 에러 핸들링 단순화
4) 재사용 가능한 컴포넌트 구축 → SearchBar, Pagination을 통한 UI 재사용
1) 검색 기능 강화
2) 실제 데이터 활용
[출처]https://jjang-j.tistory.com/20
[출처]https://velog.io/@jihyeonjeong11/React-Why-React-Query