본문으로 건너뛰기

스키마 검증 및 타입 변환

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_atupdated_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를 참조할 수 있습니다.