SDK
React Query
React 훅으로 데이터 페칭 및 캐싱하기
React Query
SDK는 TanStack Query (React Query)를 통합하여 데이터 페칭, 캐싱, 동기화를 쉽게 할 수 있습니다.
TanStack Query를 사용하면 자동 캐싱, 재검증, 백그라운드 업데이트 등의 이점을 얻을 수 있습니다.
훅 종류
SDK는 TanStack Query의 원형을 살린 다음 훅들을 제공합니다:
useQuery- 목록 조회useSuspenseQuery- Suspense와 함께 목록 조회useQueryById- 단일 항목 조회useSuspenseQueryById- Suspense와 함께 단일 항목 조회useInfiniteQuery- 무한 스크롤useSuspenseInfiniteQuery- Suspense와 함께 무한 스크롤
useQuery
컬렉션의 목록을 조회합니다.
기본 사용법
'use client'
import { client } from '@/lib/client'
export default function ProductsPage() {
const { data, isLoading, error } = client.query.useQuery(
{
collection: 'products',
options: {
limit: 10,
where: { status: { equals: 'published' } }
}
}
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<div>
{data?.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}반환값
{
data?: T[] // 컬렉션 데이터 배열
isLoading: boolean // 첫 로딩 상태
isFetching: boolean // 백그라운드 재검증 포함
error: Error | null // 에러 정보
refetch: () => void // 수동 재검증
// ... 기타 TanStack Query 속성
}쿼리 옵션
client.query.useQuery(
{
collection: 'products',
options: {
// API 쿼리 파라미터
limit: 10,
page: 1,
sort: '-createdAt',
where: { status: { equals: 'published' } }
}
},
{
// TanStack Query 옵션
staleTime: 5 * 60 * 1000, // 5분
gcTime: 10 * 60 * 1000, // 10분
refetchOnWindowFocus: false,
enabled: true, // 조건부 쿼리
retry: 3,
}
)실전 예제
검색 기능
'use client'
import { client } from '@/lib/client'
import { useState } from 'react'
export default function SearchPage() {
const [query, setQuery] = useState('')
const { data, isLoading } = client.query.useQuery({
collection: 'products',
options: {
where: {
or: [
{ name: { like: `%${query}%` } },
{ description: { like: `%${query}%` } },
]
},
limit: 20,
}
})
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
{isLoading && <div>Searching...</div>}
<div>
{data?.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
</div>
)
}페이지네이션
'use client'
import { client } from '@/lib/client'
import { useState } from 'react'
export default function ProductsPage() {
const [page, setPage] = useState(1)
const { data, isLoading } = client.query.useQuery(
{
collection: 'products',
options: {
page,
limit: 20,
sort: '-createdAt',
}
}
)
return (
<div>
{isLoading && <div>Loading...</div>}
<div>
{data?.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
<div>
<button
onClick={() => setPage(p => p - 1)}
disabled={page <= 1}
>
Previous
</button>
<span>Page {page}</span>
<button
onClick={() => setPage(p => p + 1)}
disabled={!data || data.length < 20}
>
Next
</button>
</div>
</div>
)
}조건부 쿼리
'use client'
import { client } from '@/lib/client'
export default function UserProductsPage({ userId }: { userId?: string }) {
const { data } = client.query.useQuery(
{
collection: 'products',
options: {
where: { userId: { equals: userId! } },
limit: 10,
}
},
{
enabled: !!userId, // userId가 있을 때만 쿼리 실행
}
)
if (!userId) return <div>Please select a user</div>
return (
<div>
{data?.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}useSuspenseQuery
React Suspense와 함께 사용하는 훅입니다. enabled 옵션이 없고, 항상 데이터를 반환합니다.
'use client'
import { Suspense } from 'react'
import { client } from '@/lib/client'
function ProductList() {
const { data } = client.query.useSuspenseQuery({
collection: 'products',
options: { limit: 10 }
})
// data는 항상 존재 (undefined가 아님)
return (
<div>
{data.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}
export default function ProductsPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ProductList />
</Suspense>
)
}useQueryById
단일 항목을 ID로 조회합니다.
기본 사용법
'use client'
import { client } from '@/lib/client'
export default function ProductDetailPage({ id }: { id: string }) {
const { data: product, isLoading } = client.query.useQueryById({
collection: 'products',
id,
})
if (isLoading) return <div>Loading...</div>
if (!product) return <div>Product not found</div>
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
<p>Price: {product.price}</p>
</div>
)
}실전 예제
동적 라우트
'use client'
import { client } from '@/lib/client'
import { useParams } from 'next/navigation'
export default function PostPage() {
const params = useParams()
const id = params.id as string
const { data: post, isLoading, error } = client.query.useQueryById(
{
collection: 'posts',
id,
},
{
enabled: !!id,
}
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
if (!post) return <div>Post not found</div>
return (
<article>
<h1>{post.title}</h1>
<div>{post.content}</div>
</article>
)
}useSuspenseQueryById
Suspense와 함께 단일 항목을 조회합니다.
'use client'
import { Suspense } from 'react'
import { client } from '@/lib/client'
function ProductDetail({ id }: { id: string }) {
const { data: product } = client.query.useSuspenseQueryById({
collection: 'products',
id,
})
if (!product) return <div>Not found</div>
return (
<div>
<h1>{product.name}</h1>
<p>{product.price}</p>
</div>
)
}
export default function ProductPage({ id }: { id: string }) {
return (
<Suspense fallback={<div>Loading...</div>}>
<ProductDetail id={id} />
</Suspense>
)
}useInfiniteQuery
무한 스크롤을 구현합니다.
기본 사용법
'use client'
import { client } from '@/lib/client'
export default function InfiniteProductsPage() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = client.query.useInfiniteQuery({
collection: 'products',
pageSize: 20,
options: {
sort: '-createdAt',
}
})
if (isLoading) return <div>Loading...</div>
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
))}
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
>
{isFetchingNextPage ? 'Loading...' : 'Load More'}
</button>
)}
</div>
)
}자동 무한 스크롤
Intersection Observer를 사용한 자동 로딩:
'use client'
import { client } from '@/lib/client'
import { useEffect, useRef } from 'react'
export default function InfiniteProductsPage() {
const loadMoreRef = useRef<HTMLDivElement>(null)
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = client.query.useInfiniteQuery({
collection: 'products',
pageSize: 20,
})
useEffect(() => {
if (!loadMoreRef.current || !hasNextPage) return
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !isFetchingNextPage) {
fetchNextPage()
}
},
{ threshold: 1.0 }
)
observer.observe(loadMoreRef.current)
return () => observer.disconnect()
}, [hasNextPage, isFetchingNextPage, fetchNextPage])
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.map(product => (
<div key={product.id}>{product.name}</div>
))}
</div>
))}
{hasNextPage && (
<div ref={loadMoreRef}>
{isFetchingNextPage && <div>Loading more...</div>}
</div>
)}
</div>
)
}서버 사이드 Prefetching
SSR/SSG를 위한 데이터 prefetch:
// app/products/page.tsx
import { client } from '@/lib/client'
import { HydrationBoundary, dehydrate } from '@tanstack/react-query'
import ProductList from './ProductList'
export default async function ProductsPage() {
// 서버에서 데이터 prefetch
await client.query.prefetchQuery({
collection: 'products',
options: { limit: 10 }
})
return (
<HydrationBoundary state={dehydrate(client.queryClient)}>
<ProductList />
</HydrationBoundary>
)
}// app/products/[id]/page.tsx
import { client } from '@/lib/client'
import { HydrationBoundary, dehydrate } from '@tanstack/react-query'
import ProductDetail from './ProductDetail'
export default async function ProductPage({ params }: { params: { id: string } }) {
await client.query.prefetchQueryById({
collection: 'products',
id: params.id,
})
return (
<HydrationBoundary state={dehydrate(client.queryClient)}>
<ProductDetail id={params.id} />
</HydrationBoundary>
)
}캐싱 전략
Stale Time
데이터가 "신선한" 상태로 유지되는 시간:
client.query.useQuery(
{ collection: 'products' },
{ staleTime: 5 * 60 * 1000 } // 5분 동안 신선함
)staleTime동안은 캐시에서 즉시 데이터 반환- 시간 초과 후에는 백그라운드에서 재검증
GC Time (이전 cacheTime)
메모리에서 캐시를 유지하는 시간:
client.query.useQuery(
{ collection: 'products' },
{ gcTime: 10 * 60 * 1000 } // 10분 동안 캐시 유지
)Refetch 전략
client.query.useQuery(
{ collection: 'products' },
{
refetchOnWindowFocus: true, // 윈도우 포커스 시 재검증
refetchOnMount: true, // 마운트 시 재검증
refetchInterval: 30000, // 30초마다 재검증
}
)캐시 관리
invalidateQueries
// 특정 컬렉션의 모든 쿼리 무효화
client.query.invalidateQueries('products')
// 특정 타입만 무효화
client.query.invalidateQueries('products', 'list')
client.query.invalidateQueries('products', 'detail')
client.query.invalidateQueries('products', 'infinite')getQueryData / setQueryData
// 캐시된 데이터 가져오기
const products = client.query.getQueryData('products', 'list')
const product = client.query.getQueryData('products', 'detail', '123')
// 캐시 데이터 직접 설정
client.query.setQueryData('products', 'list', newProducts)
client.query.setQueryData('products', 'detail', '123', updatedProduct)Query Keys
SDK는 자동으로 query key를 생성합니다. 직접 사용하려면 collectionKeys 헬퍼를 사용하세요:
import { collectionKeys } from '@01.software/sdk'
// Query key 생성
collectionKeys('products').all // ['products']
collectionKeys('products').lists() // ['products', 'list']
collectionKeys('products').list({ limit: 10 }) // ['products', 'list', { limit: 10 }]
collectionKeys('products').details() // ['products', 'detail']
collectionKeys('products').detail('123') // ['products', 'detail', '123']
collectionKeys('products').infinites() // ['products', 'infinite']에러 처리
const { data, error, isError } = client.query.useQuery({
collection: 'products',
})
if (isError) {
return <div>Error: {error.message}</div>
}로딩 상태
const { data, isLoading, isFetching } = client.query.useQuery({
collection: 'products'
})
// isLoading: 첫 로딩 (캐시 없음)
// isFetching: 모든 로딩 (백그라운드 재검증 포함)스켈레톤 UI
function ProductList() {
const { data, isLoading } = client.query.useQuery({
collection: 'products',
options: { limit: 10 }
})
if (isLoading) {
return (
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="skeleton h-48" />
))}
</div>
)
}
return (
<div className="grid grid-cols-3 gap-4">
{data?.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}다음 단계
- API Reference - 전체 API 레퍼런스
- 에러 핸들링 - 에러 처리 방법
- 고급 기능 - 고급 사용법