🚀 Tanstack Query
서버 상태(server state)를 관리해주는 라이브러리
데이터를 가져오고, 캐싱하고, 동기화하고, 갱신하는 것을 쉽게 만들어주는 도구
- Tanstack Query를 쓰면 직접 useState, useEffect, axios 등을 조합해서 관리할 필요가 없이, 자동으로 데이터 패칭 / 캐싱 / 로딩 처리 / 에러 핸들링을 다 해줌
📍 주요 기능
- API 요청(fetching) 관리
- 로딩 상태, 에러 상태 자동 관리
- 데이터 캐싱 (네트워크 요청 최소화)
- refetch (데이터 자동 새로고침)
- background fetching (화면은 유지, 데이터만 갱신)
- pagination, infinite scroll 지원
- 쿼리 무효화(invalidate)로 갱신
설치
npm install @tanstack/react-query
QueryClient 세팅
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './assets/index.css'
import { RouterProvider } from 'react-router-dom'
import { router } from './rotuer'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
const queryClient = new QueryClient()
createRoot(document.getElementById('root')).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode>
)
🛠️ Tanstack Query Devtools
쿼리의 상태와 데이터를 시각적으로 보여주는 툴
API 요청 상태, 캐시 상태, 쿼리의 결과를 쉽게 확인하고 디버깅할 수 있도록 도와줌
설치
npm install @tanstack/react-query-devtools
사용법
- 개발환경에서만 사용
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './assets/index.css'
import { RouterProvider } from 'react-router-dom'
import { router } from './rotuer'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient()
createRoot(document.getElementById('root')).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
{import.meta.env.DEV && <ReactQueryDevtools initialIsOpen={false} />}
</QueryClientProvider>
</StrictMode>
)
useQuery
서버나 비동기 데이터를 요청하고 관리하는 Tanstack Query의 훅
- 비동기 데이터 요청(fetch)
- 요청한 데이터의 캐싱, 로딩, 에러 상태 관리
- 데이터 자동 리페치(갱신) 기능 제공
export const useCamping = (page, perPage) => {
return useQuery({
queryKey: ['camping', page],
queryFn: () => getCampingData(page, perPage),
staleTime: 1000 * 60 * 5,
cacheTime: 1000 * 60 * 10,
})
}
// CampingPage.jsx
const { data, isLoading, isError } = useCamping(currentPage, 10)
const { data: campingData, totalCount, page, perPage } = data || {}
isLoading && <p>Loading...</p>
isError && <p>에러 발생</p>
주요 옵션
✅ queryKey
- 데이터를 고유하게 식별하는 키 (배열 형태 추천)
- 같은 queryKey를 가진 쿼리는 캐시 공유 가능
- 필수 속성
ex) queryKey: ['camping', page] ➡️ 'camping'이라는 그룹 안에서, 특정 page 별 데이터로 구분
✅ queryFn
- 데이터를 실제로 가져오는 함수
- 비동기 함수 사용 가능 (async/await)
- queryKey 기반으로 서버에 요청하거나 API 호출하는 로직을 작성
✅ staleTime (신선한 기간)
데이터를 다시 요청하지 않고 "신선하다"라고 간주하는 시간
- staleTime 동안은
👉 새로 fetch 안 하고 캐시 데이터 그대로 사용 - staleTime 지나야
👉 페이지 이동하거나 refetch 트리거할 때 다시 요청
staleTime: 1000 * 60 * 5, // 5분
- 5분 안에 컴포넌트가 언마운트 ➡️ 리마운트해도 서버 요청 안 함
- 5분 지나고 리마운트하면 서버 다시 요청
✅ cacheTime (캐시 보관 기간)
메모리에서 완전히 제거되기 전까지 보관하는 시간
- staleTime이 지나서 "신선하지 않다"고 돼도,
👉 cacheTime이 남아 있으면 캐시에 남아있음 - 컴포넌트 언마운트된 후에도
👉 cacheTime만큼 메모리에 유지
cacheTime: 1000 * 60 * 10, // 10분
- 이 쿼리를 쓰는 컴포넌트가 사라져도 10분 동안은 메모리에 유지
- 10분 안에 다시 접근하면 빠르게 보여주고 필요하면 refetch
- 10분 지나면 완전히 사라짐(가비지 컬렉션)
staleTime ↔️ cacheTime
staleTime | 데이터가 신선한 상태로 간주되는 시간. 이 시간 동안은 재요청 없이 캐시된 데이터를 사용 |
cacheTime | 메모리에 데이터가 남아있는 시간. 사용자가 페이지를 이동하거나 컴포넌트가 언마운트돼도 이 시간 동안은 캐시를 보관 |
흐름
staleTime 내 | 캐시 데이터만 사용. 새 요청 없음 |
staleTime 지나고 cacheTime 내 | 캐시 데이터 + 서버에 새 요청 (서버 응답 오면 교체) |
cacheTime 끝 | 캐시 삭제. 새로 요청 필요 |
페이지 네이션 그룹
페이지를 한 번에 표시하는 대신, 여러 개의 페이지 번호를 그룹으로 묶어서 표시
- 페이지 번호들을 일정 수의 범위로 나누어 그룹화
- 1번부터 10번까지 하나의 그룹, 11번부터 20번까지 또 다른 그룹으로 나누어가며 보여주기
const groupIndex = Math.floor((currentPage - 1) / perPage) // 현재 페이지가 어떤 페이지 그룹에 속하는지 계산
const startPage = groupIndex * perPage + 1 // 현재 페이지 그룹에서 첫 번째 페이지 번호를 계산
const endPage = Math.min(startPage + perPage - 1, totalPages) // 현재 페이지 그룹에서 마지막 페이지 번호를 계산
- 페이지 이동을 처리하는 함수
const handleFirstPage = () => onPageChange(1)
const handlePrevPage = () => onPageChange(currentPage - 1)
const handleNextPage = () => onPageChange(currentPage + 1)
const handleLastPage = () => onPageChange(totalPages)
- 페이지네이션 버튼에 쓰기 위해 시작 페이지 번호부터 끝 페이지 번호까지 숫자 만들기
const pageNumbers = Array.from(
{ length: endPage - startPage + 1 },
(_, index) => startPage + index
)
<div className={css.paginationArea}>
<button onClick={handleFirstPage} disabled={currentPage === 1}>
{'<<'}
</button>
<button onClick={handlePrevPage} disabled={currentPage === 1}>
{'<'}
</button>
{pageNumbers.map(num => (
<button
key={num}
onClick={() => onPageChange(num)}
className={num === currentPage ? css.active : ''}
>
{num}
</button>
))}
<button onClick={handleNextPage} disabled={currentPage === totalPages}>
{'>'}
</button>
<button onClick={handleLastPage} disabled={currentPage === totalPages}>
{'>>'}
</button>
</div>
📱 모바일 반응형 추가
matchMedia 사용
CSS 미디어 쿼리 조건을 JavaScript에서 직접 감지할 수 있게 해주는 API
- 화면 크기 변화에 따라 자동으로 조건을 감지하고 상태를 처리
- resize 이벤트를 별도로 다룰 필요가 없어 성능적으로 유리
- 미디어 쿼리 조건에 맞는 변화를 감지하여 바로 처리할 수 있기 때문에 코드가 간결
// 모바일 감지 (matchMedia 사용)
const [isMobile, setIsMobile] = useState(() => window.matchMedia('(max-width: 768px)').matches) // 초기 상태 설정
useEffect(() => {
const mediaQuery = window.matchMedia('(max-width: 768px)')
// 화면 크기 변화에 따라 실행
const handleMediaChange = e => setIsMobile(e.matches) // 모바일 상태일 때 true, 아닐 때 false
// 상태 변화 리스터 등록
mediaQuery.addEventListener('change', handleMediaChange)
return () => {
// 컴포넌트 언마운트 시 리스너 제거
mediaQuery.removeEventListener('change', handleMediaChange)
}
}, [])
- 조건이 이미 만족된 상태에서 창 크기가 768px 이하로 계속 유지되면, change 이벤트는 발생하지 않고 상태는 계속 true
- 창 크기가 768px을 넘거나 넘지 않을 때만 이벤트가 발생하며, 그 사이에서 조금씩 변화하는 크기에서는 이벤트가 발생하지 않음
- 단순히 "모바일인지 아닌지" 상태만 알고 싶을 때는 media query의 조건 충족 여부만 감지하면 되기 때문에, 굳이 resize처럼 모든 픽셀 변화마다 감지할 필요가 없고,이벤트 발생 횟수를 줄여 성능을 높이기 위해 change 사용
항목 | window.addEventListener('resize') | mediaQuery.addEventListener('change') |
언제 실행되나? | 창 크기가 1px만 변경돼도 계속 실행됨 | 지정한 미디어 조건이 처음 충족되거나 벗어날 때만 실행됨 |
실행 횟수 | 빈번하게 발생함 (매 픽셀 이동 시마다 발생) | 조건이 바뀔 때 한 번만 발생 |
성능 | 성능 낮을 수 있음 (쓰로틀 필요할 수 있음) | 성능 우수, 쓰로틀 필요 없음 |
용도 | 실시간 창 크기 변화 감지 | 반응형 조건 감지에 적합 (max-width, min-width 등) |
728x90
반응형
'LG 유플러스 유레카 SW > React' 카테고리의 다른 글
[#66] 비밀번호 암호화 + JWT 토큰 발급 (feat. 로그인) (2) | 2025.05.01 |
---|---|
[#65] React + Express + MongoDB 연동하기 (feat. 회원가입) (2) | 2025.04.30 |
[#63] 날씨 오픈 API 연동 (1) | 2025.04.28 |
[#62] 용돈 기입장 프로젝트 (feat. React) (0) | 2025.04.28 |
[#61] Redux란 (1) | 2025.04.24 |