본문으로 건너뛰기

LocalOnly 컬렉션

LocalOnly 컬렉션은 브라우저 세션 간에 영속화하거나 탭 간에 동기화할 필요가 없는 메모리 내 클라이언트 데이터 또는 UI 상태를 위해 설계되었습니다.

개요

localOnlyCollectionOptions을 사용하면 다음과 같은 컬렉션을 만들 수 있습니다:

  • 메모리에만 데이터를 저장합니다(영속성 없음)
  • 오류 발생 시 자동 롤백을 사용하는 낙관적 업데이트를 지원합니다
  • 선택적 초기 데이터를 제공합니다
  • 임시 UI 상태와 세션 전용 데이터에 완벽하게 작동합니다
  • 낙관적 상태에서 확인된 상태로의 전환을 자동으로 관리합니다

설치

LocalOnly 컬렉션은 핵심 TanStack DB 패키지에 포함되어 있습니다:

npm install @tanstack/react-db

기본 사용법

import { createCollection } from '@tanstack/react-db'
import { localOnlyCollectionOptions } from '@tanstack/react-db'

const uiStateCollection = createCollection(
localOnlyCollectionOptions({
id: 'ui-state',
getKey: (item) => item.id,
})
)

로컬 뮤테이션 직접 수행

중요: LocalOnly 컬렉션은 서버와 동기화되는 컬렉션과 다르게 작동합니다. LocalOnly 컬렉션에서는 collection.insert(), collection.update()collection.delete()와 같은 메서드를 호출하여 상태를 직접 뮤테이션하면 됩니다. 변경 사항은 로컬 메모리 내 데이터에 즉시 적용됩니다.

이는 서버와 동기화되는 컬렉션(Query Collection 등)과 다릅니다. 이러한 컬렉션에서는 뮤테이션 핸들러가 데이터를 백엔드로 전송합니다. LocalOnly 컬렉션에서는 모든 것이 로컬에 유지됩니다:

// Just call the methods directly - no server sync involved
uiStateCollection.insert({ id: 'theme', mode: 'dark' })
uiStateCollection.update('theme', (draft) => { draft.mode = 'light' })
uiStateCollection.delete('theme')

구성 옵션

localOnlyCollectionOptions 함수는 다음 옵션을 허용합니다:

필수 옵션

  • id: 컬렉션의 고유 식별자입니다
  • getKey: 항목에서 고유 키를 추출하는 함수입니다

선택적 옵션

  • schema: 클라이언트 측 검증을 위한 Standard Schema 호환 스키마입니다(예: Zod, Effect)
  • initialData: 생성 시 컬렉션에 채울 항목 배열입니다
  • onInsert: 삽입을 확인하기 전에 호출되는 선택적 핸들러 함수입니다
  • onUpdate: 업데이트를 확인하기 전에 호출되는 선택적 핸들러 함수입니다
  • onDelete: 삭제를 확인하기 전에 호출되는 선택적 핸들러 함수입니다

초기 데이터

생성 시 컬렉션에 초기 데이터를 채웁니다:

const uiStateCollection = createCollection(
localOnlyCollectionOptions({
id: 'ui-state',
getKey: (item) => item.id,
initialData: [
{ id: 'sidebar', isOpen: false },
{ id: 'theme', mode: 'light' },
{ id: 'modal', visible: false },
],
})
)

뮤테이션 핸들러

뮤테이션 핸들러는 완전히 선택 사항입니다. 제공된 경우 낙관적 상태가 확인되기 전에 호출됩니다:

const tempDataCollection = createCollection(
localOnlyCollectionOptions({
id: 'temp-data',
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
// Custom logic before confirming the insert
console.log('Inserting:', transaction.mutations[0].modified)
},
onUpdate: async ({ transaction }) => {
// Custom logic before confirming the update
const { original, modified } = transaction.mutations[0]
console.log('Updating from', original, 'to', modified)
},
onDelete: async ({ transaction }) => {
// Custom logic before confirming the delete
console.log('Deleting:', transaction.mutations[0].original)
},
})
)

수동 트랜잭션

수동 트랜잭션( createTransaction을 통해 생성)을 사용하여 LocalOnly 컬렉션을 작업할 때는 변경 사항을 영속화하기 위해 utils.acceptMutations()를 호출해야 합니다:

import { createTransaction } from '@tanstack/react-db'

const localData = createCollection(
localOnlyCollectionOptions({
id: 'form-draft',
getKey: (item) => item.id,
})
)

const serverCollection = createCollection(
queryCollectionOptions({
queryKey: ['items'],
queryFn: async () => api.items.getAll(),
getKey: (item) => item.id,
onInsert: async ({ transaction }) => {
await api.items.create(transaction.mutations[0].modified)
},
})
)

const tx = createTransaction({
mutationFn: async ({ transaction }) => {
// Handle server collection mutations explicitly in mutationFn
await Promise.all(
transaction.mutations
.filter((m) => m.collection === serverCollection)
.map((m) => api.items.create(m.modified))
)

// After server mutations succeed, accept local collection mutations
localData.utils.acceptMutations(transaction)
},
})

// Apply mutations to both collections in one transaction
tx.mutate(() => {
localData.insert({ id: 'draft-1', data: '...' })
serverCollection.insert({ id: '1', name: 'Item' })
})

await tx.commit()

완전한 예제: 모달 상태 관리

import { createCollection, eq } from '@tanstack/react-db'
import { localOnlyCollectionOptions } from '@tanstack/react-db'
import { useLiveQuery } from '@tanstack/react-db'
import { z } from 'zod'

// Define schema
const modalStateSchema = z.object({
id: z.string(),
isOpen: z.boolean(),
data: z.any().optional(),
})

type ModalState = z.infer<typeof modalStateSchema>

// Create collection
export const modalStateCollection = createCollection(
localOnlyCollectionOptions({
id: 'modal-state',
getKey: (item) => item.id,
schema: modalStateSchema,
initialData: [
{ id: 'user-profile', isOpen: false },
{ id: 'settings', isOpen: false },
{ id: 'confirm-delete', isOpen: false },
],
})
)

// Use in component
function UserProfileModal() {
const { data: modals } = useLiveQuery({
query: (q) =>
q
.from({ modal: modalStateCollection })
.where(({ modal }) => eq(modal.id, 'user-profile')),
})

const modalState = modals[0]

const openModal = (data?: any) => {
modalStateCollection.update('user-profile', (draft) => {
draft.isOpen = true
draft.data = data
})
}

const closeModal = () => {
modalStateCollection.update('user-profile', (draft) => {
draft.isOpen = false
draft.data = undefined
})
}

if (!modalState?.isOpen) return null

return (
<div className="modal">
<h2>User Profile</h2>
<pre>{JSON.stringify(modalState.data, null, 2)}</pre>
<button onClick={closeModal}>Close</button>
</div>
)
}

완전한 예제: 양식 초안 상태

import { createCollection, eq } from '@tanstack/react-db'
import { localOnlyCollectionOptions } from '@tanstack/react-db'
import { useLiveQuery } from '@tanstack/react-db'

type FormDraft = {
id: string
formData: Record<string, any>
lastModified: Date
}

// Create collection for form drafts
export const formDraftsCollection = createCollection(
localOnlyCollectionOptions({
id: 'form-drafts',
getKey: (item) => item.id,
})
)

// Use in component
function CreatePostForm() {
const { data: drafts } = useLiveQuery({
query: (q) =>
q
.from({ draft: formDraftsCollection })
.where(({ draft }) => eq(draft.id, 'new-post')),
})

const currentDraft = drafts[0]

const updateDraft = (field: string, value: any) => {
if (currentDraft) {
formDraftsCollection.update('new-post', (draft) => {
draft.formData[field] = value
draft.lastModified = new Date()
})
} else {
formDraftsCollection.insert({
id: 'new-post',
formData: { [field]: value },
lastModified: new Date(),
})
}
}

const clearDraft = () => {
if (currentDraft) {
formDraftsCollection.delete('new-post')
}
}

const submitForm = async () => {
if (!currentDraft) return

await api.posts.create(currentDraft.formData)
clearDraft()
}

return (
<form onSubmit={(e) => { e.preventDefault(); submitForm() }}>
<input
value={currentDraft?.formData.title || ''}
onChange={(e) => updateDraft('title', e.target.value)}
/>
<button type="submit">Publish</button>
<button type="button" onClick={clearDraft}>Clear Draft</button>
</form>
)
}

사용 사례

LocalOnly 컬렉션은 다음에 적합합니다:

  • 임시 UI 상태(모달, 사이드바, 툴팁)
  • 현재 세션 동안의 양식 초안 데이터
  • 클라이언트 측 계산 또는 파생 데이터
  • 마법사/다단계 양식 상태
  • 임시 필터 또는 검색 상태
  • 메모리 내 캐시

LocalStorageCollection과의 비교

기능LocalOnlyLocalStorage
영속성없음(메모리 내에서만)localStorage
탭 간 동기화아니요
페이지 새로고침 후 유지아니요
성능가장 빠름빠름
크기 제한메모리 제한약 5~10MB
적합한 용도임시 UI 상태사용자 기본 설정

자세히 알아보기