쿼리 컬렉션의 초기 데이터 및 플레이스홀더 데이터 의미
상태 및 범위
이 문서는 TanStack Query initialData 및
placeholderData의 @tanstack/query-db-collection 경계에서의 의미를 정의합니다. 이는
RFC #1643에 대한
설계 후속 작업이며 이슈 #346에 대한 내용입니다. 함께 제공되는
구현은 기존 동작을 변경하지 않고 승인된 eager initialData 동작을 추가합니다.
영속성 형식은 변경되지 않습니다.
이 어댑터는 서로 다른 두 모델을 연결합니다:
- TanStack Query는 문서 캐시와 원격 쿼리 수명 주기를 소유합니다.
- TanStack DB는 정규화된 행, 로컬 쿼리, 낙관적 쓰기를 소유합니다.
- 쿼리 컬렉션은 Query 응답을 행으로 투영하고 각 구체화된 행을 소유하는 Query 키를 기록합니다.
initialData 및 placeholderData을 배열을 제공하는 동등한 방법으로
취급해서는 안 됩니다. Query Core 5.90.20에서 initialData은 Query 캐시
상태를 status: "success" 및 dataUpdatedAt 타임스탬프로 초기화합니다. 반면,
placeholderData은 Query 상태가 보류 중일 때만 옵저버별로 계산됩니다.
이는 isPlaceholderData: true를 포함한 옵저버 결과를 생성하지만 저장되지는
않습니다.
결정
Query가 소유하는 initialData을 추가적인 eager 모드 옵션으로 지원합니다. 기존 행 추출 및 소유권 파이프라인을 통해 즉시 구체화합니다.
첫 번째 구현에서는 placeholderData을 노출하거나 구체화하지 않습니다.
쿼리 컬렉션은 옵저버가 생성되기 전에 QueryClient에 시드되거나 하이드레이션된 데이터를 이미 구체화할 수 있으며, QueryClient 기본값은 이미 initialData을 간접적으로 제공할 수 있습니다. 이는 유용한 기존 동작이지만 구성상의 공백을 해소하지는 못합니다. 애플리케이션은 일반적으로 여러 쿼리 컬렉션에서 하나의 QueryClient를 공유합니다. 클라이언트 전체 기본값은 너무 광범위하고, 명령형 setQueryData은 컬렉션 생성, 정확한 Query 키, 다른 곳의 초기화를 조정해야 합니다. 추가 필드는 컬렉션 로컬 선언을 제공하면서 Query를 캐시 권위자로 유지합니다.
최소 추가 API는 다음과 같습니다:
initialData?: TQueryData | (() => TQueryData)
initialDataUpdatedAt?: number | (() => number | undefined)
이 필드들은 최상위에 평평하게 유지됩니다. 해당 타입은 추출된 행 배열이 아니라 원래 Query 응답을 설명합니다. 따라서 래핑된 응답은 네트워크 응답과 동일한 어댑터 select을 사용합니다:
queryCollectionOptions({
queryKey: ["todos"],
queryFn: fetchTodos,
initialData: { items: serverTodos, nextCursor: null },
select: (response) => response.items,
// ...
})
Query 키는 계속 캐시 식별자를 정의합니다. 동일한 QueryClient의 두 쿼리 컬렉션이 동일한 정확한 키를 사용하면 하나의 공유 Query 문서를 관찰합니다. initialData은 해당 문서가 아직 존재하지 않을 때만 문서를 초기화합니다.
컬렉션 로컬 구성은 컬렉션 로컬 캐시 데이터를 생성하지 않으며, 이후 옵저버는 기존 문서를 초기화 값으로 대체해서는 안 됩니다.
독립적인 초기 문서가 필요한 컬렉션은 서로 다른 키를 사용해야 합니다.
이 초기 API는 syncMode: "eager"로 제한됩니다. 단일 구성 수준 값으로는 요청 시 생성되는 무한한 하위 집합 키 집합을 적법하게 초기화할 수 없으며, Query의 initialData 함수는 쿼리 키나 하위 집합 컨텍스트를 받지 않습니다.
정확한 요청 시 키의 데이터를 이미 알고 있는 애플리케이션은 대신 해당 Query 캐시 항목을 시드하거나 하이드레이션해야 합니다. 향후 하위 집합을 인식하는 초기화 함수에는 명시적인 키/하위 집합 인수와 별도의 설계가 필요합니다.
placeholderData은 계속 Query UI 용어로 남습니다. DB 컬렉션에는 옵저버 로컬 결과 표면이 없습니다. 플레이스홀더를 구체화하면 모든 DB 쿼리와 뮤테이션에 표시되고, 행 소유권이 할당되며, 잠재적으로 영속화됩니다. 호출자는 사용하는 UI에서 플레이스홀더를 렌더링해야 합니다. 향후 임시 행 기능이 필요하다면 Query의 placeholderData 옵션이 아니라 명시적인 출처와 수명 주기를 갖는 DB 기능이어야 합니다.
권위 모델
여기서 "권위"는 두 가지 차원을 가집니다. Query는 Query 키에 대한 권위 있는 문서를 소유하고, DB는 현재 정규화된 행 상태를 소유합니다. 서버 결과는 가장 최신 원격 스냅샷이지만, 로컬 낙관적 트랜잭션이 해당 행을 일시적으로 오버레이할 수 있습니다.
| 단계 | Query 문서 권위 | 구체화된 행 권위 | 결과 |
|---|---|---|---|
| 캐시된 데이터 없음 | 없음 | 기존 DB/영속 행(있는 경우) | 컬렉션은 Query를 기다리며, 없음은 빈 결과가 아닙니다. |
| 초기 데이터 | Query 캐시 initialData | 일반 로컬 오버레이가 적용되는 투영된 행 | 임시 표시 데이터가 아닌 실제 캐시된 스냅샷입니다. |
| 가져오기/다시 가져오기 진행 중 | 기존 Query 문서 | 기존 행 | 로딩은 행이나 소유권을 지우지 않습니다. |
| 서버 성공 | 반환된 응답이 Query 문서를 대체합니다 | 투영된 서버 행이 소유 Query 키와 조정됩니다 | 누락된 행은 이 Query 소유자의 리스를 잃고, 공유 행은 유지됩니다. |
| 가져오기/다시 가져오기 오류 | 마지막 성공 Query 문서 | 기존 행 | 오류가 초기 행이나 이전에 가져온 행을 철회하지 않습니다. |
| 플레이스홀더 표시 | Query 문서 없음 | 행 없음 | 플레이스홀더 데이터는 DB에 전달되지 않습니다. |
initialDataUpdatedAt 및 staleTime은 계속 Query가 소유합니다. 이들은 가져오기를 시작할지 결정하며, 어댑터는 이들의 신선도 계산을 재현하지 않습니다. 따라서 초기 값은 일반적인 Query 권위를 가진 시드 스냅샷이며, 승격을 기다리는 약한 행 유형이 아닙니다. 첫 번째 성공한 서버 결과는 이후의 모든 다시 가져오기와 동일한 경로로 이를 조정합니다.
동작 매트릭스
| 고려 사항 | initialData | placeholderData |
|---|---|---|
| Query 캐시 | 성공한 Query 데이터로 저장됨 | 저장되지 않으며 옵저버 전용임 |
| 기존 캐시 항목 | 기존 캐시/하이드레이션 데이터가 우선하며, initialData은 다시 적용되지 않음 | 해당 없음 |
| DB 구체화 | eager 모드에서 즉시 수행됨 | 수행되지 않음 |
| 래핑된 응답 | 어댑터 select(initialData)가 행을 추출하고 원래 봉투는 캐시됨 | 지원되지 않음 |
| 함수 값 | Query 생성 시 Query가 한 번 평가함 | 어댑터로 전달되거나 평가되지 않음 |
| 소유권 | 서버 성공과 동일하게 Query 키가 투영된 행을 소유함 | 소유권 없음 |
| 겹치는 하위 집합 | 초기 eager 전용 API에는 해당 없음 | 해당 없음 |
| 준비 상태 | 초기 성공 결과가 컬렉션을 동기적으로 준비 상태로 만들 수 있음 | 컬렉션을 준비 상태로 만들 수 없음 |
| 다시 가져오기 성공 | 추가, 업데이트, 제거를 정상적으로 조정함 | 해당 없음 |
| 다시 가져오기 오류 | 초기 행과 소유권이 유지되고 오류 상태가 보고됨 | 해당 없음 |
| 취소/정리 | 기존 Query-행 소유권 및 영속 보존 규칙이 적용됨 | 영향 없음 |
| Query 캐시 GC/언로드 | 기존 규칙이 적용됨 | 영향 없음 |
| Query 탈수화 | Query가 초기 응답의 영속성을 소유함 | 영속화되지 않음 |
| DB 영속성/하이드레이션 | 행과 소유자 메타데이터는 기존 형식을 사용하며 출처 태그는 추가되지 않음 | 영속화되거나 하이드레이션되지 않음 |
| 서버 성공 전 직접 쓰기 | 아래 기존 쓰기 규칙에 따라 허용됨 | 대상 행 없음 |
| QueryClient 기본값 | eager initialData에 지원됨. 아래 호환성 가드 참조 | 이 어댑터 경계에서 억제되어야 함 |
선택 및 쓰기
어댑터 select은 계속 단방향 행 추출기입니다. 초기 응답과 네트워크 응답에 동일하게 적용되며, TanStack Query는 원래 응답 형태를 유지합니다. Query 옵저버 수준의 select은 계속 지원되지 않습니다.
첫 번째 서버 결과 이전의 직접 쓰기는 현재 권위 규칙을 따릅니다. 즉시 DB를 업데이트하며, 역방향 업데이트가 적법한 경우에만 Query 캐시를 패치할 수 있습니다. 원시 배열은 대체할 수 있습니다. 래핑된 응답에서는 select이 캐시된 객체의 배열 속성을 참조로 반환하여 래퍼를 보존할 수 있을 때만 기존의 최선 노력 패치가 적법합니다. response.edges.map(...)과 같은 파생 투영에는 일반적인 역투영이 없으므로, 어댑터는 해당 Query 문서를 변경하지 않고 무효화 또는 다시 가져오기에 의존해야 합니다. 행을 감싸는 봉투를 임의로 생성해서는 안 됩니다.
다음에 성공한 원격 응답은 해당 Query 키에 대해 계속 권위를 가지며 직접 캐시 패치나 정규화된 행 값을 덮어쓸 수 있습니다. 뮤테이션 핸들러와 낙관적 트랜잭션 장벽은 기존 의미를 유지합니다.
소유권, 영속성 및 전환
초기 행은 기존 queryToRows 및 rowToQueries 관계를 사용합니다. 행에 seed, temporary 또는 placeholder 비트는 추가되지 않습니다. 따라서 다음 불변식이 유지됩니다:
- 초기 결과든 가져온 결과든 성공한 결과는 해당 Query 키에 대한 완전한 스냅샷입니다.
- 다른 Query 키가 행을 소유하지 않는 경우에만 스냅샷에서 누락된 행이 삭제됩니다.
- 언로드, 캐시 GC 및 컬렉션 정리는 관련 소유권만 제거합니다.
- 실패하거나 취소된 가져오기는 마지막 성공 스냅샷을 빈 스냅샷으로 바꿀 수 없습니다.
- 영속성에는 Query 데이터와 기존 행 소유권 메타데이터만 기록되며, 옵저버 전용 표시 상태는 영속화되지 않습니다.
예상되는 전환은 다음과 같습니다:
- 초기, 최신: 구체화하고 준비 상태가 됩니다. Query의 일반적인 신선도 트리거가 가져오기를 지시하기 전까지 가져오지 않습니다.
- 초기, 오래됨: Query가 가져오는 동안 구체화하고 준비 상태가 됩니다. 성공하면 동일한 소유권을 조정하고, 오류가 발생하면 시드를 유지합니다.
- 데이터 없이 로딩: 컬렉션이 독립적으로 소유하거나 하이드레이션한 이전 행을 유지하며, 빈 결과로 추론하지 않습니다.
- 플레이스홀더에서 로딩/성공/오류로: 플레이스홀더는 UI 전용입니다. 성공할 때까지 DB에는 전환이 보이지 않으며, 오류는 DB를 변경하지 않습니다.
- 네트워크 완료 전 정리: 기존 취소 및 준비 상태 리스너 정리를 적용합니다. 늦은 결과가 정리된 컬렉션을 변경해서는 안 됩니다.
기본값 및 호환성 가드
쿼리 컬렉션은 현재 QueryObserver을 생성하므로 QueryClient 기본값에 쿼리 컬렉션이 노출하지 않는 의미 필드가 포함될 수 있습니다. Query 기본값이 확인된 후 구현은 다음 설계를 명시적으로 적용해야 합니다:
- 요청 시 모드에서 명시적으로 구성된
initialData을 거부합니다. - 기본
initialData이 요청 시 하위 집합 옵저버를 초기화하지 못하도록 합니다. - 명시적 또는 기본
placeholderData이 모든 쿼리 컬렉션 옵저버에 도달하지 못하도록 합니다. - 생략된 eager
initialData및initialDataUpdatedAt이 QueryClient 기본값을 계속 상속하도록 합니다. - 함수 값 초기화 함수를 어댑터 메타데이터나 영속화된 소유권 메타데이터에 절대 복사하지 않습니다. Query Core는 이를 옵션으로 소유할 수 있으며 Query 상태에는 평가된 데이터만 저장합니다.
기본 플레이스홀더 데이터를 조용히 구체화하면 최상위 필드를 추가하기 전에도 공개 호환성 표를 위반합니다. 따라서 이 가드는 플레이스홀더 의미 체계 지원이 아니라 정확성을 위한 수정입니다.
다른 Query 소유 옵션은 현재 분류를 유지합니다. Query 키 생성, 어댑터 select, 구독, notifyOnChangeProps 및 구조적 공유는 어댑터가 소유하거나 재해석합니다. 이 설계에서는 중첩 queryOptions, 런타임 바인딩 API, 하위 집합 중복 제거 또는 리스 관리자를 도입하지 않습니다.
거부된 설계
- 두 옵션을 모두 기계적으로 전달합니다.
result.isSuccess은 플레이스홀더 옵저버 결과에 대해 true이므로, 현재 성공 핸들러가 표시 전용 데이터를 정규화하고 영속적인 것처럼 보이는 소유권을 부여하게 됩니다. - 플레이스홀더 행에 태그를 지정하고 나중에 승격하거나 삭제합니다. 태그는 실제 행과의 충돌, 겹치는 옵저버, 로컬 쓰기, 언로드, GC, 영속성 및 하이드레이션을 견뎌야 합니다. Query의 옵저버 로컬 플레이스홀더에는 이러한 전환을 안전하게 이끌 컬렉션 전체 수명 주기가 없습니다.
- 초기 행을 소유되지 않은 임시 행으로 취급합니다. Query는 초기 데이터를 실제 캐시된 데이터로 간주합니다. 일반 소유권을 우회하면 행이 유출되거나 한 Query의 정리가 다른 Query가 여전히 나타내는 행을 제거할 수 있습니다.
- 하나의 값으로 모든 요청 시 키를 시드합니다. 컬렉션 수준 초기화 함수는 임의의 조건자/순서/제한 하위 집합의 포함 여부를 입증할 수 없습니다.
- 선택한 행으로 래핑된 응답을 생성합니다. 읽기 투영은 역함수가 아닙니다. 메타데이터, 커서 또는 엣지를 임의로 생성하면 Query 캐시의 의미가 손상됩니다.
- 초기화 함수를 영속화합니다. 함수는 structured-clone에 안전하지 않으며 데이터가 아니라 런타임 구성입니다.
구현 및 테스트 순서
각 동작 PR은 이름이 지정된 실패 테스트 또는 특성화 테스트로 시작해야 합니다.
- 기존 경계를 보호합니다. 명시적 및 QueryClient 기본
placeholderData이 절대 구체화되지 않고, 컬렉션을 준비 상태로 만들지 않으며, 탈수화 후 데이터로 남지 않음을 입증하는 집중 테스트를 추가합니다. eager 및 요청 시 모드에서 현재 기본initialData동작을 특성화합니다. 그런 다음 옵저버를 생성할 때 플레이스홀더 및 요청 시 초기화를 억제합니다. - eager 초기 데이터를 추가합니다. 두 개의 평면 타입 필드를 추가하고 정의된 값만 전달하여 QueryClient 기본값을 유지합니다. 정적 및 함수 값, 최신 및 오래된 타임스탬프, 동기적 준비 상태, 가져오기 오류, 취소, 정리, 명시적 요청 시 구성 오류, 하나의 QueryClient를 사용하는 여러 컬렉션, 동일 키에서 첫 초기화 함수가 승리하는 동작을 테스트합니다.
- 투영과 쓰기를 고정합니다. 원시 배열, 직접 속성 래핑 응답 및 파생 투영을 테스트합니다. 전체 초기 봉투가 Query 캐시에 유지되고, 적법한 직접 쓰기가 이를 보존하며, 부적법한 역투영이 이를 임의 생성하지 않고, 서버 성공이 행을 조정하는지 확인합니다.
- 소유권을 고정합니다. 초기에서 서버 행 제거로의 전환, 외부에서 하이드레이션/영속화된 행과의 겹치는 소유권, GC/언로드,
gcTime내 재마운트 및 정리 후 늦은 알림을 테스트합니다. 기존 소유권 메커니즘을 재사용하며 두 번째 시드 소유권 맵은 추가하지 않습니다. - 영속성을 고정합니다. 초기 원시 및 래핑 데이터를 탈수화하고
structuredClone합니다. Query와 DB 상태를 하이드레이션하고, 소유권 조정 및 Query 메타데이터나 어댑터 영속성 메타데이터에 함수가 나타나지 않는지 확인합니다. - 제공된 API를 문서화합니다. 확정된 동작을 사용자 가이드의 Query 옵션, 행 추출, 직접 쓰기, 요청 시 및 영속성 섹션으로 옮깁니다. 이러한 계약이 제공된 후 RFC #1643을 업데이트하고 #346을 종료하거나 범위를 좁힙니다.
플레이스홀더 구체화, 하위 집합 인식 초기화 및 적법한 일반 역투영은 별도의 향후 제안입니다. 어느 것도 최소 eager initialData API의 전제 조건이 아닙니다.