01 SOFTWARE
SDK

Query Builder

Supabase 스타일의 타입 안전한 쿼리 빌더

쿼리 빌더

SDK는 Supabase 스타일의 직관적이고 타입 안전한 쿼리 빌더를 제공합니다.

기본 사용법

const queryBuilder = client.from('products')

CRUD 작업

find() - 목록 조회

여러 항목을 조회합니다.

const { data } = await client.from('products').find({
  limit: 10,
  page: 1,
  sort: '-createdAt',
  where: {
    status: { equals: 'published' }
  }
})

console.log(data.docs)        // Product[]
console.log(data.pagination)  // 페이지네이션 정보

반환 타입

{
  data?: {
    docs: T[]
    pagination: {
      totalDocs: number
      limit: number
      page: number
      totalPages: number
      hasNextPage: boolean
      hasPrevPage: boolean
      nextPage: number | null
      prevPage: number | null
    }
  }
  error?: SDKError
}

findById() - ID로 조회

특정 항목을 ID로 조회합니다.

const { data: product } = await client
  .from('products')
  .findById('product_id')

console.log(product) // Product | undefined

create() - 생성

새 항목을 생성합니다.

const { data: newProduct } = await client.from('products').create({
  name: '새 상품',
  price: 10000,
  status: 'draft',
  description: '상품 설명',
})

console.log(newProduct) // Product

TypeScript가 필수 필드와 선택 필드를 자동으로 추론합니다.

update() - 수정

기존 항목을 수정합니다.

const { data: updatedProduct } = await client
  .from('products')
  .update('product_id', {
    name: '수정된 상품명',
    price: 15000,
  })

console.log(updatedProduct) // Product

remove() - 삭제

항목을 삭제합니다.

const { data: deletedProduct } = await client
  .from('products')
  .remove('product_id')

console.log(deletedProduct) // Product (삭제된 항목)

쿼리 옵션

정렬 (sort)

// 오름차순
await client.from('products').find({
  sort: 'price'
})

// 내림차순
await client.from('products').find({
  sort: '-createdAt'
})

// 여러 필드로 정렬
await client.from('products').find({
  sort: '-createdAt,name'
})

페이지네이션

const { data } = await client.from('products').find({
  page: 2,
  limit: 20,
})

console.log(data.pagination.hasNextPage)  // boolean
console.log(data.pagination.nextPage)     // number | null
console.log(data.pagination.totalPages)   // number

필터링 (where)

기본 비교 연산자

await client.from('products').find({
  where: {
    // 같음
    status: { equals: 'published' },

    // 같지 않음
    status: { not_equals: 'draft' },

    // 보다 큼
    price: { greater_than: 1000 },

    // 보다 큼 또는 같음
    price: { greater_than_equal: 1000 },

    // 보다 작음
    price: { less_than: 50000 },

    // 보다 작음 또는 같음
    price: { less_than_equal: 50000 },

    // 포함 (배열)
    tags: { contains: 'featured' },

    // 포함하지 않음
    tags: { not_contains: 'hidden' },

    // ~로 시작
    name: { like: 'iPhone%' },

    // 존재함
    description: { exists: true },

    // 존재하지 않음
    thumbnail: { exists: false },
  }
})

IN 연산자

await client.from('products').find({
  where: {
    status: { in: ['published', 'featured'] },
    category: { not_in: ['archived', 'deleted'] },
  }
})

날짜 필터

await client.from('posts').find({
  where: {
    createdAt: {
      greater_than: '2024-01-01T00:00:00.000Z'
    },
    publishedAt: {
      less_than: new Date().toISOString()
    }
  }
})

복합 조건

AND 조건

await client.from('products').find({
  where: {
    and: [
      { status: { equals: 'published' } },
      { price: { greater_than: 1000 } },
      { stock: { greater_than: 0 } },
    ]
  }
})

OR 조건

await client.from('products').find({
  where: {
    or: [
      { status: { equals: 'published' } },
      { status: { equals: 'featured' } },
    ]
  }
})

복잡한 조합

await client.from('products').find({
  where: {
    and: [
      { status: { equals: 'published' } },
      { price: { greater_than: 1000, less_than: 50000 } },
      {
        or: [
          { category: { equals: 'electronics' } },
          { tags: { contains: 'featured' } },
        ]
      }
    ]
  }
})

관계 필터링

관계된 데이터를 기준으로 필터링할 수 있습니다.

// 특정 카테고리의 상품 조회
await client.from('products').find({
  where: {
    category: { equals: 'category_id' }
  }
})

// 관계 필드의 속성으로 필터링
await client.from('products').find({
  where: {
    'category.slug': { equals: 'electronics' }
  }
})

실전 예제

검색 기능

async function searchProducts(query: string) {
  const { data } = await client.from('products').find({
    where: {
      or: [
        { name: { like: `%${query}%` } },
        { description: { like: `%${query}%` } },
        { tags: { contains: query } },
      ]
    },
    limit: 20,
  })

  return data?.docs || []
}

필터링과 정렬

async function getFeaturedProducts() {
  const { data } = await client.from('products').find({
    where: {
      and: [
        { status: { equals: 'published' } },
        { tags: { contains: 'featured' } },
        { stock: { greater_than: 0 } },
      ]
    },
    sort: '-createdAt',
    limit: 10,
  })

  return data?.docs || []
}

가격 범위 검색

async function getProductsByPriceRange(min: number, max: number) {
  const { data } = await client.from('products').find({
    where: {
      and: [
        { price: { greater_than_equal: min } },
        { price: { less_than_equal: max } },
        { status: { equals: 'published' } },
      ]
    },
    sort: 'price',
  })

  return data?.docs || []
}

페이지네이션 구현

async function getProductsPage(page: number = 1, limit: number = 20) {
  const { data } = await client.from('products').find({
    page,
    limit,
    where: { status: { equals: 'published' } },
    sort: '-createdAt',
  })

  return {
    products: data?.docs || [],
    pagination: data?.pagination,
  }
}

// 사용 예
const { products, pagination } = await getProductsPage(1, 20)
console.log(pagination.hasNextPage)
console.log(pagination.totalPages)

카테고리별 필터링

async function getProductsByCategory(categorySlug: string) {
  const { data } = await client.from('products').find({
    where: {
      and: [
        { 'category.slug': { equals: categorySlug } },
        { status: { equals: 'published' } },
      ]
    },
    sort: '-createdAt',
  })

  return data?.docs || []
}

타입 안전성

쿼리 빌더는 완전한 타입 안전성을 제공합니다.

// ✅ OK - 올바른 필드
await client.from('products').find({
  where: { name: { equals: 'iPhone' } }
})

// ❌ 타입 에러 - 존재하지 않는 필드
await client.from('products').find({
  where: { invalidField: { equals: 'value' } }
})

// ✅ OK - 올바른 연산자
await client.from('products').find({
  where: { price: { greater_than: 1000 } }
})

// ❌ 타입 에러 - 잘못된 연산자
await client.from('products').find({
  where: { price: { invalid_operator: 1000 } }
})

에러 처리

const response = await client.from('products').find()

if (response.error) {
  console.error('Error:', response.error.message)
  console.error('Suggestion:', response.error.suggestion)
  return
}

// 성공한 경우에만 data 접근
const products = response.data.docs

또는 타입 가드 사용:

import { isSuccessResponse, isErrorResponse } from '@01.software/sdk'

const response = await client.from('products').find()

if (isSuccessResponse(response)) {
  // TypeScript가 response.data의 타입을 자동 추론
  console.log(response.data.docs)
} else if (isErrorResponse(response)) {
  // TypeScript가 response.error의 타입을 자동 추론
  console.error(response.error.message)
}

성능 최적화

필요한 데이터만 조회

// limit을 적절히 설정
await client.from('products').find({
  limit: 10, // 한 번에 10개만 조회
})

인덱싱된 필드로 필터링

// ID, slug 등 인덱싱된 필드를 우선 사용
await client.from('products').find({
  where: { slug: { equals: 'iphone-15' } }
})

캐싱 활용

React Query를 사용하면 자동으로 캐싱됩니다.

// React Query가 자동으로 캐싱
const { data } = client.query.useCollection('products', {
  limit: 10,
})

자세한 내용은 React Query 문서를 참조하세요.

다음 단계

On this page