01 SOFTWARE
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 저장소를 참조하세요.

다음 단계

On this page