PowerSync 컬렉션
PowerSync 컬렉션은 TanStack DB와 PowerSync 간의 원활한 통합을 제공하여, 메모리 내 TanStack DB 컬렉션과 PowerSync의 SQLite 데이터베이스 간 자동 동기화를 활성화합니다. 이를 통해 오프라인 사용이 가능한 영속성, 실시간 동기화 기능 및 강력한 충돌 해결을 제공합니다.
개요
@tanstack/powersync-db-collection 패키지를 사용하면 다음과 같은 컬렉션을 생성할 수 있습니다:
- 기본 PowerSync SQLite 데이터베이스의 상태를 자동으로 미러링합니다
- PowerSync 레코드가 변경되면 반응형으로 업데이트합니다
- 쿼리 기반 동기화를 지원하여 활성 라이브 쿼리에 관련된 데이터만 로드합니다
- 오류 발생 시 롤백되는 낙관적 뮤테이션을 지원합니다
- TanStack DB 트랜잭션과 PowerSync를 동기화하는 영속성 핸들러를 제공합니다
- PowerSync의 효율적인 SQLite 기반 스토리지 엔진을 사용합니다
- 오프라인 우선 시나리오에서 PowerSync의 실시간 동기화 기능과 함께 작동합니다
- PowerSync에 내장된 충돌 해결 및 데이터 일관성 보장을 활용합니다
- PostgreSQL, MongoDB 및 MySQL 백엔드와 실시간 동기화를 활성화합니다
1. 설치
선호하는 프레임워크 통합과 함께 PowerSync 컬렉션 패키지를 설치합니다. PowerSync는 현재 Web, React Native 및 Node.js에서 작동합니다. 아래 예제에서는 Web SDK를 사용합니다. 자세한 내용은 PowerSync 빠른 시작 문서를 참조할 수 있습니다.
npm install @tanstack/powersync-db-collection @powersync/web @journeyapps/wa-sqlite
2. PowerSync 데이터베이스 및 스키마 생성
import { Schema, Table, column } from "@powersync/web"
// Define your schema
const APP_SCHEMA = new Schema({
documents: new Table({
name: column.text,
author: column.text,
created_at: column.text,
archived: column.integer,
}),
})
// Initialize PowerSync database
const db = new PowerSyncDatabase({
database: {
dbFilename: "app.sqlite",
},
schema: APP_SCHEMA,
})
3. (선택 사항) 백엔드와 동기화 구성
import {
AbstractPowerSyncDatabase,
PowerSyncBackendConnector,
PowerSyncCredentials,
} from "@powersync/web"
// TODO implement your logic here
class Connector implements PowerSyncBackendConnector {
fetchCredentials: () => Promise<PowerSyncCredentials | null>
/** Upload local changes to the app backend.
*
* Use {@link AbstractPowerSyncDatabase.getCrudBatch} to get a batch of changes to upload.
*
* Any thrown errors will result in a retry after the configured wait period (default: 5 seconds).
*/
uploadData: (database: AbstractPowerSyncDatabase) => Promise<void>
}
// Configure the client to connect to a PowerSync service and your backend
db.connect(new Connector())
4. TanStack DB 컬렉션 생성
컬렉션을 생성하는 주요 방법은 두 가지입니다. 타입 추론을 사용하거나 스키마 유효성 검사를 사용할 수 있습니다. 타입 추론은 기본 PowerSync SQLite 테이블에서 컬렉션 타입을 추론합니다. 스키마 유효성 검사는 추가 입력/출력 유효성 검사와 타입 변환에 사용할 수 있습니다.
옵션 1: 테이블 타입 추론 사용
컬렉션 타입은 PowerSync 스키마의 테이블 정의에서 자동으로 추론됩니다. 테이블은 컬렉션 작업의 유효성을 내부적으로 검사하는 기본 표준 스키마 검증기를 구성하는 데 사용됩니다.
컬렉션 뮤테이션은 SQLite 타입을 허용하고 쿼리는 SQLite 타입의 데이터를 보고합니다.
import { createCollection } from "@tanstack/react-db"
import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection"
const documentsCollection = createCollection(
powerSyncCollectionOptions({
database: db,
table: APP_SCHEMA.props.documents,
})
)
/** Note: The types for input and output are defined as this */
// Used for mutations like `insert` or `update`
type DocumentCollectionInput = {
id: string
name: string | null
author: string | null
created_at: string | null // SQLite TEXT
archived: number | null // SQLite integer
}
// The type of query/data results
type DocumentCollectionOutput = DocumentCollectionInput
표준 PowerSync SQLite 타입은 다음 TypeScript 타입에 매핑됩니다:
| PowerSync 열 타입 | TypeScript 타입 | 설명 |
|---|---|---|
column.text | string | null | 문자열, JSON, 날짜(ISO 문자열) 등에 일반적으로 사용되는 텍스트 값 |
column.integer | number | null | 정수 값이며 불리언에도 사용됩니다(0/1) |
column.real | number | null | 부동 소수점 숫자 |
참고: 모든 PowerSync 열 타입은 기본적으로 null을 허용합니다.
옵션 2: 스키마 유효성 검사를 사용하는 SQLite 타입
사용자 지정 스키마를 사용하여 컬렉션 뮤테이션에 대한 추가 유효성 검사를 수행할 수 있습니다. 아래 스키마는
name, author 및 created_at 필드가 입력에 필수임을 단언합니다. name에는 추가 문자열 길이 검사도 있습니다.
참고: 이 예제에 지정된 입력 및 출력 타입은 여전히 기본 SQLite 타입을 충족합니다. 타입이 다른 경우 추가 deserializationSchema이 필요합니다. 자세한 내용은 아래 예제를 참조할 수 있습니다.
애플리케이션 로직(백엔드 포함)은 들어오는 모든 동기화 데이터가 schema을 사용한 유효성 검사를 통과하도록 강제해야 합니다. 데이터 유효성을 검사하지 않으면 컬렉션 데이터가 일관되지 않게 됩니다. 이는 치명적인 오류입니다! 이 경우에 대응하려면 onDeserializationError 핸들러를 제공해야 합니다.
import { createCollection } from "@tanstack/react-db"
import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection"
import { z } from "zod"
// Schema validates SQLite types but adds constraints
const schema = z.object({
id: z.string(),
name: z.string().min(3, { message: "Should be at least 3 characters" }),
author: z.string(),
created_at: z.string(), // SQLite TEXT for dates
archived: z.number(),
})
const documentsCollection = createCollection(
powerSyncCollectionOptions({
database: db,
table: APP_SCHEMA.props.documents,
schema,
onDeserializationError: (error) => {
// Present fatal error
},
})
)
/** Note: The types for input and output are defined as this */
// Used for mutations like `insert` or `update`
type DocumentCollectionInput = {
id: string
name: string
author: string
created_at: string // SQLite TEXT
archived: number // SQLite integer
}
// The type of query/data results
type DocumentCollectionOutput = DocumentCollectionInput
옵션 3: SQLite 입력 타입을 풍부한 출력 타입으로 변환
SQLite 호환 입력 타입을 유지하면서 SQLite 타입을 더 풍부한 타입(예: Date 객체)으로 변환할 수 있습니다:
참고: 변환된 타입은 TanStack DB에서 PowerSync SQLite 영속성 처리기에 제공됩니다. 이러한 타입은 SQLite에 영속화하려면
직렬화해야 합니다. 대부분의 타입은 기본적으로 변환됩니다. 사용자 지정 타입의 경우
serializer 매개변수를 제공하여 직렬화를 재정의합니다.
아래 예제에서는 nullable 열을 사용하지만, 이는 필수 사항이 아닙니다.
애플리케이션 로직(백엔드 포함)은 들어오는 모든 동기화 데이터가 schema을 사용한 유효성 검사를 통과하도록 강제해야 합니다. 데이터 유효성을 검사하지 않으면 컬렉션 데이터가 일관되지 않게 됩니다. 이는 치명적인 오류입니다! 이 경우에 대응하려면 onDeserializationError 핸들러를 제공해야 합니다.
const schema = z.object({
id: z.string(),
name: z.string().nullable(),
created_at: z
.string()
.nullable()
.transform((val) => (val ? new Date(val) : null)), // Transform SQLite TEXT to Date
archived: z
.number()
.nullable()
.transform((val) => (val != null ? val > 0 : null)), // Transform SQLite INTEGER to boolean
})
const documentsCollection = createCollection(
powerSyncCollectionOptions({
database: db,
table: APP_SCHEMA.props.documents,
schema,
onDeserializationError: (error) => {
// Present fatal error
},
// Optional: custom column serialization
serializer: {
// Dates are serialized by default, this is just an example
created_at: (value) => (value ? value.toISOString() : null),
},
})
)
/** Note: The types for input and output are defined as this */
// Used for mutations like `insert` or `update`
type DocumentCollectionInput = {
id: string
name: string | null
author: string | null
created_at: string | null // SQLite TEXT
archived: number | null
}
// The type of query/data results
type DocumentCollectionOutput = {
id: string
name: string | null
author: string | null
created_at: Date | null // JS Date instance
archived: boolean | null // JS boolean
}
옵션 4: 역직렬화를 사용하는 사용자 지정 입력/출력 타입
입력 및 출력 타입은 내부 SQLite 타입과 완전히 분리할 수 있습니다. 이를 사용하면 입력 뮤테이션에 풍부한 값을 허용할 수 있습니다.
들어오는 동기화된(SQLite) 업데이트를 유효성 검사하고 변환하려면 추가 deserializationSchema이 필요합니다. 이 스키마는 들어오는 SQLite 업데이트를 출력 타입으로 변환해야 합니다.
애플리케이션 로직(백엔드 포함)은 들어오는 모든 동기화 데이터가 deserializationSchema을 사용한 유효성 검사를 통과하도록 강제해야 합니다. 데이터 유효성을 검사하지 않으면 컬렉션 데이터가 일관되지 않게 됩니다. 이는 치명적인 오류입니다! 이 경우에 대응하려면 onDeserializationError 핸들러를 제공해야 합니다.
// Our input/output types use Date and boolean
const schema = z.object({
id: z.string(),
name: z.string(),
author: z.string(),
created_at: z.date(), // Accept Date objects as input
archived: z.boolean(), // Accept Booleans as input
})
// Schema to transform from SQLite types to our output types
const deserializationSchema = z.object({
id: z.string(),
name: z.string(),
author: z.string(),
created_at: z
.string()
.transform((val) => (new Date(val))), // SQLite TEXT to Date
archived: z
.number()
.transform((val) => (val > 0), // SQLite INTEGER to Boolean
})
const documentsCollection = createCollection(
powerSyncCollectionOptions({
database: db,
table: APP_SCHEMA.props.documents,
schema,
deserializationSchema,
onDeserializationError: (error) => {
// Present fatal error
},
})
)
/** Note: The types for input and output are defined as this */
// Used for mutations like `insert` or `update`
type DocumentCollectionInput = {
id: string
name: string
author: string
created_at: Date
archived: boolean
}
// The type of query/data results
type DocumentCollectionOutput = DocumentCollectionInput
기능
오프라인 우선
PowerSync 컬렉션은 기본적으로 오프라인 우선입니다. 모든 데이터가 SQLite 데이터베이스에 로컬로 저장되므로 인터넷 연결 없이도 앱이 작동할 수 있습니다. 연결이 복원되면 변경 사항이 자동으로 동기화됩니다.
실시간 동기화
PowerSync 백엔드에 연결되면 연결된 모든 클라이언트에서 변경 사항이 실시간으로 자동 동기화됩니다. 동기화 프로세스는 다음을 처리합니다:
- 서버와의 양방향 동기화
- 충돌 해결
- 오프라인 변경 사항의 대기열 관리
- 연결 손실 시 자동 재시도
쿼리 기반 동기화
컬렉션은 선택적 on-demand 동기화 모드를 지원합니다. 이 모드는 전체 테이블을 미리 메모리에 동기화하는 대신 활성 라이브 쿼리에 필요한 행만 로드합니다. 특정 시점에 대부분의 행에 액세스하지 않는 대규모 데이터 세트에 유용합니다.
풍부한 JavaScript 타입 사용
PowerSync 컬렉션은 Date, Boolean 및 사용자 지정 객체와 같은 풍부한 JavaScript 타입을 지원하면서 SQLite 호환성을 유지합니다. 컬렉션은 직렬화와 역직렬화를 자동으로 처리합니다:
import { z } from "zod"
import { Schema, Table, column } from "@powersync/web"
import { createCollection } from "@tanstack/react-db"
import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection"
// Define PowerSync SQLite schema
const APP_SCHEMA = new Schema({
tasks: new Table({
title: column.text,
due_date: column.text, // Stored as ISO string in SQLite
completed: column.integer, // Stored as 0/1 in SQLite
metadata: column.text, // Stored as JSON string in SQLite
}),
})
// Define rich types schema
const taskSchema = z.object({
id: z.string(),
title: z.string().nullable(),
due_date: z
.string()
.nullable()
.transform((val) => (val ? new Date(val) : null)), // Convert to Date
completed: z
.number()
.nullable()
.transform((val) => (val != null ? val > 0 : null)), // Convert to boolean
metadata: z
.string()
.nullable()
.transform((val) => (val ? JSON.parse(val) : null)), // Parse JSON
})
// Create collection with rich types
const tasksCollection = createCollection(
powerSyncCollectionOptions({
database: db,
table: APP_SCHEMA.props.tasks,
schema: taskSchema,
})
)
// Work with rich types in your code
await tasksCollection.insert({
id: crypto.randomUUID(),
title: "Review PR",
due_date: "2025-10-30T10:00:00Z", // String input is automatically converted to Date
completed: 0, // Number input is automatically converted to boolean
metadata: JSON.stringify({ priority: "high" }),
})
// Query returns rich types
const task = tasksCollection.get("task-1")
console.log(task.due_date instanceof Date) // true
console.log(typeof task.completed) // "boolean"
console.log(task.metadata.priority) // "high"
풍부한 타입을 사용한 타입 안전성
컬렉션은 전체 과정에서 타입 안전성을 유지합니다:
type TaskInput = {
id: string
title: string | null
due_date: string | null // Accept ISO string for mutations
completed: number | null // Accept 0/1 for mutations
metadata: string | null // Accept JSON string for mutations
}
type TaskOutput = {
id: string
title: string | null
due_date: Date | null // Get Date object in queries
completed: boolean | null // Get boolean in queries
metadata: {
priority: string
[key: string]: any
} | null
}
// TypeScript enforces correct types:
tasksCollection.insert({
due_date: new Date(), // Error: Type 'Date' is not assignable to type 'string'
})
const task = tasksCollection.get("task-1")
task.due_date.getTime() // OK - TypeScript knows this is a Date
낙관적 업데이트
컬렉션에 대한 업데이트는 먼저 로컬 상태에 낙관적으로 적용된 다음 PowerSync 및 백엔드와 동기화됩니다. 동기화 중 오류가 발생하면 변경 사항이 자동으로 롤백됩니다.
메타데이터 추적
메타데이터 추적을 사용하면 컬렉션 작업(삽입, 업데이트, 삭제)에 사용자 지정 메타데이터를 연결할 수 있습니다. 이 메타데이터는 작업과 함께 영속화되며 업로드 처리 중 PowerSync CrudEntry 레코드에서 사용할 수 있습니다. 이는 감사 정보, 작업 소스 또는 사용자 지정 처리 힌트와 같이 뮤테이션에 대한 추가 컨텍스트를 백엔드에 전달할 때 유용합니다.
메타데이터 추적 활성화
PowerSync 테이블에서 메타데이터 추적을 활성화해야 합니다:
const APP_SCHEMA = new Schema({
documents: new Table(
{
name: column.text,
author: column.text,
},
{
// Enable metadata tracking on this table
trackMetadata: true,
}
),
})
작업에서 메타데이터 사용
활성화하면 모든 컬렉션 작업에 메타데이터를 전달할 수 있습니다:
const documents = createCollection(
powerSyncCollectionOptions({
database: db,
table: APP_SCHEMA.props.documents,
})
)
// Insert with metadata
await documents.insert(
{
id: crypto.randomUUID(),
name: "Report Q4",
author: "Jane Smith",
},
{
metadata: {
source: "web-app",
userId: "user-123",
timestamp: Date.now(),
},
}
).isPersisted.promise
// Update with metadata
await documents.update(
docId,
{ metadata: { reason: "typo-fix", editor: "user-456" } },
(doc) => {
doc.name = "Report Q4 (Updated)"
}
).isPersisted.promise
// Delete with metadata
await documents.delete(docId, {
metadata: { deletedBy: "user-789", reason: "duplicate" },
}).isPersisted.promise
업로드 중 메타데이터 액세스
커넥터에서 업로드를 처리할 때 PowerSync CrudEntry 레코드에서 메타데이터를 사용할 수 있습니다:
import { CrudEntry } from "@powersync/web"
class Connector implements PowerSyncBackendConnector {
// ...
async uploadData(database: AbstractPowerSyncDatabase) {
const batch = await database.getCrudBatch()
if (!batch) return
for (const entry of batch.crud) {
console.log("Operation:", entry.op) // PUT, PATCH, DELETE
console.log("Table:", entry.table)
console.log("Data:", entry.opData)
console.log("Metadata:", entry.metadata) // Custom metadata (stringified)
// Parse metadata if needed
if (entry.metadata) {
const meta = JSON.parse(entry.metadata)
console.log("Source:", meta.source)
console.log("User ID:", meta.userId)
}
// Process the operation with the backend...
}
await batch.complete()
}
}
참고: 작업에 메타데이터가 제공되었지만 테이블에 trackMetadata: true가 없으면 경고가 기록되고 메타데이터는 무시됩니다.
구성 옵션
powerSyncCollectionOptions 함수는 다음 옵션을 허용합니다:
interface PowerSyncCollectionConfig<TTable extends Table, TSchema> {
// Required options
database: PowerSyncDatabase
table: Table
// Schema validation and type transformation
schema?: StandardSchemaV1
deserializationSchema?: StandardSchemaV1 // Required for custom input types
onDeserializationError?: (error: StandardSchemaV1.FailureResult) => void // Required for custom input types
// Optional Custom serialization
serializer?: {
[Key in keyof TOutput]?: (value: TOutput[Key]) => SQLiteCompatibleType
}
// Performance tuning
syncBatchSize?: number // Control batch size for initial sync, defaults to 1000
}
고급 트랜잭션
여러 작업을 일괄 처리하거나 복잡한 트랜잭션 시나리오를 처리하는 등 트랜잭션 처리를 더 세밀하게 제어해야 하는 경우, PowerSync의 트랜잭션 시스템을 TanStack DB 트랜잭션과 함께 직접 사용할 수 있습니다.
import { createTransaction } from "@tanstack/react-db"
import { PowerSyncTransactor } from "@tanstack/powersync-db-collection"
// Create a transaction that won't auto-commit
const batchTx = createTransaction({
autoCommit: false,
mutationFn: async ({ transaction }) => {
// Use PowerSyncTransactor to apply the transaction to PowerSync
await new PowerSyncTransactor({ database: db }).applyTransaction(
transaction
)
},
})
// Perform multiple operations in the transaction
batchTx.mutate(() => {
// Add multiple documents in a single transaction
for (let i = 0; i < 5; i++) {
documentsCollection.insert({
id: crypto.randomUUID(),
name: `Document ${i}`,
content: `Content ${i}`,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
})
}
})
// Commit the transaction
await batchTx.commit()
// Wait for the changes to be persisted
await batchTx.isPersisted.promise
이 접근 방식을 사용하면 다음을 수행할 수 있습니다:
- 여러 작업을 단일 트랜잭션으로 일괄 처리합니다
- 트랜잭션이 커밋되는 시점을 제어합니다
- 모든 작업이 원자적으로 수행되도록 보장합니다
- 영속성 확인을 기다립니다
- 복잡한 트랜잭션 시나리오를 처리합니다
주문형 동기화 모드(쿼리 기반 동기화)
기본적으로 PowerSync 컬렉션은 eager 동기화 모드를 사용하지만, 대신 on-demand 동기화 모드를 사용하도록 선택할 수 있습니다. 주문형 모드에서 컬렉션은 쿼리 기반으로 작동합니다. 즉, 모든 데이터를 동기화한 후 필터링하는 대신 활성 라이브 쿼리의 조건자를 만족하는 SQLite 데이터만 로드합니다.
내부적으로 데이터가 흐르는 방식을 이해하려면 두 가지 독립적인 차원이 작동한다는 점을 아는 것이 도움이 됩니다:
- 동기화 모드(
eager대on-demand) - SQLite에서 TanStack DB 컬렉션으로 데이터가 이동하는 방식을 제어합니다 - 동기화 스트림(선택 사항) -
onLoad또는onLoadSubset후크를 사용하여 PowerSync Service에서 SQLite로 데이터가 이동하는 방식을 제어합니다
이러한 차원은 독립적으로 결합되며, 아래에서는 여섯 가지 조합을 다룹니다:
| 예 | 동기화 모드 | 동기화 스트림 | 매개변수 |
|---|---|---|---|
| 1 | Eager | 아니요 | - |
| 2 | On-demand | 아니요 | - |
| 3 | Eager | 예 | 하드코딩됨 |
| 4 | On-demand | 예 | 하드코딩됨 |
| 5 | On-demand | 예 | 동적 - extractSimpleComparisons |
| 6 | On-demand | 예 | 동적 - parseWhereExpression |
예제 1과 2에서는 SQLite가 PowerSync의 동기화 메커니즘에 의해 이미 완전히 채워져 있다고 가정합니다. 예제 3~6에서는 동기화 스트림을 추가하여 처음부터 PowerSync Service의 어떤 데이터가 SQLite에 도달할지 제어합니다. 이는 디바이스에 데이터의 일부만 저장하려는 경우에 유용합니다.
예제 3과 4에서는 동기화 스트림 구독 매개변수가 하드코딩됩니다. 예제 5와 6에서는 라이브 쿼리의 조건자에서 해당 매개변수를 동적으로 도출하는 방법을 보여 주므로 쿼리가 변경되면 구독이 자동으로 조정됩니다.
1. Eager 모드
eager 모드에서는 SQLite 데이터베이스의 모든 데이터가 TanStack DB 컬렉션으로 동기화됩니다. 그런 다음 라이브 쿼리가 이 전체 데이터 세트를 필터링하여 최종 결과를 생성합니다. 따라서 컬렉션에는 활성 쿼리와 일치하는지 여부와 관계없이 모든 행이 포함됩니다.
SQLite
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
| 3 | list_2 | 0 |
| 4 | list_2 | 1 |
Eager 모드는 모든 SQLite 데이터를 TanStack DB 컬렉션으로 동기화합니다
컬렉션
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
| 3 | list_2 | 0 |
| 4 | list_2 | 1 |
TanStack DB 쿼리는 list_id = 'list_1'에 대해 필터링합니다
결과
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
const collection = createCollection(
powerSyncCollectionOptions({
database: db,
table: AppSchema.props.todos,
syncMode: 'eager'
}),
)
const liveQuery = createLiveQueryCollection({
query: (q) =>
q
.from({ todo: collection })
.where(
({ todo }) => eq(todo.list_id, 'list_id'),
)
.select(({ todo }) => ({
id: todo.id,
completed: todo.completed,
})),
})
2. 주문형 모드
on-demand 모드에서는 활성 TanStack DB 라이브 쿼리의 조건자를 만족하는 SQLite 데이터만 TanStack DB 컬렉션으로 동기화됩니다. 컬렉션에는 현재 등록된 쿼리와 관련된 행만 포함되므로 메모리에 보유되는 데이터의 양이 줄어듭니다.
SQLite
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
| 3 | list_2 | 0 |
| 4 | list_2 | 1 |
주문형 모드는 SQLite에서 TanStack DB 컬렉션으로 라이브 쿼리 중 하나라도 만족하는 데이터만 동기화합니다: list_id = 'list_1'
컬렉션
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
TanStack DB 쿼리는 list_id = 'list_1'에 대해 필터링합니다
결과
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
const collection = createCollection(
powerSyncCollectionOptions({
database: db,
table: AppSchema.props.todos,
syncMode: 'on-demand'
}),
)
// live query that tells the collection what the data domain is
const liveQuery = createLiveQueryCollection({
query: (q) =>
q
.from({ todo: collection })
.where(
({ todo }) => eq(todo.list_id, 'list_id'),
)
.select(({ todo }) => ({
id: todo.id,
completed: todo.completed,
})),
})
OnLoad/OnLoadSubset 후크를 사용한 동기화 스트림 통합
동기화 스트림은 eager 및 on-demand 동기화 모드 모두의 데이터 로드 후크를 통해 사용할 수 있습니다. 이를 통해 컬렉션이 정의될 때(Eager 모드) 또는 라이브 쿼리 조건자에 따라 컬렉션의 데이터 경계가 변경될 때(주문형) 동기화 스트림을 호출할 수 있습니다.
다음 동기화 스트림 정의가 있다고 가정합니다:
config:
edition: 3
streams:
lists:
query: SELECT * FROM lists WHERE owner_id = auth.user_id()
auto_subscribe: true
todos:
query: SELECT * FROM todos WHERE list_id = subscription.parameter('list') AND list_id IN (SELECT id FROM lists WHERE owner_id = auth.user_id())
3. 동기화 스트림을 사용하는 Eager 모드
컬렉션이 처음 로드될 때 동기화 스트림을 구독하려면 onLoad 후크를 사용합니다. 그러면 컬렉션이 쿼리를 제공하기 전에 SQLite가 채워집니다. eager 모드에서는 SQLite의 모든 행이 TanStack DB 컬렉션으로 동기화되고, 라이브 쿼리 필터링은 이 전체 데이터 세트에 대해 실행됩니다.
후크는 선택적으로 정리 함수를 반환하여 동기화 스트림 구독을 취소할 수 있습니다.
이 예제는 PowerSync Service에 4개의 todo가 있는 상태에서 시작하며, 그중 2개만 동기화 스트림을 통해 SQLite 데이터베이스로 동기화됩니다. Eager 모드이므로 두 항목 모두 SQLite 데이터베이스에서 컬렉션으로 동기화됩니다. 마지막으로 TanStack DB 쿼리는 라이브 쿼리 조건자와 일치하는 하나의 todo만 반환합니다.
PS Service
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
| 3 | list_2 | 0 |
| 4 | list_2 | 1 |
동기화 스트림을 구독하고 list_id = 'list_1'인 항목만 동기화합니다
SQLite
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
Eager 모드는 모든 SQLite 데이터를 TanStack DB 컬렉션으로 동기화합니다
컬렉션
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
TanStack DB 쿼리는 completed = 1에 대해 필터링합니다
결과
| id | list_id | completed |
|---|---|---|
| 2 | list_1 | 1 |
const collection = createCollection(
powerSyncCollectionOptions({
database: db,
table: AppSchema.props.todos,
syncMode: 'eager',
onLoad: async () => {
console.log('onLoad')
const subscription = await db
.syncStream('todos', { list: 'list_1' })
.subscribe()
await subscription.waitForFirstSync()
return () => {
console.log('onUnload')
subscription.unsubscribe()
}
},
}),
)
// A live query that filters by the `completed` state.
const liveQuery = createLiveQueryCollection({
query: (q) =>
q
.from({ todo: collection })
.where(({ todo }) => eq(todo.completed, 1))
.select(({ todo }) => ({
id: todo.id,
completed: todo.completed,
})),
})
4. 동기화 스트림을 사용하는 주문형 모드
컬렉션의 데이터 경계가 변경될 때마다(즉, 활성 라이브 쿼리 집합이 변경될 때마다) 동기화 스트림을 구독하려면 onLoadSubset 후크를 사용합니다. on-demand 모드에서는 활성 라이브 쿼리 조건자를 만족하는 행만 SQLite에서 TanStack DB 컬렉션으로 동기화됩니다.
후크는 선택적으로 정리 함수를 반환하여 해당 하위 집합이 더 이상 필요하지 않을 때 동기화 스트림 구독을 취소할 수 있습니다.
이 예제는 PowerSync Service에 4개의 todo가 있는 상태에서 시작하며, 그중 2개만 동기화 스트림을 통해 SQLite 데이터베이스로 동기화됩니다. 주문형 모드이므로 일치하는 todo 1개만 SQLite 데이터베이스에서 컬렉션으로 동기화됩니다. 마지막으로 TanStack DB 쿼리는 라이브 쿼리 조건자와 일치하는 하나의 todo만 반환합니다.
PS Service
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
| 3 | list_2 | 0 |
| 4 | list_2 | 1 |
동기화 스트림을 구독하고 list_id = 'list_1'인 항목만 동기화합니다
SQLite
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
주문형 모드는 라이브 쿼리 중 하나라도 만족하는 SQLite 데이터를 TanStack DB 컬렉션으로만 동기화합니다: completed = 1
컬렉션
| id | list_id | completed |
|---|---|---|
| 2 | list_1 | 1 |
TanStack DB 쿼리는 completed = 1에 대해 필터링합니다
결과
| id | list_id | completed |
|---|---|---|
| 2 | list_1 | 1 |
const collection = createCollection(
powerSyncCollectionOptions({
database: db,
table: AppSchema.props.todos,
syncMode: 'on-demand',
onLoadSubset: async (options) => {
console.log('onLoadSubset')
const subscription = await db
.syncStream('todos', { list: 'list_1' })
.subscribe()
await subscription.waitForFirstSync()
return () => {
console.log('onUnloadSubset')
subscription.unsubscribe()
}
},
}),
)
// A live query that filters by the `completed` state.
const liveQuery = createLiveQueryCollection({
query: (q) =>
q
.from({ todo: collection })
.where(({ todo }) => eq(todo.completed, 1))
.select(({ todo }) => ({
id: todo.id,
completed: todo.completed,
})),
})
5. 단순 조건자를 사용하는 주문형 모드
이전 예제에서는 동기화 스트림 구독 매개변수가 하드코딩되었습니다. 실제로는 라이브 쿼리의 where 절에서 이러한 매개변수를 동적으로 도출하려는 경우가 많습니다. 이렇게 하면 쿼리를 변경할 때 PowerSync Service에서 동기화되는 데이터가 자동으로 조정됩니다. extractSimpleComparisons는 onLoadSubset의 표현식 트리를 { field, operator, value } 객체의 평면 목록으로 구문 분석하는 편의 도우미입니다.
다음과 같은 라이브 쿼리가 있다고 가정합니다:
.where(({ todo }) => eq(todo.list_id, selectedListId))
onLoadSubset는 options.where를 표현식 트리 for eq(list_id, '<uuid>')로 받습니다.
list_id 값은 표현식 트리에서 구문 분석되어 syncStream()에 전달됩니다.
참고: 이 예제는 extractSimpleComparisons 사용을 설명하기 위한 것이므로 앞의 두 예제와 약간 다릅니다.
이 예제는 PowerSync Service에 4개의 todo가 있는 상태에서 시작하며, 동기화 스트림 구독 기준(list_id = "list_1")은 컬렉션에 등록된 라이브 쿼리에서 도출됩니다. 2개의 todo만 동기화 스트림을 통해 SQLite 데이터베이스로 동기화됩니다. 두 개의 todo가 SQLite 데이터베이스에서 컬렉션으로 동기화됩니다. 마지막으로 두 todo 모두 eq(todo.list_id, 'list_id')와 일치하므로 TanStack DB 쿼리는 두 todo를 모두 반환합니다.
PS Service
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
| 3 | list_2 | 0 |
| 4 | list_2 | 1 |
라이브 쿼리에서 list_id 기준을 도출하고 동기화 스트림을 구독하여 list = 'list_1'인 항목만 동기화합니다
SQLite
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
주문형 모드는 SQLite에서 TanStack DB 컬렉션으로 라이브 쿼리 중 하나라도 만족하는 데이터만 동기화합니다: list_id = 'list_1'
컬렉션
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
TanStack DB 쿼리는 list_id = 'list_1'에 대해 필터링합니다
결과
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
const collection = createCollection(
powerSyncCollectionOptions({
database: db,
table: AppSchema.props.todos,
syncMode: 'on-demand',
onLoadSubset: async (options) => {
// Extract simple comparisons from the where expression
const comparisons = extractSimpleComparisons(options.where)
// comparisons = [{ field: ['todo', 'list_id'], operator: 'eq', value: '<uuid>' }]
// Find the list_id filter
const listIdFilter = comparisons.find(
(c) => c.field.includes('list_id') && c.operator === 'eq',
)
if (!listIdFilter) {
console.warn('No list_id filter found, skipping Sync Stream')
return
}
console.log(`Subscribing to todos for list: ${listIdFilter.value}`)
const subscription = await db
.syncStream('todos', { list: listIdFilter.value })
.subscribe()
await subscription.waitForFirstSync()
return () => {
console.log(`Unsubscribing from todos for list: ${listIdFilter.value}`)
subscription.unsubscribe()
}
},
}),
)
// Simple filter -> triggers `onLoadSubset` with `eq(list_id, '...')`
const liveQuery = createLiveQueryCollection({
query: (q) =>
q
.from({ todo: collection })
.where(
({ todo }) => eq(todo.list_id, 'list_id'), // or some listId variable
)
.select(({ todo }) => ({
id: todo.id,
completed: todo.completed,
})),
})
6. 복합 조건자를 사용하는 주문형 모드
extractSimpleComparisons는 단순한 단일 조건 필터에 잘 작동하지만, 라이브 쿼리는 and, or 또는 기타 중첩 표현식을 사용하여 여러 조건을 결합하는 경우가 많습니다. parseWhereExpression를 사용하면 각 연산자(eq, and 등)에 대한 사용자 지정 핸들러 함수를 제공할 수 있습니다. 이를 통해 복합 표현식 트리를 풀고 syncStream()에 전달되는 매개변수 객체로 변환하는 방식을 완전히 제어할 수 있습니다. 동기화 스트림이 여러 매개변수를 허용하고 하나의 복합 where 절에서 모든 매개변수를 추출해야 할 때 유용합니다. 예를 들어 list_id와 completed 상태를 동시에 필터링할 수 있습니다.
todos의 동기화 스트림 정의에 completed 구독 매개변수를 추가하여 약간 조정한다고 가정합니다:
todos:
query: SELECT * FROM todos WHERE list_id = subscription.parameter('list') AND completed = subscription.parameter("completed") AND list_id IN (SELECT id FROM lists WHERE owner_id = auth.user_id())
list 매개변수 이름은 대부분의 예제와 일관되도록 그대로 유지되지만, 다음 예제에서 작동하려면 list_id에 매핑해야 합니다. 또는 동기화 스트림 정의에서 list_id로 이름을 지정하여 프로그래밍 방식의 매핑 단계를 생략할 수 있습니다.
이 예제는 PowerSync Service에 4개의 todo가 있는 상태에서 시작하며, 동기화 스트림 구독 기준(list_id = "list_1" and completed = 1)은 컬렉션에 등록된 라이브 쿼리에서 도출됩니다. 1개의 todo만 동기화 스트림을 통해 SQLite 데이터베이스로 동기화됩니다. 하나의 todo가 SQLite 데이터베이스에서 컬렉션으로 동기화됩니다. 마지막으로 TanStack DB 쿼리는 eq(todo.list_id, 'list_id') and eq(todo.completed, 1)와 일치하는 todo 1개를 반환합니다.
PS Service
| id | list_id | completed |
|---|---|---|
| 1 | list_1 | 0 |
| 2 | list_1 | 1 |
| 3 | list_2 | 0 |
| 4 | list_2 | 1 |
라이브 쿼리에서 list_id 및 completed 기준을 도출하고, list = 'list_1' AND completed = 1인 항목을 동기화 스트림으로 구독합니다
SQLite
| id | list_id | completed |
|---|---|---|
| 2 | list_1 | 1 |
주문형 모드는 라이브 쿼리 중 하나라도 만족하는 SQLite 데이터를 TanStack DB 컬렉션으로만 동기화합니다: list = 'list_1' AND completed = 1
컬렉션
| id | list_id | completed |
|---|---|---|
| 2 | list_1 | 1 |
TanStack DB 쿼리는 list = 'list_1' AND completed = 1에 대해 필터링합니다
결과
| id | list_id | completed |
|---|---|---|
| 2 | list_1 | 1 |
const collection = createCollection(
powerSyncCollectionOptions({
database: db,
table: AppSchema.props.todos,
syncMode: 'on-demand',
onLoadSubset: async (options) => {
// Parse the where into a flat params record using custom handlers
const streamParams = parseWhereExpression(options.where, {
handlers: {
eq: (field: Array<string>, value: unknown) => {
const mappedField = mapFields(field[field.length - 1]!)
return {
[mappedField]: value,
}
},
and: (...filters: Array<Record<string, unknown>>) =>
Object.assign({}, ...filters),
},
onUnknownOperator: (op, _args) => {
console.warn(`Ignoring unsupported operator in stream params: ${op}`)
return {}
},
})
// For a query like: where(({ todo }) => and(eq(todo.list_id, 'abc'), eq(todo.completed, 0)))
// streamParams = { list: 'abc', completed: 0 }
if (!streamParams || Object.keys(streamParams).length === 0) {
console.warn('No stream params extracted, skipping Sync Stream')
return
}
console.log(
`Subscribing to todos with params: ${JSON.stringify(streamParams)}`,
)
const subscription = await db
.syncStream('todos', streamParams)
.subscribe()
await subscription.waitForFirstSync()
return () => subscription.unsubscribe()
},
}),
)
// Compound filter -> triggers `onLoadSubset` with `and(eq(list_id, '...'), eq(completed, 1))`
const liveQuery = createLiveQueryCollection({
query: (q) =>
q
.from({ todo: collection })
.where(({ todo }) =>
and(eq(todo.list_id, 'list_1'), eq(todo.completed, 1)),
)
.select(({ todo }) => ({
id: todo.id,
completed: todo.completed,
})),
})