[React] Tanstack Query를 이용한 검색 및 페이지네이션 최적화

gyo_zaa·2025년 1월 19일

React

목록 보기
4/4
post-thumbnail

📍 Tanstack Query의 등장 배경

React로 개발할 때, 데이터를 서버에서 가져와 화면에 보여주는 작업은 매우 중요합니다. 예를 들어, 웹 사이트에서 검색을 하거나, 게시글 목록을 불러오거나 할 때 모두 서버와 데이터를 주고받게 됩니다.
이러한 데이터를 서버 상태라고 합니다. 그런데 이 서버 상태를 효율적으로 관리하는 것이 쉽지 않기 때문에 Tanstack Query(구 React Query)라는 라이브러리가 등장하게 되었습니다.


📍 Tanstack Query가 왜 필요할까? (기존 방식의 문제점)

React에는 기본적으로 useStateuseEffect라는 기능을 이용해 데이터를 가져오고 상태를 저장할 수 있습니다. 하지만 이는 다음과 같은 문제점이 존재합니다.

1) 중복 요청 문제

같은 데이터를 여러 번 요청하면 불필요한 네트워크 요청이 많아져 성능이 저하됩니다. 예를 들어, 사용자가 "뒤로 가기" 버튼을 눌렀을 때 동일한 데이터를 다시 불러와야 하는 상황이 발생할 수 있습니다.

2) 로딩 및 에러 관리의 어려움

서버에서 데이터를 가져오는 동안 "로딩 중..." 표시를 해야 하고, 만약 에러가 발생하면 사용자에게 적절히 알려줘야합니다. 이러한 로딩 및 에러 처리를 수동으로 구현해야 하므로 코드가 복잡해지고 유지보수가 어려워집니다.

3) 데이터 캐싱 없음

React의 기본 상태 관리만으로는 가져온 데이터를 캐싱하지 않기 때문에, 검색을 여러 번 반복할 경우 동일한 요청이 매번 서버로 전송됩니다. 이로 인해 불필요한 네트워크 트래픽이 발생하고 성능이 저하됩니다.

4) 자동 새로고침 부족

사용자가 페이지를 새로고침하거나, 브라우저의 다른 탭을 열었다가 다시 돌아왔을 때, 기존 데이터를 유지하고 최신 데이터를 자동으로 불러오는 기능이 부족합니다.
이를 직접 구현하려면 많은 추가적인 코드가 필요로 합니다.


📍 Tanstack Query가 어떻게 문제를 해결할까??? (해결책)

Tanstack Query는 서버에서 데이터를 가져올 때 발생하는 여러 문제를 자동으로 해결해 줍니다.
다음과 같은 핵심 기능을 통해 보다 효율적인 데이터 관리를 지원합니다.

1) 자동 데이터 캐싱 (중복 요청 방지)

한 번 가져온 데이터를 내부적으로 저장(캐싱)하여 동일한 요청이 들어오면 서버가 아닌 캐시에서 데이터를 가져옵니다.
이를 통해, 불필요한 네트워크 요청을 방지하고 페이지 이동 후에도 데이터를 빠르게 보여 줄 수 있습니다.

2) 자동 로딩 및 에러 핸들링

데이터를 가져올 때 Tanstack Query가 자동으로 로딩 상태, 성공, 실패 상태를 관리합니다. 이를 통해, 개발자는 상태 관리를 하지 않고도 데이터 상태를 쉽게 확인할 수 있습니다.

3) 자동으로 최신 데이터 유지

Tanstack Query는 페이지 포커스 또는 일정 시간 이후 자동으로 데이터를 새로고침(fetch)하여 항상 최신 상태를 유지합니다.
이 기능을 통해 사용자는 새로고ㄹ침 버튼을 누르지 않아도 최신 데이터를 확인할 수 있습니다.

4) 간편한 상태 관리

서버에서 데이터를 불러오는 작업과 상태 관리를 하나의 훅(useQuery)를 통해 간단하게 처리할 수 있습니다.
React의 useState, useEffect를 사용할 때보다 코드가 훨씬 직관적이고 간결해집니다.


📍 프로젝트 개요

1. 프로젝트 목표

이 프로젝트는 Tanstack Query를 활용하여 서버 데이터를 효율적으로 가져오고 관리하는 방법을 학습하기 위해 진행되었습니다.
jsonplaceholder의 데이터를 활용하여 검색, 페이지네이션, 상세 페이지 유지 기능을 구현하는 것이 주요 목표입니다.

2. 주요 기능 소개

1) 데이터 페칭 및 캐싱

  • JsonPlaceholder API의 todos 데이터를 가져와 화면에 목록으로 출력
  • 불필요한 중복 요청을 방지하기 위해 Tanstack Query의 캐싱 기능을 활용

2) 검색 기능

  • 사용자가 입력한 검색어를 기반으로 리스트 필터링
  • URL 파라미터를 이용해 검색 상태 유지

3) 페이지네이션

  • 데이터를 일정 개수씩 나누어 페이지별로 보여주기
  • 페이지 이동 시 기존 데이터를 다시 불러오지 않고 유지하도록 구현

4) 상세 페이지 및 뒤로 가기 유지 기능

  • 리스트의 항목을 클릭하면 상세 페이지로 이동
  • 뒤로 가기를 눌렀을 때 기존의 검색 및 페이지네이션 상태 유지

3. 사용 기술 스택

  • React(Vite) : 빠른 빌드 및 개발 환경 제공
  • Tanstack Query : 데이터 페칭 및 상태 관리를 효율적으로 처리
  • React Router : 페이지 이동 및 URL 상태 유지
  • Axios : HTTP 요청을 위한 라이브러리
  • PropTypes : 컴포넌트의 props 유효성 검사

4. 프로젝트 폴더 구조

📂 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 빌드 도구 설정

📍 프로젝트 코드 분석

① main.jsx - 앱의 진입점 설정

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를 실행하도록 설정

② 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 파라미터
	- 특정 항목의 정보를 동적으로 전달하고 상세 페이지에서 활용

③ placeApi.js - API 호출

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 데이터를 가져오며, 페이지네이션을 처리!

(fetchPlaces 함수)

export const fetchPlaces = async (query, page) => { ... }

비동기 함수로 데이터를 요청하고 응답을 받을 때까지 기다림

  • query: 검색어 (현재는 사용되지 않지만 이후 검색 기능 확장을 위해 포함)
  • page: 현재 페이지 번호

(API 요청)

const response = await axios.get(
  "https://jsonplaceholder.typicode.com/todos",
  {
    params: {
      _page: page, // 요청할 페이지 번호
      _limit: 10,  // 페이지당 최대 항목 수
    },
  }
);
  • axios.get()를 사용하여 JSONPlaceholder API의 todos 데이터를 가져옴
  • params 객체를 사용해 API에 전달할 쿼리 파라미터 설정
  • 예를 들어, 사용자가 page=2로 요청
    https://jsonplaceholder.typicode.com/todos?_page=2&_limit=10
    -> 11~20번째 목록이 반환!

④ usePlaces.js - Tanstack Query 데이터 페칭

import { 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. 페이지 이동할 때 이전 데이터를 유지하면서 새로운 데이터를 가져옴

⑤ Home.jsx - 메인 페이지(검색, 페이지네이션)

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 컴포넌트를 사용하여 페이지 이동 처리

⑥ SearchBar.jsx - 검색 기능

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. 결과가 화면에 표시

⑦ Pagination.jsx - 페이지네이션 기능

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}
      >
        &lt;
      </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}
      >
        &gt;
      </button>
    </div>
  );
};

// PropTypes를 사용한 prop 검증 추가
Pagination.propTypes = {
  total: PropTypes.number.isRequired, // total prop을 필수 숫자로 설정
};

export default Pagination;

이 컴포넌트는 애플리케이션의 페이지네이션 기능을 제공하며, URL page 쿼리 파라미터를 업데이트하여 특정 페이지로 이동하도록 구현

1. URL에서 현재 page 값을 가져와 현재 페이지와 그룹을 계산
2. 시작 및 끝 페이지 범위를 설정
3. 사용자가 페이지 버튼 클릭 → URL 변경 → 새 페이지 데이터 로드
4. 이전(&lt;) / 다음(&gt;) 버튼 클릭 시 페이지 그룹 이동

⑧ Detail.jsx - 상세페이지

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를 사용하여 이전 페이지로 돌아가는 기능을 제공


📍 프로젝트 결과물

프로젝트 결과물


📍 프로젝트 마무리 및 회고

1. 프로젝트를 통해 배운 점

이번 프로젝트를 통해 Tanstack Query를 활용한 서버 상태 관리를 보다 효과적으로 구현하는 방법을 배울 수 있었습니다.

1) 효율적인 서버 상태 관리

  • 불필요한 네트워크 요청을 방지하고, 데이터 캐싱을 활용하여 성능을 최적화하는 방법

2) URL 기반 상태 관리

  • React Router의 useSearchParams를 이용해 검색과 페이지네이션 상태를 유지하는 방법

3) 사용자 경험 향상

  • 검색, 페이지네이션, 상세 페이지 이동 시 데이터를 유지하여 사용자 경험을 개선하는 전략

2. 프로젝트 성과

이 프로젝트에서는 JSONPlaceholder API를 이용해 할 일 목록을 가져오고, 이를 검색 및 페이지네이션 기능과 결합하여 다음과 같은 성과를 얻었습니다.

1) 데이터 페칭 최적화 → TanStack Query의 캐싱 기능 활용
2) 페이지 간 이동 시 상태 유지 → React Router를 통한 상태 관리
3) 간결한 코드 유지 → useQuery 훅을 사용하여 데이터 로딩 및 에러 핸들링 단순화
4) 재사용 가능한 컴포넌트 구축 → SearchBar, Pagination을 통한 UI 재사용

3. 프로젝트 개선점 및 향후 계획

1) 검색 기능 강화

  • 현재는 전체 데이터를 검색하지만, API 필터링 기능을 추가하여 보다 정확한 결과 제공 계획

2) 실제 데이터 활용

  • JSONPlaceholder가 아닌 실제 REST API를 연동해 보다 실용적인 프로젝트로 확장

📚 참고한 글

[출처]https://jjang-j.tistory.com/20
[출처]https://velog.io/@jihyeonjeong11/React-Why-React-Query

profile
닌자가 되자!

0개의 댓글