본문으로 건너뛰기

스키마 검증 및 타입 변환

TanStack DB는 스키마를 사용하여 애플리케이션 전체에서 데이터가 유효하고 타입 안전하도록 보장합니다.

학습 내용​

이 가이드에서는 다음을 다룹니다:

  • TanStack DB에서 스키마 검증이 작동하는 방식
  • TInput 및 TOutput 타입 이해
  • 일반적인 패턴: 검증, 변환 및 기본값
  • 오류 처리 및 모범 사례

빠른 예제​

스키마는 낙관적 뮤테이션의 잘못된 데이터가 컬렉션에 들어가기 전에 이를 감지합니다:

import { z } from 'zod'
import { createCollection } from '@tanstack/react-db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'

const todoSchema = z.object({
id: z.string(),
text: z.string().min(1, "Text is required"),
completed: z.boolean(),
priority: z.number().min(0).max(5)
})

const collection = createCollection(
queryCollectionOptions({
schema: todoSchema,
queryKey: ['todos'],
queryFn: async () => api.todos.getAll(),
getKey: (item) => item.id,
// ...
})
)

// Invalid data throws SchemaValidationError
collection.insert({
id: "1",
text: "", // ❌ Too short
completed: "yes", // ❌ Wrong type
priority: 10 // ❌ Out of range
})
// Error: Validation failed with 3 issues

// Valid data works
collection.insert({
id: "1",
text: "Buy groceries", // ✅
completed: false, // ✅
priority: 2 // ✅
})

스키마는 타입 변환 및 기본값과 같은 고급 기능도 지원합니다:

const todoSchema = z.object({
id: z.string(),
text: z.string().min(1),
completed: z.boolean().default(false), // Auto-fill missing values
created_at: z.string().transform(val => new Date(val)) // Convert types
})

collection.insert({
id: "1",
text: "Buy groceries",
created_at: "2024-01-01T00:00:00Z" // String in
// completed auto-filled with false
})

const todo = collection.get("1")
console.log(todo.created_at.getFullYear()) // Date object out!

지원되는 스키마 라이브러리​

TanStack DB는 모든 StandardSchema 호환 라이브러리를 지원합니다:

이 가이드의 예제는 Zod를 사용하지만 패턴은 모든 라이브러리에 적용됩니다.


핵심 개념: TInput과 TOutput​

TanStack DB에서 스키마를 효과적으로 사용하려면 TInput과 TOutput을 이해하는 것이 중요합니다.

중요: 스키마는 클라이언트 변경 사항만 검증합니다. 즉, collection.insert() 및 collection.update()를 통해 삽입하거나 업데이트하는 데이터입니다. 서버 또는 동기화 계층에서 로드된 데이터는 자동으로 검증하지 않습니다. 서버 데이터를 검증해야 한다면 통합 계층에서 명시적으로 검증해야 합니다.

TInput과 TOutput이란 무엇인가요?​

변환을 포함하는 스키마를 정의하면 두 가지 타입이 생깁니다:

  • TInput: insert() 또는 update()을 호출할 때 사용자가 제공하는 타입
  • TOutput: 컬렉션에 저장되고 쿼리에서 반환되는 타입
const todoSchema = z.object({
id: z.string(),
text: z.string(),
created_at: z.string().transform(val => new Date(val))
})

// TInput type: { id: string, text: string, created_at: string }
// TOutput type: { id: string, text: string, created_at: Date }

스키마는 TInput을 TOutput으로 변환하는 경계 역할을 합니다.

핵심 설계 원칙: TInput은 TOutput의 상위 집합이어야 합니다​

변환을 사용할 때 TInput은 TOutput에 포함된 모든 값을 허용해야 합니다. 이는 업데이트가 올바르게 작동하는 데 필수적입니다.

그 이유는 다음과 같습니다. collection.update(id, (draft) => {...})을 호출하면 draft 매개변수의 타입은 TInput이지만 이미 TOutput으로 변환된 데이터가 포함됩니다. 복잡한 타입 처리 없이 작동하려면 스키마가 입력 형식과 출력 형식을 모두 허용해야 합니다.

// ❌ BAD: TInput only accepts strings
const schema = z.object({
created_at: z.string().transform(val => new Date(val))
})
// TInput: { created_at: string }
// TOutput: { created_at: Date }
// Problem: draft.created_at is a Date, but TInput only accepts string!

// ✅ GOOD: TInput accepts both string and Date (superset of TOutput)
const schema = z.object({
created_at: z.union([z.string(), z.date()])
.transform(val => typeof val === 'string' ? new Date(val) : val)
})
// TInput: { created_at: string | Date }
// TOutput: { created_at: Date }
// Success: draft.created_at can be a Date because TInput accepts Date!

경험 법칙: 스키마가 타입 A를 타입 B로 변환한다면 z.union([A, B])을 사용하여 TInput이 둘 다 허용해야 합니다.

이것이 중요한 이유​

컬렉션의 모든 데이터는 TOutput입니다:

  • 컬렉션에 저장된 데이터
  • 쿼리에서 반환된 데이터
  • PendingMutation.modified의 데이터
  • 뮤테이션 핸들러의 데이터
const collection = createCollection({
schema: todoSchema,
onInsert: async ({ transaction }) => {
const item = transaction.mutations[0].modified

// item is TOutput
console.log(item.created_at instanceof Date) // true

// If your API needs a string, serialize it
await api.todos.create({
...item,
created_at: item.created_at.toISOString() // Date → string
})
}
})

// User provides TInput
collection.insert({
id: "1",
text: "Task",
created_at: "2024-01-01T00:00:00Z" // string
})

// Collection stores and returns TOutput
const todo = collection.get("1")
console.log(todo.created_at.getFullYear()) // It's a Date!

검증 패턴​

스키마는 데이터 품질을 보장하는 강력한 검증 기능을 제공합니다.

기본 타입 검증​

const userSchema = z.object({
id: z.string(),
name: z.string(),
age: z.number(),
email: z.string().email(),
active: z.boolean()
})

collection.insert({
id: "1",
name: "Alice",
age: "25", // ❌ Wrong type - expects number
email: "not-an-email", // ❌ Invalid email format
active: true
})
// Throws SchemaValidationError

문자열 제약 조건​

const productSchema = z.object({
id: z.string(),
name: z.string().min(3, "Name must be at least 3 characters"),
sku: z.string().length(8, "SKU must be exactly 8 characters"),
description: z.string().max(500, "Description too long"),
url: z.string().url("Must be a valid URL")
})

숫자 제약 조건​

const orderSchema = z.object({
id: z.string(),
quantity: z.number()
.int("Must be a whole number")
.positive("Must be greater than 0"),
price: z.number()
.min(0.01, "Price must be at least $0.01")
.max(999999.99, "Price too high"),
discount: z.number()
.min(0)
.max(100)
})

열거형 검증​

const taskSchema = z.object({
id: z.string(),
status: z.enum(['todo', 'in-progress', 'done']),
priority: z.enum(['low', 'medium', 'high', 'urgent'])
})

collection.insert({
id: "1",
status: "completed", // ❌ Not in enum
priority: "medium" // ✅
})

선택적 및 null 허용 필드​

const personSchema = z.object({
id: z.string(),
name: z.string(),
nickname: z.string().optional(), // Can be omitted
middleName: z.string().nullable(), // Can be null
bio: z.string().optional().nullable() // Can be omitted OR null
})

// All valid:
collection.insert({ id: "1", name: "Alice" }) // nickname omitted
collection.insert({ id: "2", name: "Bob", middleName: null })
collection.insert({ id: "3", name: "Carol", bio: null })

배열 검증​

const postSchema = z.object({
id: z.string(),
title: z.string(),
tags: z.array(z.string()).min(1, "At least one tag required"),
likes: z.array(z.number()).max(1000)
})

collection.insert({
id: "1",
title: "My Post",
tags: [], // ❌ Need at least one
likes: [1, 2, 3]
})

사용자 지정 검증​

const userSchema = z.object({
id: z.string(),
username: z.string()
.min(3)
.refine(
(val) => /^[a-zA-Z0-9_]+$/.test(val),
"Username can only contain letters, numbers, and underscores"
),
password: z.string()
.min(8)
.refine(
(val) => /[A-Z]/.test(val) && /[0-9]/.test(val),
"Password must contain at least one uppercase letter and one number"
)
})

필드 간 검증​

const dateRangeSchema = z.object({
id: z.string(),
start_date: z.string(),
end_date: z.string()
}).refine(
(data) => new Date(data.end_date) > new Date(data.start_date),
"End date must be after start date"
)

변환 패턴​

스키마는 데이터가 컬렉션에 들어올 때 데이터를 변환할 수 있습니다.

문자열에서 날짜로​

가장 일반적인 변환으로, ISO 문자열을 Date 객체로 변환합니다:

const eventSchema = z.object({
id: z.string(),
name: z.string(),
start_time: z.string().transform(val => new Date(val))
})

collection.insert({
id: "1",
name: "Conference",
start_time: "2024-06-15T10:00:00Z" // TInput: string
})

const event = collection.get("1")
console.log(event.start_time.getFullYear()) // TOutput: Date

문자열에서 숫자로​

const formSchema = z.object({
id: z.string(),
quantity: z.string().transform(val => parseInt(val, 10)),
price: z.string().transform(val => parseFloat(val))
})

collection.insert({
id: "1",
quantity: "42", // String from form input
price: "19.99"
})

const item = collection.get("1")
console.log(typeof item.quantity) // "number"

JSON 문자열에서 객체로​

const configSchema = z.object({
id: z.string(),
settings: z.string().transform(val => JSON.parse(val))
})

collection.insert({
id: "1",
settings: '{"theme":"dark","notifications":true}' // JSON string
})

const config = collection.get("1")
console.log(config.settings.theme) // "dark" (parsed object)

계산된 필드​

const userSchema = z.object({
id: z.string(),
first_name: z.string(),
last_name: z.string()
}).transform(data => ({
...data,
full_name: `${data.first_name} ${data.last_name}` // Computed
}))

collection.insert({
id: "1",
first_name: "John",
last_name: "Doe"
})

const user = collection.get("1")
console.log(user.full_name) // "John Doe"

문자열에서 열거형으로​

const orderSchema = z.object({
id: z.string(),
status: z.string().transform(val =>
val.toUpperCase() as 'PENDING' | 'SHIPPED' | 'DELIVERED'
)
})

새니타이제이션​

const commentSchema = z.object({
id: z.string(),
text: z.string().transform(val => val.trim()), // Remove whitespace
username: z.string().transform(val => val.toLowerCase()) // Normalize
})

복잡한 변환​

const productSchema = z.object({
id: z.string(),
name: z.string(),
price_cents: z.number()
}).transform(data => ({
...data,
price_dollars: data.price_cents / 100, // Add computed field
display_price: `$${(data.price_cents / 100).toFixed(2)}` // Formatted
}))

기본값​

스키마는 누락된 필드에 기본값을 자동으로 제공할 수 있습니다.

리터럴 기본값​

const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean().default(false),
priority: z.number().default(0),
tags: z.array(z.string()).default([])
})

collection.insert({
id: "1",
text: "Buy groceries"
// completed, priority, and tags filled automatically
})

const todo = collection.get("1")
console.log(todo.completed) // false
console.log(todo.priority) // 0
console.log(todo.tags) // []

함수 기본값​

기본값을 동적으로 생성합니다:

const postSchema = z.object({
id: z.string(),
title: z.string(),
created_at: z.date().default(() => new Date()),
view_count: z.number().default(0),
slug: z.string().default(() => crypto.randomUUID())
})

collection.insert({
id: "1",
title: "My First Post"
// created_at, view_count, and slug generated automatically
})

조건부 기본값​

const userSchema = z.object({
id: z.string(),
username: z.string(),
role: z.enum(['user', 'admin']).default('user'),
permissions: z.array(z.string()).default(['read'])
})

복합 기본값​

const eventSchema = z.object({
id: z.string(),
name: z.string(),
metadata: z.record(z.unknown()).default(() => ({
created_by: 'system',
version: 1
}))
})

변환과 기본값 결합​

const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean().default(false),
created_at: z.string()
.default(() => new Date().toISOString())
.transform(val => new Date(val))
})

collection.insert({
id: "1",
text: "Task"
// completed defaults to false
// created_at defaults to current time, then transforms to Date
})

타임스탬프 처리​

타임스탬프를 사용할 때는 일반적으로 사용자 입력을 변환하기보다 생성 날짜를 자동으로 지정하려고 합니다.

타임스탬프에 기본값 사용​

created_at 및 updated_at 필드에는 기본값을 사용하여 타임스탬프를 자동으로 생성합니다:

const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean().default(false),
created_at: z.date().default(() => new Date()),
updated_at: z.date().default(() => new Date())
})

// Timestamps generated automatically
collection.insert({
id: "1",
text: "Buy groceries"
// created_at and updated_at filled automatically
})

// Update timestamps
collection.update("1", (draft) => {
draft.text = "Buy groceries and milk"
draft.updated_at = new Date()
})

외부 소스의 날짜 입력 허용​

외부 소스(폼, API)에서 날짜 입력을 받는 경우 문자열과 Date 객체를 모두 허용하도록 유니언 타입을 사용해야 합니다. 이렇게 하면 TInput이 TOutput의 상위 집합이 됩니다:

const eventSchema = z.object({
id: z.string(),
name: z.string(),
scheduled_for: z.union([
z.string(), // Accept ISO string from form input (part of TInput)
z.date() // Accept Date from existing data (TOutput) or programmatic input
]).transform(val =>
typeof val === 'string' ? new Date(val) : val
)
})
// TInput: { scheduled_for: string | Date }
// TOutput: { scheduled_for: Date }
// ✅ TInput is a superset of TOutput (accepts both string and Date)

// Works with string input (new data)
collection.insert({
id: "1",
name: "Meeting",
scheduled_for: "2024-12-31T15:00:00Z" // From form input
})

// Works with Date input (programmatic)
collection.insert({
id: "2",
name: "Workshop",
scheduled_for: new Date()
})

// Updates work - scheduled_for is already a Date, and TInput accepts Date
collection.update("1", (draft) => {
draft.name = "Updated Meeting"
// draft.scheduled_for is a Date and can be used or modified
})

오류 처리​

검증에 실패하면 TanStack DB는 자세한 정보가 포함된 SchemaValidationError를 발생시킵니다.

기본 오류 처리​

import { SchemaValidationError } from '@tanstack/db'

try {
collection.insert({
id: "1",
email: "not-an-email",
age: -5
})
} catch (error) {
if (error instanceof SchemaValidationError) {
console.log(error.type) // 'insert' or 'update'
console.log(error.message) // "Validation failed with 2 issues"
console.log(error.issues) // Array of validation issues
}
}

오류 구조​

error.issues = [
{
path: ['email'],
message: 'Invalid email address'
},
{
path: ['age'],
message: 'Number must be greater than 0'
}
]

UI에 오류 표시​

const handleSubmit = async (data: unknown) => {
try {
collection.insert(data)
} catch (error) {
if (error instanceof SchemaValidationError) {
// Show errors by field
error.issues.forEach(issue => {
const fieldName = issue.path?.join('.') || 'unknown'
showFieldError(fieldName, issue.message)
})
}
}
}

React 예제​

import { SchemaValidationError } from '@tanstack/db'

function TodoForm() {
const [errors, setErrors] = useState<Record<string, string>>({})

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setErrors({})

try {
todoCollection.insert({
id: crypto.randomUUID(),
text: e.currentTarget.text.value,
priority: parseInt(e.currentTarget.priority.value)
})
} catch (error) {
if (error instanceof SchemaValidationError) {
const newErrors: Record<string, string> = {}
error.issues.forEach(issue => {
const field = issue.path?.[0] || 'form'
newErrors[field] = issue.message
})
setErrors(newErrors)
}
}
}

return (
<form onSubmit={handleSubmit}>
<input name="text" />
{errors.text && <span className="error">{errors.text}</span>}

<input name="priority" type="number" />
{errors.priority && <span className="error">{errors.priority}</span>}

<button type="submit">Add Todo</button>
</form>
)
}

모범 사례​

변환을 단순하게 유지​

성능 참고: 스키마 검증은 동기식이며 모든 낙관적 뮤테이션에서 실행됩니다. 빈번한 업데이트에서는 변환을 단순하게 유지합니다.

// ❌ Avoid expensive operations
const schema = z.object({
data: z.string().transform(val => {
// Heavy computation on every mutation
return expensiveParsingOperation(val)
})
})

// ✅ Better: Validate only, process elsewhere
const schema = z.object({
data: z.string() // Simple validation
})

// Process in component or mutation handler when needed
const processedData = expensiveParsingOperation(todo.data)

변환에 유니언 타입 사용(필수)​

스키마가 데이터를 다른 타입으로 변환하는 경우 TInput이 TOutput의 상위 집합이 되도록 유니언 타입을 반드시 사용해야 합니다. 이는 선택 사항이 아니며, 사용하지 않으면 업데이트가 실패합니다.

// ✅ REQUIRED: TInput accepts both string (new data) and Date (existing data)
const schema = z.object({
created_at: z.union([z.string(), z.date()])
.transform(val => typeof val === 'string' ? new Date(val) : val)
})
// TInput: { created_at: string | Date }
// TOutput: { created_at: Date }

// ❌ WILL BREAK: Updates fail because draft contains Date but TInput only accepts string
const schema = z.object({
created_at: z.string().transform(val => new Date(val))
})
// TInput: { created_at: string }
// TOutput: { created_at: Date }
// Problem: collection.update() passes a Date to a schema expecting string!

필요한 이유: collection.update() 중에 draft 객체에는 TOutput 데이터(이미 변환된 데이터)가 포함됩니다. 스키마는 이 데이터를 허용해야 하므로 TInput은 TOutput의 상위 집합이어야 합니다.

경계에서 검증​

컬렉션 스키마가 검증을 처리하도록 합니다. 검증 로직을 중복하지 않습니다:

// ❌ Avoid: Duplicate validation
function addTodo(text: string) {
if (!text || text.length < 3) {
throw new Error("Text too short")
}
todoCollection.insert({ id: "1", text })
}

// ✅ Better: Let schema handle it
const todoSchema = z.object({
id: z.string(),
text: z.string().min(3, "Text must be at least 3 characters")
})

타입 추론​

스키마에서 TypeScript가 타입을 추론하도록 합니다:

const todoSchema = z.object({
id: z.string(),
text: z.string(),
completed: z.boolean()
})

type Todo = z.infer<typeof todoSchema> // Inferred type

// ✅ Use the inferred type
const collection = createCollection(
queryCollectionOptions({
schema: todoSchema,
// TypeScript knows the item type automatically
getKey: (item) => item.id // item is Todo
})
)

사용자 지정 오류 메시지​

사용자에게 유용한 오류 메시지를 제공합니다:

const userSchema = z.object({
username: z.string()
.min(3, "Username must be at least 3 characters")
.max(20, "Username is too long (max 20 characters)")
.regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores"),
email: z.string().email("Please enter a valid email address"),
age: z.number()
.int("Age must be a whole number")
.min(13, "You must be at least 13 years old")
})

전체 컨텍스트 예제​

예제 1: 풍부한 타입을 사용하는 Todo 앱​

검증, 변환 및 기본값을 보여 주는 완전한 Todo 애플리케이션입니다:

import { z } from 'zod'
import { createCollection } from '@tanstack/react-db'
import { not } from '@tanstack/db'
import { queryCollectionOptions } from '@tanstack/query-db-collection'

// Schema with validation, transformations, and defaults
const todoSchema = z.object({
id: z.string(),
text: z.string().min(1, "Todo text cannot be empty"),
completed: z.boolean().default(false),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
due_date: z.union([
z.string(),
z.date()
]).transform(val => typeof val === 'string' ? new Date(val) : val).optional(),
created_at: z.union([
z.string(),
z.date()
]).transform(val => typeof val === 'string' ? new Date(val) : val)
.default(() => new Date()),
tags: z.array(z.string()).default([])
})

type Todo = z.infer<typeof todoSchema>

// Collection setup
const todoCollection = createCollection(
queryCollectionOptions({
queryKey: ['todos'],
queryFn: async () => {
const response = await fetch('/api/todos')
const todos = await response.json()
// Reuse schema to parse and transform API responses
return todos.map((todo: any) => todoSchema.parse(todo))
},
getKey: (item) => item.id,
schema: todoSchema,
queryClient,

onInsert: async ({ transaction }) => {
const todo = transaction.mutations[0].modified

// Serialize dates for API
await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...todo,
due_date: todo.due_date?.toISOString(),
created_at: todo.created_at.toISOString()
})
})
},

onUpdate: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map(async (mutation) => {
const { original, changes } = mutation

// Serialize any date fields in changes
const serialized = {
...changes,
due_date: changes.due_date instanceof Date
? changes.due_date.toISOString()
: changes.due_date
}

await fetch(`/api/todos/${original.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(serialized)
})
})
)
},

onDelete: async ({ transaction }) => {
await Promise.all(
transaction.mutations.map(async (mutation) => {
await fetch(`/api/todos/${mutation.original.id}`, {
method: 'DELETE'
})
})
)
}
})
)

// Component usage
function TodoApp() {
const { data: todos } = useLiveQuery(q =>
q.from({ todo: todoCollection })
.where(({ todo }) => not(todo.completed))
.orderBy(({ todo }) => todo.created_at, 'desc')
)

const [errors, setErrors] = useState<Record<string, string>>({})

const addTodo = (text: string, priority: 'low' | 'medium' | 'high') => {
try {
todoCollection.insert({
id: crypto.randomUUID(),
text,
priority,
due_date: "2024-12-31T23:59:59Z"
// completed, created_at, tags filled automatically by defaults
})
setErrors({})
} catch (error) {
if (error instanceof SchemaValidationError) {
const newErrors: Record<string, string> = {}
error.issues.forEach(issue => {
const field = issue.path?.[0] || 'form'
newErrors[field] = issue.message
})
setErrors(newErrors)
}
}
}

const toggleComplete = (todo: Todo) => {
todoCollection.update(todo.id, (draft) => {
draft.completed = !draft.completed
})
}

return (
<div>
<h1>Todos</h1>

{errors.text && <div className="error">{errors.text}</div>}

<button onClick={() => addTodo("Buy groceries", "high")}>
Add Todo
</button>

<ul>
{todos?.map(todo => (
<li key={todo.id}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleComplete(todo)}
/>
<span>{todo.text}</span>
<span>Priority: {todo.priority}</span>
{todo.due_date && (
<span>Due: {todo.due_date.toLocaleDateString()}</span>
)}
<span>Created: {todo.created_at.toLocaleDateString()}</span>
</li>
))}
</ul>
</div>
)
}

예제 2: 계산된 필드를 사용하는 전자상거래 제품​

import { z } from 'zod'

// Schema with computed fields and transformations
const productSchema = z.object({
id: z.string(),
name: z.string().min(3, "Product name must be at least 3 characters"),
description: z.string().max(500, "Description too long"),
base_price: z.number().positive("Price must be positive"),
tax_rate: z.number().min(0).max(1).default(0.1),
discount_percent: z.number().min(0).max(100).default(0),
stock: z.number().int().min(0).default(0),
category: z.enum(['electronics', 'clothing', 'food', 'other']),
tags: z.array(z.string()).default([]),
created_at: z.union([z.string(), z.date()])
.transform(val => typeof val === 'string' ? new Date(val) : val)
.default(() => new Date())
}).transform(data => ({
...data,
// Computed fields
final_price: data.base_price * (1 + data.tax_rate) * (1 - data.discount_percent / 100),
in_stock: data.stock > 0,
display_price: `$${(data.base_price * (1 + data.tax_rate) * (1 - data.discount_percent / 100)).toFixed(2)}`
}))

type Product = z.infer<typeof productSchema>

const productCollection = createCollection(
queryCollectionOptions({
queryKey: ['products'],
queryFn: async () => api.products.getAll(),
getKey: (item) => item.id,
schema: productSchema,
queryClient,

onInsert: async ({ transaction }) => {
const product = transaction.mutations[0].modified

// API only needs base fields, not computed ones
await api.products.create({
name: product.name,
description: product.description,
base_price: product.base_price,
tax_rate: product.tax_rate,
discount_percent: product.discount_percent,
stock: product.stock,
category: product.category,
tags: product.tags
})
}
})
)

// Usage
function ProductList() {
const { data: products } = useLiveQuery(q =>
q.from({ product: productCollection })
.where(({ product }) => product.in_stock) // Use computed field
.orderBy(({ product }) => product.final_price, 'asc')
)

const addProduct = () => {
productCollection.insert({
id: crypto.randomUUID(),
name: "Wireless Mouse",
description: "Ergonomic wireless mouse",
base_price: 29.99,
discount_percent: 10,
category: "electronics",
stock: 50
// tax_rate, tags, created_at filled by defaults
// final_price, in_stock, display_price computed automatically
})
}

return (
<div>
{products?.map(product => (
<div key={product.id}>
<h3>{product.name}</h3>
<p>{product.description}</p>
<p>Price: {product.display_price}</p>
<p>Stock: {product.in_stock ? `${product.stock} available` : 'Out of stock'}</p>
<p>Category: {product.category}</p>
</div>
))}
</div>
)
}

통합 작성자를 위한 안내​

사용자 지정 컬렉션(Electric 또는 TrailBase와 같은)을 빌드하는 경우 스토리지 형식과 메모리 내 컬렉션 형식 간의 데이터 파싱 및 직렬화를 처리해야 합니다. 이는 클라이언트 뮤테이션 중에 수행되는 스키마 검증과는 별개입니다.

스키마, 데이터 파싱 및 타입 변환을 처리하는 방법을 비롯하여 사용자 지정 컬렉션 통합을 만드는 방법에 대한 종합적인 문서는 Collection Options Creator Guide를 참조할 수 있습니다.