오류 처리
TanStack DB는 견고한 데이터 동기화와 상태 관리를 보장하기 위한 포괄적인 오류 처리 기능을 제공합니다. 이 가이드에서는 기본 제공 오류 처리 메커니즘과 이를 효과적으로 사용하는 방법을 다룹니다.
오류 유형
TanStack DB는 더 나은 오류 처리와 타입 안전성을 위해 이름이 지정된 오류 클래스를 제공합니다. 모든 오류 클래스는 @tanstack/db에서 가져올 수 있습니다(또는 더 일반적으로 프레임워크별 패키지(예: @tanstack/react-db)에서 가져옵니다):
import {
SchemaValidationError,
CollectionInErrorStateError,
DuplicateKeyError,
MissingHandlerError,
TransactionError,
// ... and many more
} from "@tanstack/db"
SchemaValidationError
삽입 또는 업데이트 작업 중 데이터가 컬렉션의 스키마와 일치하지 않으면 발생합니다:
import { SchemaValidationError } from "@tanstack/db"
try {
todoCollection.insert({ text: 123 }) // Invalid type
} catch (error) {
if (error instanceof SchemaValidationError) {
console.log(error.type) // 'insert' or 'update'
console.log(error.issues) // Array of validation issues
// Example issue: { message: "Expected string, received number", path: ["text"] }
}
}
오류에는 다음이 포함됩니다:
type: 'insert' 또는 'update' 작업인지 여부issues: 메시지와 경로가 포함된 검증 문제의 배열message: 모든 문제를 나열하는 서식이 지정된 오류 메시지
스키마 검증이 발생하는 경우:
스키마 검증은 클라이언트 뮤테이션에서만, 즉 데이터를 명시적으로 삽입하거나 업데이트할 때 수행됩니다:
- 삽입 중 -
collection.insert()이 호출될 때 - 업데이트 중 -
collection.update()이 호출될 때
스키마는 서버 또는 동기화 계층에서 들어오는 데이터를 검증하지 않습니다. 해당 데이터는 이미 유효한 것으로 간주됩니다.
const schema = z.object({
id: z.string(),
created_at: z.string().transform(val => new Date(val))
// TInput: string, TOutput: Date
})
// Validation happens here ✓
collection.insert({
id: "1",
created_at: "2024-01-01" // TInput: string
})
// If successful, stores: { created_at: Date } // TOutput: Date
스키마 검증과 타입 변환에 대한 자세한 내용은 스키마 가이드를 참조할 수 있습니다.
Query Collection 오류 추적
Query Collection은 utils 객체를 통해 향상된 오류 추적 유틸리티를 제공합니다. 이러한 메서드는 오류 상태 정보를 노출하고 실패한 쿼리에 대한 복구 메커니즘을 제공합니다:
import { createCollection } from "@tanstack/db"
import { queryCollectionOptions } from "@tanstack/query-db-collection"
import { useLiveQuery } from "@tanstack/react-db"
const syncedCollection = createCollection(
queryCollectionOptions({
queryClient,
queryKey: ['synced-data'],
queryFn: fetchData,
getKey: (item) => item.id,
})
)
// Component can check error state
function DataList() {
const { data } = useLiveQuery({
query: (q) => q.from({ item: syncedCollection }),
})
const isError = syncedCollection.utils.isError
const errorCount = syncedCollection.utils.errorCount
return (
<>
{isError && errorCount > 3 && (
<Alert>
Unable to sync. Showing cached data.
<button onClick={() => syncedCollection.utils.clearError()}>
Retry
</button>
</Alert>
)}
{/* Render data */}
</>
)
}
오류 추적 메서드:
lastError: 쿼리에서 발생한 가장 최근 오류를 반환하거나, 오류가 발생하지 않았으면undefined을 반환합니다:isError: 컬렉션이 현재 오류 상태인지 나타내는 불리언을 반환합니다:errorCount: 연속된 동기화 실패 횟수를 반환합니다. 이 카운터는 쿼리가 완전히 실패할 때만 증가하며(재시도 시도마다 증가하지 않음), 쿼리가 성공하면 재설정됩니다:clearError(): 오류 상태를 지우고 쿼리 다시 가져오기를 트리거합니다. 이 메서드는lastError과errorCount을 모두 재설정합니다:
증분 서브셋 로드 오류
증분 loadSubset 실패가 발생해도 이미
사용 가능한 행을 삭제하거나 공유 소스 컬렉션을 error 상태로 전환하지 않습니다. 이 실패는 해당 서브셋을 요청한
구독에 속합니다:
const subscription = todoCollection.subscribeChanges(handleChanges, {
includeInitialState: false,
})
subscription.on('loadSubset:error', ({ error, options }) => {
console.error('Subset failed', options, error)
})
subscription.requestSnapshot()
// The most recent failure remains available for diagnostics.
console.log(subscription.lastError)
순서가 지정된 라이브 쿼리에서는 utils.setWindow()이 동일한 오류로 거부됩니다. 마지막
실패는 utils.lastSubsetError으로도 확인할 수 있으며, 마지막
성공한 스냅샷은 계속 읽을 수 있습니다:
try {
await liveTodos.utils.setWindow({ offset: 0, limit: 100 })
} catch (error) {
console.error(liveTodos.utils.lastSubsetError)
}
이펙트는 onSourceError을 통해 서브셋 실패를 보고하고, 증분 결과를 더 이상 완전하게 유지할 수 없으므로 삭제됩니다.
반드시 다시 가져와야 하는 truncate에서 모든 활성 서브셋을 다시 로드할 수 없으면 구독은 마지막으로 성공한 스냅샷을 유지하고 서브셋 오류를 보고합니다. 불완전한 재생 배치를 삭제한 다음 일반 소스 변경 사항 게시를 재개합니다. 다음 truncate에서는 모든 활성 서브셋을 다시 시도합니다. 겹치는 truncate는 하나의 원자적 재생을 형성합니다. 모든 진행 중인 요청이 완료되거나 실패하고, 가장 최신 시도가 결과를 결정하며, 해당 시도가 성공한 경우에만 구독자에게 대체 결과를 전달합니다.
컬렉션 상태 및 오류 상태
컬렉션은 상태를 추적하고 상태 간에 전환됩니다:
import { useLiveQuery } from "@tanstack/react-db"
const TodoList = () => {
const { data, status, isError, isLoading, isReady } = useLiveQuery(
(query) => query.from({ todos: todoCollection })
)
if (isError) {
return <div>Collection is in error state</div>
}
if (isLoading) {
return <div>Loading...</div>
}
return <div>{data?.map(todo => <div key={todo.id}>{todo.text}</div>)}</div>
}
컬렉션 상태 값:
idle- 아직 시작되지 않음loading- 초기 데이터 로드 중initialCommit- 초기 데이터 처리 중ready- 사용할 준비가 됨error- 오류 상태cleaned-up- 정리되어 더 이상 사용할 수 없음
Suspense 및 오류 경계 사용(React)
React 애플리케이션에서는 useLiveSuspenseQuery, React Suspense 및 오류 경계를 사용하여 로딩 및 오류 상태를 처리할 수 있습니다:
import { useLiveSuspenseQuery } from "@tanstack/react-db"
import { Suspense } from "react"
import { ErrorBoundary } from "react-error-boundary"
const TodoList = () => {
// No need to check status - Suspense and ErrorBoundary handle it
const { data } = useLiveSuspenseQuery(
(query) => query.from({ todos: todoCollection })
)
// data is always defined here
return <div>{data.map(todo => <div key={todo.id}>{todo.text}</div>)}</div>
}
const App = () => (
<ErrorBoundary fallback={<div>Failed to load todos</div>}>
<Suspense fallback={<div>Loading...</div>}>
<TodoList />
</Suspense>
</ErrorBoundary>
)
이 접근 방식을 사용하면 컴포넌트 로직 내부가 아니라 <Suspense>가 로딩 상태를, <ErrorBoundary>이 오류 상태를 처리합니다. 자세한 내용은 라이브 쿼리의 React Suspense 섹션을 참조할 수 있습니다.
트랜잭션 오류 처리
뮤테이션이 실패하면 TanStack DB가 낙관적 업데이트를 자동으로 롤백합니다:
const todoCollection = createCollection({
id: "todos",
onInsert: async ({ transaction }) => {
const response = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(transaction.mutations[0].modified),
})
if (!response.ok) {
// Throwing an error will rollback the optimistic state
throw new Error(`HTTP Error: ${response.status}`)
}
return response.json()
},
})
// Usage - optimistic update will be rolled back if the mutation fails
try {
const tx = todoCollection.insert({
id: "1",
text: "New todo",
completed: false,
})
await tx.isPersisted.promise
} catch (error) {
// The optimistic update has been automatically rolled back
console.error("Failed to create todo:", error)
}
트랜잭션 상태 및 오류 정보
트랜잭션에는 다음 상태가 있습니다:
pending- 트랜잭션 처리 중persisting- 뮤테이션 함수 실행 중completed- 트랜잭션이 성공적으로 완료됨failed- 트랜잭션이 실패하고 롤백됨
컬렉션 작업에서 트랜잭션 오류 정보에 액세스합니다:
const todoCollection = createCollection({
id: "todos",
onUpdate: async ({ transaction }) => {
const response = await fetch(`/api/todos/${transaction.mutations[0].key}`, {
method: "PUT",
body: JSON.stringify(transaction.mutations[0].modified),
})
if (!response.ok) {
throw new Error(`Update failed: ${response.status}`)
}
},
})
try {
const tx = await todoCollection.update("todo-1", (draft) => {
draft.completed = true
})
await tx.isPersisted.promise
} catch (error) {
// Transaction has been rolled back
console.log(tx.state) // "failed"
console.log(tx.error) // { message: "Update failed: 500", error: Error }
}
또는 수동으로 트랜잭션을 생성합니다:
const tx = createTransaction({
mutationFn: async ({ transaction }) => {
throw new Error("API failed")
}
})
tx.mutate(() => {
collection.insert({ id: "1", text: "Item" })
})
try {
await tx.commit()
} catch (error) {
// Transaction has been rolled back
console.log(tx.state) // "failed"
console.log(tx.error) // { message: "API failed", error: Error }
}
컬렉션 작업 오류
잘못된 컬렉션 상태
error 상태의 컬렉션은 작업을 수행할 수 없으며 수동으로 복구해야 합니다:
import { CollectionInErrorStateError } from "@tanstack/db"
try {
todoCollection.insert(newTodo)
} catch (error) {
if (error instanceof CollectionInErrorStateError) {
// Collection needs to be cleaned up and restarted
await todoCollection.cleanup()
// Now retry the operation
todoCollection.insert(newTodo)
}
}
뮤테이션 핸들러 누락
직접 뮤테이션을 사용하려면 핸들러를 구성해야 합니다:
const todoCollection = createCollection({
id: "todos",
getKey: (todo) => todo.id,
// Missing onInsert handler
})
// This will throw an error
todoCollection.insert(newTodo)
// Error: Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured
삽입 작업 오류
DuplicateKeyError
기존 키가 있는 항목을 삽입할 때 발생합니다:
import { DuplicateKeyError } from "@tanstack/db"
try {
todoCollection.insert({ id: "existing-id", text: "Todo" })
} catch (error) {
if (error instanceof DuplicateKeyError) {
console.log(`Duplicate key: ${error.message}`)
// Consider using update() instead, or check if item exists first
}
}
UndefinedKeyError
정의된 키 없이 객체를 생성할 때 발생합니다:
import { UndefinedKeyError } from "@tanstack/db"
const collection = createCollection({
id: "todos",
getKey: (item) => item.id,
})
try {
collection.insert({ text: "Todo" }) // Missing 'id' field
} catch (error) {
if (error instanceof UndefinedKeyError) {
console.log("Item is missing required key field")
// Ensure your items have the key field defined by getKey
}
}
업데이트 작업 오류
UpdateKeyNotFoundError
컬렉션에 존재하지 않는 키를 업데이트하려고 할 때 발생합니다:
import { UpdateKeyNotFoundError } from "@tanstack/db"
try {
todoCollection.update("nonexistent-key", draft => {
draft.completed = true
})
} catch (error) {
if (error instanceof UpdateKeyNotFoundError) {
console.log("Key not found - item may have been deleted")
// Consider using insert() if the item doesn't exist
}
}
KeyUpdateNotAllowedError
항목의 키를 변경하려고 할 때 발생합니다(허용되지 않으므로 대신 삭제 후 다시 삽입해야 합니다):
import { KeyUpdateNotAllowedError } from "@tanstack/db"
try {
todoCollection.update("todo-1", draft => {
draft.id = "todo-2" // Not allowed!
})
} catch (error) {
if (error instanceof KeyUpdateNotAllowedError) {
console.log("Cannot change item keys")
// Instead, delete the old item and insert a new one
}
}
삭제 작업 오류
DeleteKeyNotFoundError
존재하지 않는 키를 삭제하려고 할 때 발생합니다:
import { DeleteKeyNotFoundError } from "@tanstack/db"
try {
todoCollection.delete("nonexistent-key")
} catch (error) {
if (error instanceof DeleteKeyNotFoundError) {
console.log("Key not found - item may have already been deleted")
// This may be acceptable in some scenarios (idempotent deletes)
}
}
동기화 오류 처리
쿼리 컬렉션 동기화 오류
쿼리 컬렉션은 초기 로드 실패와 이후 재가져오기 실패를 구분합니다:
import { queryCollectionOptions } from "@tanstack/query-db-collection"
const todoCollection = createCollection(
queryCollectionOptions({
queryKey: ["todos"],
queryFn: async () => {
const response = await fetch("/api/todos")
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status}`)
}
return response.json()
},
queryClient,
getKey: (item) => item.id,
schema: todoSchema,
// Standard TanStack Query error handling options
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
})
)
동기화 오류가 발생하면 다음과 같이 처리됩니다:
- 오류가 콘솔에 기록됩니다:
[QueryCollection] Error observing query... - 사용할 수 있는 스냅샷이 없으므로 초기 실패 시 컬렉션이
error으로 표시됩니다 - 컬렉션이 초기 오류 상태인 동안
preload()및toArrayWhenReady()와 같은 준비 대기는markError(error)에 전달된 원인과 함께 거부됩니다 - 이후 재가져오기 실패가 발생해도 컬렉션은
ready상태로 유지되며 캐시된 데이터가 보존됩니다 - 오류 추적 카운터가 업데이트됩니다(
lastError,errorCount) - 이후 재가져오기가 성공하면 초기
error컬렉션이ready로 복구되며, 새로운 준비 대기는 정상적으로 해결됩니다
동기화 쓰기 오류
동기화 함수는 쓰기 작업 중 발생하는 자체 오류를 처리해야 합니다:
const collection = createCollection({
id: "todos",
sync: {
sync: ({ begin, write, commit }) => {
begin()
try {
// Will throw if key already exists
write({ type: "insert", value: { id: "existing-id", text: "Todo" } })
} catch (error) {
// Error: Cannot insert document with key "existing-id" from sync because it already exists
}
commit()
}
}
})
정리 오류 처리
정리 오류는 정리 프로세스가 차단되지 않도록 격리됩니다:
const collection = createCollection({
id: "todos",
sync: {
sync: ({ begin, commit }) => {
begin()
commit()
// Return a cleanup function
return () => {
// If this throws, the error is re-thrown in a microtask
// but cleanup continues successfully
throw new Error("Sync cleanup failed")
}
},
},
})
// Cleanup completes even if the sync cleanup function throws
await collection.cleanup() // Resolves successfully
// Error is re-thrown asynchronously via queueMicrotask
오류 복구 패턴
컬렉션 정리 및 재시작
오류 상태의 컬렉션을 정리합니다:
if (todoCollection.status === "error") {
// Cleanup will stop sync and reset the collection
await todoCollection.cleanup()
// Collection will automatically restart on next access
todoCollection.preload() // Or any other operation
}
정상적인 기능 저하
동기화에 실패해도 컬렉션은 캐시된 데이터로 계속 작동합니다:
const TodoApp = () => {
const { data, isError } = useLiveQuery((query) =>
query.from({ todos: todoCollection })
)
return (
<div>
{isError && (
<div>Sync failed, but you can still view cached data</div>
)}
{data?.map(todo => <TodoItem key={todo.id} todo={todo} />)}
</div>
)
}
트랜잭션 롤백 연쇄
트랜잭션이 실패하면 충돌하는 트랜잭션이 자동으로 롤백됩니다:
const tx1 = createTransaction({ mutationFn: async () => {} })
const tx2 = createTransaction({ mutationFn: async () => {} })
tx1.mutate(() => collection.update("1", draft => { draft.value = "A" }))
tx2.mutate(() => collection.update("1", draft => { draft.value = "B" })) // Same item
// Rolling back tx1 will also rollback tx2 due to conflict
tx1.rollback() // tx2 is automatically rolled back
트랜잭션 수명 주기 오류
트랜잭션은 오용을 방지하기 위해 작업 전에 상태를 검증합니다. 다음은 발생할 수 있는 구체적인 오류입니다:
MissingMutationFunctionError
필수 mutationFn 없이 트랜잭션을 생성할 때 발생합니다:
import { MissingMutationFunctionError } from "@tanstack/db"
try {
const tx = createTransaction({}) // Missing mutationFn
} catch (error) {
if (error instanceof MissingMutationFunctionError) {
console.log("mutationFn is required when creating a transaction")
}
}
TransactionNotPendingMutateError
트랜잭션이 더 이상 대기 중이 아닌 상태에서 mutate()을 호출할 때 발생합니다:
import { TransactionNotPendingMutateError } from "@tanstack/db"
const tx = createTransaction({ mutationFn: async () => {} })
await tx.commit()
try {
tx.mutate(() => {
collection.insert({ id: "1", text: "Item" })
})
} catch (error) {
if (error instanceof TransactionNotPendingMutateError) {
console.log("Cannot mutate - transaction is no longer pending")
}
}
TransactionNotPendingCommitError
트랜잭션이 더 이상 대기 중이 아닌 상태에서 commit()를 호출할 때 발생합니다:
import { TransactionNotPendingCommitError } from "@tanstack/db"
const tx = createTransaction({ mutationFn: async () => {} })
tx.mutate(() => collection.insert({ id: "1", text: "Item" }))
await tx.commit()
try {
await tx.commit() // Trying to commit again
} catch (error) {
if (error instanceof TransactionNotPendingCommitError) {
console.log("Transaction already committed")
}
}
TransactionAlreadyCompletedRollbackError
이미 완료된 트랜잭션에서 rollback()를 호출할 때 발생합니다:
import { TransactionAlreadyCompletedRollbackError } from "@tanstack/db"
const tx = createTransaction({ mutationFn: async () => {} })
tx.mutate(() => collection.insert({ id: "1", text: "Item" }))
await tx.commit()
try {
tx.rollback() // Can't rollback after commit
} catch (error) {
if (error instanceof TransactionAlreadyCompletedRollbackError) {
console.log("Cannot rollback - transaction already completed")
}
}
동기화 트랜잭션 오류
동기화 트랜잭션을 사용할 때 다음 오류가 발생할 수 있습니다:
NoPendingSyncTransactionWriteError
활성 동기화 트랜잭션 없이 write()을 호출할 때 발생합니다:
const collection = createCollection({
id: "todos",
sync: {
sync: ({ write }) => {
// Calling write without begin() first
write({ type: "insert", value: { id: "1", text: "Todo" } })
// Error: No pending sync transaction to write to
}
}
})
SyncTransactionAlreadyCommittedWriteError
동기화 트랜잭션이 이미 커밋된 후 write()을 호출할 때 발생합니다:
const collection = createCollection({
id: "todos",
sync: {
sync: ({ begin, write, commit }) => {
begin()
commit()
// Trying to write after commit
write({ type: "insert", value: { id: "1", text: "Todo" } })
// Error: The pending sync transaction is already committed
}
}
})
NoPendingSyncTransactionCommitError
활성 동기화 트랜잭션 없이 commit()을 호출할 때 발생합니다.
SyncTransactionAlreadyCommittedError
이미 커밋된 동기화 트랜잭션에서 commit()를 호출할 때 발생합니다.
모범 사례
-
instanceof 검사 사용 - 오류 처리에서 문자열 비교 대신
instanceof을 사용합니다:// ✅ Good - type-safe error handling
if (error instanceof SchemaValidationError) {
// Handle validation error
}
// ❌ Avoid - brittle string matching
if (error.message.includes("validation failed")) {
// Handle validation error
} -
특정 오류 유형 가져오기 - 트리 셰이킹을 개선하려면 필요한 오류 클래스만 가져옵니다
-
항상 SchemaValidationError 처리 - 유효성 검사 실패에 대한 명확한 피드백을 제공합니다
-
컬렉션 상태 확인 - React 컴포넌트에서
isError,isLoading,isReady플래그를 사용합니다 -
트랜잭션 프로미스 처리 - 항상
isPersisted.promise거부를 처리합니다
예제: 완전한 오류 처리
import {
createCollection,
SchemaValidationError,
DuplicateKeyError,
UpdateKeyNotFoundError,
DeleteKeyNotFoundError,
TransactionNotPendingCommitError,
createTransaction
} from "@tanstack/db"
import { useLiveQuery } from "@tanstack/react-db"
const todoCollection = createCollection({
id: "todos",
schema: todoSchema,
getKey: (todo) => todo.id,
onInsert: async ({ transaction }) => {
const response = await fetch("/api/todos", {
method: "POST",
body: JSON.stringify(transaction.mutations[0].modified),
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return response.json()
},
sync: {
sync: ({ begin, write, commit }) => {
// Your sync implementation
begin()
// ... sync logic
commit()
}
}
})
const TodoApp = () => {
const { data, status, isError, isLoading } = useLiveQuery(
(query) => query.from({ todos: todoCollection })
)
const handleAddTodo = async (text: string) => {
try {
const tx = await todoCollection.insert({
id: crypto.randomUUID(),
text,
completed: false,
})
// Wait for persistence
await tx.isPersisted.promise
} catch (error) {
if (error instanceof SchemaValidationError) {
alert(`Validation error: ${error.issues[0]?.message}`)
} else if (error instanceof DuplicateKeyError) {
alert("A todo with this ID already exists")
} else {
alert(`Failed to add todo: ${error.message}`)
}
}
}
const handleCleanup = async () => {
try {
await todoCollection.cleanup()
// Collection will restart on next access
} catch (error) {
console.error("Cleanup failed:", error)
}
}
if (isError) {
return (
<div>
<div>Collection error - data may be stale</div>
<button onClick={handleCleanup}>
Restart Collection
</button>
</div>
)
}
if (isLoading) {
return <div>Loading todos...</div>
}
return (
<div>
<button onClick={() => handleAddTodo("New todo")}>
Add Todo
</button>
{data?.map(todo => (
<div key={todo.id}>{todo.text}</div>
))}
</div>
)
}
함께 보기
- API 참조 - 자세한 API 문서
- 뮤테이션 가이드 - 낙관적 업데이트와 롤백에 대해 알아봅니다
- TanStack Query 오류 처리 - 쿼리별 오류 처리