~/web-app-dev.md
WEB_APP_DEV

TanStack DB와 Supabase 연결은 캐시가 아니라 클라이언트 데이터 그래프 설계다

10분 읽기·2026.07.28·출처 5·00
오늘의 질문

서버 상태 라이브러리를 하나 더 붙이면 자동으로 실시간 제품 경험이 좋아질까?

web-app-dev.md
Client data graph

TanStack DB와 Supabase 연결은 캐시가 아니라 클라이언트 데이터 그래프 설계다

1. 왜 지금 봐야 하나

Supabase의 2026년 7월 Developer Update에는 @supabase-labs/tanstack-db가 알파로 소개됐다. 설명은 짧다. TanStack DB collection을 Supabase table과 PostgREST, Realtime 위에서 동기화한다는 것이다. 하지만 웹앱 개발자 관점에서는 “또 하나의 상태 관리 도구”보다 큰 신호다.

지금 많은 제품은 Supabase 같은 BaaS 위에서 빠르게 만들어진다. 처음에는 supabase.from('todos').select()와 React Query 캐시만으로 충분하다. 그런데 제품이 조금만 복잡해지면 문제가 바뀐다.

  • 같은 table을 여러 화면에서 다른 조건으로 읽는다.
  • optimistic update를 넣고 싶지만 rollback, 중복 refetch, 다른 사용자의 변경 반영이 헷갈린다.
  • join, filter, sort, aggregate 결과를 컴포넌트마다 다시 계산한다.
  • Realtime을 붙였는데 어떤 변경을 어느 캐시에 반영해야 할지 수동 배관이 늘어난다.
  • RLS와 Auth는 서버에서 계속 맞아야 하지만, 클라이언트도 “내가 가진 데이터 그래프”를 일관되게 유지해야 한다.

TanStack DB는 이 문제를 “요청 캐시”가 아니라 typed collection과 live query로 구성된 클라이언트 데이터 그래프로 본다. Supabase 어댑터는 그 그래프의 원천을 Postgres table, PostgREST 요청, Realtime change로 연결하려는 시도다.

오늘의 핵심 문장: Supabase + TanStack DB의 핵심은 fetch를 감추는 것이 아니라, 서버가 진실이고 클라이언트는 그 진실의 부분 그래프를 materialize한다는 계약을 명확히 하는 것이다.

2. 핵심 개념

React Query 같은 서버 상태 라이브러리는 보통 “요청 단위”로 생각한다. ['todos'], ['todos', projectId], ['todo', id] 같은 query key가 있고, 응답을 캐시한다. 화면은 이 캐시를 읽고, mutation 뒤에는 invalidate/refetch를 한다.

TanStack DB는 한 단계 다른 모델을 제안한다.

  1. Collection: typed row들의 집합이다. 예를 들어 todos collection은 Supabase의 public.todos table에 대응할 수 있다.
  2. Live query: 컴포넌트가 collection 위에서 where, join, orderBy, select를 선언한다. collection 데이터가 바뀌면 query 결과가 반응형으로 갱신된다.
  3. Optimistic mutation: collection에 먼저 insert/update/delete를 적용하고, 서버가 거절하면 rollback한다.
  4. Sync strategy: 데이터를 언제 얼마나 가져올지 정한다. TanStack DB 문서는 eager, on-demand, progressive sync mode를 구분한다.

Supabase 어댑터 README에 나온 기본 형태는 이렇다.

zsh — 생존확인.sh
import { createCollection } from '@tanstack/react-db'
import { supabaseCollectionOptions } from '@supabase-labs/tanstack-db'

const todos = createCollection(
  supabaseCollectionOptions({
    tableName: 'todos',
    schema: todosSchema,
    keys: ['id'],
    supabase,
    realtime: true,
  }),
)

여기서 중요한 필드는 tableName, schema, keys, realtime이다.

  • tableName은 PostgREST endpoint와 연결된다.
  • schema는 row 모양을 검증하고 타입을 만든다.
  • keys는 collection 안에서 row를 구분하는 기준이다. 실제 primary key와 맞아야 한다.
  • realtime: true는 다른 클라이언트가 바꾼 row를 Postgres change subscription으로 받아 collection에 합치는 옵션이다.

즉 이 모델에서는 “컴포넌트가 API를 호출한다”보다 “컴포넌트가 collection 위에 필요한 view를 선언한다”가 먼저다.

zsh — 생존확인.sh
const { data } = useLiveQuery((q) =>
  q
    .from({ todo: todos })
    .where(({ todo }) => eq(todo.completed, false))
    .orderBy(({ todo }) => todo.priority, 'desc'),
)

이 코드의 공부 포인트는 문법이 아니다. 클라이언트가 더 이상 activeTodos, sortedTodos, todoCount 같은 파생 상태를 컴포넌트마다 흩뿌리지 않고, collection 위 live query로 표현한다는 점이다.

3. 최신 이슈와 연결

이번 Supabase 어댑터는 아직 알파다. README는 Realtime integration이 안정화 중이며 예상보다 많은 Realtime message를 소비할 수 있다고 경고한다. 그래서 free plan 또는 spend cap이 있는 조직에서 테스트하고, 필요하면 collection별로 realtime: false를 설정하라고 안내한다.

이 경고가 중요하다. 실시간 동기화는 “켜면 UX가 좋아지는 옵션”이 아니다. 비용과 권한과 일관성의 계약이다.

첫째, 비용 계약이다. Realtime을 table 단위로 켜면 클라이언트는 변경 이벤트를 받는다. 작은 todo 앱에서는 편하지만, 이벤트가 많은 table에서는 message 사용량이 비용과 rate limit으로 돌아온다. 모든 collection을 실시간으로 만드는 대신, 협업·알림·진행 상태처럼 “다른 사용자의 변경이 즉시 보여야 제품 가치가 생기는 table”만 골라야 한다.

둘째, 권한 계약이다. Supabase 어댑터는 PostgREST와 Realtime을 사용하므로 기존 RLS가 적용된다고 설명한다. 반대로 RLS 없이 Realtime을 쓰면 모든 변경이 모든 client로 broadcast될 수 있다고 경고한다. 즉 collection을 예쁘게 만들기 전에 table의 RLS policy와 Realtime publication 노출 범위를 먼저 확인해야 한다.

셋째, SSR 계약이다. README는 TanStack DB가 major frontend framework를 지원하지만 아직 server-side rendering을 지원하지 않아 collection fetch가 client side에서 일어난다고 적는다. Next.js나 TanStack Start에서 초기 HTML에 어떤 데이터를 담을지, 이후 클라이언트 collection이 언제 hydrate 또는 fetch할지 분리해서 설계해야 한다. 서버 컴포넌트에서 한 번만 읽을 데이터라면 어댑터의 queryOnce나 기존 server-side Supabase client가 더 맞을 수 있다.

넷째, 초기 데이터 계약이다. TanStack DB의 @tanstack/query-db-collection@1.2.0 릴리스는 eager collection에서 TanStack Query의 initialDatainitialDataUpdatedAt 지원을 추가했다. 동시에 QueryClient 기본 placeholderData는 collection row로 materialize되지 않고, 기본 initialData도 on-demand subset observer를 seed하지 않는다고 명시했다. 이건 “placeholder로 화면을 채우는 것”과 “collection 안의 authoritative row로 삼는 것”을 구분하라는 의미다.

4. 개발자 관점 해석

Supabase + TanStack DB를 도입할 때 가장 위험한 오해는 “React Query를 대체하면 캐시 무효화가 사라진다”다. 실제로는 질문이 바뀐다.

4-1. Query key보다 collection identity가 먼저다

TanStack DB Query Collection 문서는 collection을 QueryClient와 parameter set마다 한 번 만들고 재사용하라고 안내한다. SSR, 테스트, multi-tenant app에서는 QueryClient가 request-local 또는 route-local일 수 있다. 같은 QueryClient와 같은 queryKey로 collection을 여러 번 만들면 각 collection은 독립적인 materialized state, lifecycle, subscription, optimistic mutation을 가진다.

실무적으로는 이렇게 생각하면 된다.

  • tenant/project/account처럼 서버 자원의 경계는 collection scope가 될 수 있다.
  • where, orderBy, limit, offset마다 collection을 새로 만들면 안 된다.
  • 그런 relational subset은 syncMode: 'on-demand'와 live query predicate로 표현한다.

이 원칙을 지키지 않으면 클라이언트 안에 같은 table의 “작은 DB”가 여러 개 생긴다. 그러면 optimistic update와 Realtime event가 어느 collection에 반영됐는지 추적하기 어려워진다.

4-2. Predicate push-down은 성능 최적화이자 정확성 경계다

TanStack DB overview는 on-demand mode를 큰 dataset, search UI, catalog처럼 대부분의 row를 접근하지 않는 경우에 맞는 전략으로 설명한다. Query Collection 문서는 LoadSubsetOptionsqueryFn으로 넘겨 where, orderBy, limit, offset을 API parameter로 번역할 수 있다고 설명한다.

Supabase 어댑터 README도 filter, order, limit, offset, join, aggregate 중 많은 연산을 PostgREST로 push한다고 적는다. 하지만 모든 연산이 서버로 내려가는 것은 아니다. 일부는 matching rows를 fetch한 뒤 client-side에서 처리될 수 있다.

그래서 팀은 “이 live query는 서버에서 몇 row를 가져오는가?”를 코드 리뷰 질문으로 삼아야 한다. 특히 RLS가 맞더라도 너무 넓게 가져와 클라이언트에서 필터링하면 성능과 비용 문제가 생긴다.

4-3. Optimistic mutation은 UX 기능이 아니라 실패 설계다

Optimistic update는 사용자가 빠르게 느끼게 해준다. 하지만 Supabase가 최종 source of truth라면 서버에서 실패할 수 있다.

  • RLS가 update를 거절할 수 있다.
  • unique constraint나 foreign key constraint가 실패할 수 있다.
  • network가 끊길 수 있다.
  • Realtime event가 늦게 도착하거나 중복으로 보일 수 있다.

좋은 optimistic mutation 설계는 “일단 화면만 바꾸기”가 아니다. rollback 메시지, pending 표시, retry 가능 여부, 서버가 계산한 column(updated_at, count, status 등)을 다시 반영하는 규칙까지 포함한다.

4-4. Realtime은 table별 제품 요구사항으로 켠다

realtime: true는 매력적이다. 하지만 모든 table에 켜면 안 된다. 다음 기준으로 나눠보자.

  • 켜기 좋은 table: 채팅, presence, 협업 task, 주문 상태, 실시간 dashboard처럼 다른 사용자의 변경이 곧 UX 가치인 경우
  • 끄기 좋은 table: 설정, 약관, 관리자용 reference data, 변경 빈도가 낮고 refetch로 충분한 경우
  • 주의할 table: 개인정보, 결제, audit log, 조직 내부 데이터처럼 RLS와 publication 범위를 강하게 검증해야 하는 경우

5. 내 프로젝트에 적용할 체크포인트

Supabase 프로젝트에 TanStack DB 계열을 붙이기 전에 아래를 먼저 확인하자.

데이터 모델

  • collection 하나가 실제 Postgres table 하나에 대응하는가?
  • keys가 primary key 또는 composite primary key와 일치하는가?
  • row schema가 Supabase generated type과 런타임 validation 사이에서 어긋나지 않는가?
  • insert/update payload type이 row type과 다를 때 transform 규칙이 있는가?

권한과 노출

  • collection이 읽는 table에 RLS가 켜져 있는가?
  • anonymous/authenticated role별 select/insert/update/delete policy가 테스트되어 있는가?
  • Realtime publication에 올린 table이 외부 client에게 노출되어도 되는가?
  • “RLS가 있으니 괜찮다”가 아니라 실제 user 세션으로 읽기/쓰기 테스트를 했는가?

동기화 전략

  • 이 collection은 eager, on-demand, progressive 중 무엇이 맞는가?
  • live query의 predicate가 PostgREST로 push-down되는가, 아니면 client-side fallback인가?
  • Realtime이 꼭 필요한 table인가, 아니면 refetch/invalidation으로 충분한가?
  • message 사용량과 비용을 확인할 환경 또는 spend cap이 있는가?

렌더링 경계

  • 이 데이터가 초기 SSR HTML에 꼭 필요한가?
  • 서버에서 한 번 읽으면 되는 데이터와 client collection으로 살아 있어야 하는 데이터를 분리했는가?
  • placeholderData와 initialData를 collection의 authoritative row처럼 오해하지 않는가?

실패 처리

  • optimistic mutation 실패 시 사용자에게 무엇을 보여주는가?
  • rollback 이후 form state나 toast 메시지는 일관적인가?
  • 서버 constraint 실패, RLS 실패, 네트워크 실패를 각각 테스트했는가?
  • Realtime event 중복/지연이 있어도 최종 상태가 맞는가?

6. 오늘 10분 액션

오늘 당장 라이브러리를 도입하지 않아도 된다. 대신 내 프로젝트의 “클라이언트 데이터 그래프”를 그려보자.

  1. 화면 하나를 고른다. 예: 프로젝트 보드, 주문 목록, 댓글 목록.
  2. 그 화면이 읽는 table/API를 모두 적는다.
  3. 각 데이터가 다음 중 어디에 속하는지 표시한다.
    • 서버에서 한 번 읽으면 되는 데이터
    • 클라이언트에서 파생 view가 많은 데이터
    • 다른 사용자의 변경이 즉시 보여야 하는 데이터
    • optimistic update가 필요한 데이터
  4. Realtime이 필요한 table에만 별표를 친다.
  5. 그 table의 RLS policy와 primary key를 확인한다.
  6. 마지막으로 질문한다. “이 데이터는 query cache로 충분한가, 아니면 collection/live query 모델이 필요한가?”

이 10분 액션의 목적은 도구 선택이 아니다. 서버 데이터가 클라이언트 안에서 어떤 모양으로 살아 움직이는지 보는 것이다.

7. 더 볼 자료

중복 회피 메모

로컬 content/generated와 Supabase 최근 web_app_dev 글을 확인했다. 최근 글은 Supabase RLS 테스트 권한 행렬, Realtime binary payload 호환성, Hono null-prototype 입력 객체, Next.js Partial Prefetching/App Shell, Supabase Pipelines/CDC, Next.js 보안 릴리스, TypeScript 7 빌드 계약 등을 다뤘다. 이번 글은 Supabase를 다시 다루지만 RLS 테스트, Realtime binary payload, CDC 분석 분리, Auth URL, JWT 검증이 아니라 @supabase-labs/tanstack-db 알파와 TanStack DB의 collection/live query/on-demand sync를 바탕으로 클라이언트 데이터 그래프, optimistic mutation, SSR 경계, Realtime 비용·권한 계약을 다룬다.

오늘 10분 액션+5%

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)