Sdk
Advanced
고급 사용법 및 최적화
Advanced
SDK의 고급 기능과 최적화 방법입니다.
성능 최적화
React Query 캐싱 전략
Stale Time 최적화
데이터의 변경 빈도에 따라 staleTime을 조정하세요.
// 자주 변경되는 데이터 (재고, 가격 등)
const { data } = client.query.useQuery(
{ collection: 'products' },
{ staleTime: 30 * 1000 } // 30초
)
// 가끔 변경되는 데이터 (카테고리, 태그 등)
const { data } = client.query.useQuery(
{ collection: 'categories' },
{ staleTime: 5 * 60 * 1000 } // 5분
)
// 거의 변경되지 않는 데이터 (설정, 약관 등)
const { data } = client.query.useQuery(
{ collection: 'settings' },
{ staleTime: 60 * 60 * 1000 } // 1시간
)Prefetching
다음 페이지를 미리 로드하여 UX를 개선하세요.
import { useEffect } from 'react'
function ProductsPage({ page }: { page: number }) {
const { data } = client.query.useQuery({
collection: 'products',
options: { page, limit: 20 }
})
// 다음 페이지 prefetch
useEffect(() => {
if (data && data.length === 20) {
client.query.prefetchQuery({
collection: 'products',
options: { page: page + 1, limit: 20 }
})
}
}, [data, page])
return <ProductList products={data} />
}필요한 필드만 요청
API 요청 시 필요한 필드만 요청하여 페이로드를 줄이세요.
const { data } = client.query.useQuery({
collection: 'products',
options: {
limit: 100,
// Payload CMS의 select 기능 사용
}
})
// 데이터 가공이 필요한 경우
const productNames = data?.map(p => ({ id: p.id, name: p.name }))컴포넌트 최적화
React.memo
불필요한 리렌더링을 방지하세요.
import { memo } from 'react'
const ProductCard = memo(({ product }: { product: Product }) => {
return (
<div>
<h3>{product.name}</h3>
<p>{product.price}</p>
</div>
)
})
function ProductList({ products }: { products: Product[] }) {
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}가상화
긴 목록은 가상화로 최적화하세요.
import { useVirtualizer } from '@tanstack/react-virtual'
function VirtualProductList() {
const { data } = client.query.useQuery({
collection: 'products',
options: { limit: 1000 }
})
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: data?.length || 0,
getScrollElement: () => parentRef.current,
estimateSize: () => 100,
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map(item => (
<div
key={item.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${item.size}px`,
transform: `translateY(${item.start}px)`,
}}
>
<ProductCard product={data![item.index]} />
</div>
))}
</div>
</div>
)
}Server-Side Rendering (SSR)
Server Component
import { serverClient } from '@/lib/server-client'
import { Suspense } from 'react'
async function ProductList() {
const { data } = await serverClient.from('products').find({
limit: 20,
where: { status: { equals: 'published' } }
})
return (
<div>
{data?.docs.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
export default function ProductsPage() {
return (
<Suspense fallback={<ProductListSkeleton />}>
<ProductList />
</Suspense>
)
}Static Site Generation (SSG)
import { serverClient } from '@/lib/server-client'
export async function generateStaticParams() {
const { data } = await serverClient.from('products').find({
limit: 100,
})
return data?.docs.map(product => ({
slug: product.slug,
})) || []
}
export default async function ProductPage({
params
}: {
params: { slug: string }
}) {
const { data: product } = await serverClient
.from('products')
.findById(params.slug)
return <ProductDetail product={product} />
}Incremental Static Regeneration (ISR)
export const revalidate = 3600 // 1시간마다 재생성
export default async function ProductsPage() {
const { data } = await serverClient.from('products').find({
limit: 20,
})
return <ProductList products={data?.docs} />
}고급 쿼리
복잡한 필터링
const { data } = await client.from('products').find({
where: {
and: [
{ status: { equals: 'published' } },
{
or: [
{
and: [
{ price: { greater_than: 10000 } },
{ category: { equals: 'electronics' } }
]
},
{
and: [
{ tags: { contains: 'featured' } },
{ stock: { greater_than: 0 } }
]
}
]
}
]
}
})동적 쿼리 빌더
interface FilterOptions {
category?: string
minPrice?: number
maxPrice?: number
tags?: string[]
inStock?: boolean
}
function buildProductQuery(filters: FilterOptions) {
const conditions: any[] = [
{ status: { equals: 'published' } }
]
if (filters.category) {
conditions.push({ category: { equals: filters.category } })
}
if (filters.minPrice !== undefined) {
conditions.push({ price: { greater_than_equal: filters.minPrice } })
}
if (filters.maxPrice !== undefined) {
conditions.push({ price: { less_than_equal: filters.maxPrice } })
}
if (filters.tags && filters.tags.length > 0) {
conditions.push({
or: filters.tags.map(tag => ({ tags: { contains: tag } }))
})
}
if (filters.inStock) {
conditions.push({ stock: { greater_than: 0 } })
}
return { and: conditions }
}
// 사용
const { data } = await client.from('products').find({
where: buildProductQuery({
category: 'electronics',
minPrice: 10000,
maxPrice: 100000,
tags: ['featured', 'new'],
inStock: true,
})
})배치 작업
병렬 요청
// 여러 컬렉션을 동시에 조회
const [products, categories, tags] = await Promise.all([
client.from('products').find({ limit: 10 }),
client.from('product-categories').find(),
client.from('product-tags').find(),
])순차 요청
// 의존성이 있는 요청
const { data: category } = await client
.from('product-categories')
.findById(categoryId)
if (category) {
const { data: products } = await client.from('products').find({
where: { category: { equals: category.id } }
})
}캐시 관리
수동 캐시 업데이트
import { useQueryClient } from '@tanstack/react-query'
function ProductForm() {
const queryClient = useQueryClient()
async function handleCreate(data: ProductInput) {
const { data: newProduct } = await client.from('products').create(data)
// 캐시에 새 상품 추가
queryClient.setQueryData(
['collection', 'products'],
(old: any) => ({
...old,
docs: [newProduct, ...old.docs],
})
)
}
return <form onSubmit={handleCreate}>...</form>
}캐시 무효화
import { useQueryClient } from '@tanstack/react-query'
function ProductActions({ productId }: { productId: string }) {
const queryClient = useQueryClient()
async function handleDelete() {
await client.from('products').remove(productId)
// 상품 목록 캐시 무효화
queryClient.invalidateQueries(['collection', 'products'])
// 특정 상품 캐시 제거
queryClient.removeQueries(['collection', 'products', productId])
}
return <button onClick={handleDelete}>Delete</button>
}타입 안전성 강화
커스텀 타입 가드
function isProduct(value: any): value is Product {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
'price' in value
)
}
// 사용
const response = await client.from('products').findById(id)
if (isSuccessResponse(response) && isProduct(response.data)) {
console.log(response.data.name) // 타입 안전
}제네릭 헬퍼
async function fetchCollection<T>(
collection: string,
query: QueryParams
): Promise<T[]> {
const { data } = await client.from(collection).find(query)
return (data?.docs as T[]) || []
}
// 사용
const products = await fetchCollection<Product>('products', {
limit: 10,
})모니터링 및 로깅
성능 모니터링
const client = createBrowserClient({
clientKey: process.env.NEXT_PUBLIC_SOFTWARE_CLIENT_KEY!,
debug: {
logRequests: true,
logResponses: true,
},
errorLogger: {
log: (error) => {
// 에러 추적
analytics.track('SDK Error', {
code: error.code,
message: error.message,
})
},
},
})커스텀 로거
class CustomLogger {
private logs: any[] = []
log(type: string, data: any) {
this.logs.push({
type,
data,
timestamp: new Date().toISOString(),
})
if (this.logs.length > 100) {
this.flush()
}
}
flush() {
// 로그를 서버로 전송
fetch('/api/logs', {
method: 'POST',
body: JSON.stringify(this.logs),
})
this.logs = []
}
}
const logger = new CustomLogger()
const client = createBrowserClient({
clientKey: process.env.NEXT_PUBLIC_SOFTWARE_CLIENT_KEY!,
errorLogger: {
log: (error) => logger.log('error', error),
},
})보안 강화
Rate Limiting
class RateLimiter {
private requests: number[] = []
private limit: number
private window: number
constructor(limit: number, window: number) {
this.limit = limit
this.window = window
}
async throttle() {
const now = Date.now()
this.requests = this.requests.filter(time => now - time < this.window)
if (this.requests.length >= this.limit) {
const waitTime = this.window - (now - this.requests[0])
await new Promise(resolve => setTimeout(resolve, waitTime))
}
this.requests.push(Date.now())
}
}
const rateLimiter = new RateLimiter(10, 1000) // 초당 10개 요청
async function fetchWithRateLimit() {
await rateLimiter.throttle()
return client.from('products').find()
}Request 검증
function validateProductInput(data: any): ProductInput {
if (!data.name || data.name.trim().length === 0) {
throw new ValidationError('Product name is required')
}
if (!data.price || data.price < 0) {
throw new ValidationError('Valid price is required')
}
return data as ProductInput
}
async function createProduct(data: any) {
const validData = validateProductInput(data)
return client.from('products').create(validData)
}테스팅
단위 테스트
import { describe, it, expect, vi } from 'vitest'
import { createBrowserClient } from '@01.software/sdk'
describe('SDK Client', () => {
it('should fetch products', async () => {
const client = createBrowserClient({
clientKey: 'test-key'
})
const { data } = await client.from('products').find()
expect(data).toBeDefined()
expect(Array.isArray(data?.docs)).toBe(true)
})
})Mock 데이터
import { vi } from 'vitest'
vi.mock('@01.software/sdk', () => ({
createBrowserClient: () => ({
from: () => ({
find: vi.fn().mockResolvedValue({
data: {
docs: [
{ id: '1', name: 'Product 1', price: 10000 },
{ id: '2', name: 'Product 2', price: 20000 },
],
pagination: {
totalDocs: 2,
page: 1,
totalPages: 1,
hasNextPage: false,
}
}
})
})
})
}))더 많은 예제와 패턴은 GitHub 저장소를 참조하세요.
다음 단계
- API Reference - 전체 API 레퍼런스
- Examples - 실전 예제
- GitHub - 소스 코드