"매 모든 derived state 의 single canonical origin". 매 1970s database normalization 에서 시작 — 매 modern frontend (Redux, TanStack Query, RSC) 의 핵심 원리. 매 duplication 의 elimination → 매 inconsistency bug 의 소멸.
매 핵심
매 정의
매 한 데이터 의 하나의 authoritative location.
매 다른 모든 view/component 의 derive (read-only projection).
매 update 의 single entry point — 매 race / drift 의 prevention.
매 frontend 적용 layer
Server state SSOT: 매 backend DB — 매 client 의 cache (TanStack Query, SWR).
URL state SSOT: 매 query params / path — 매 shareable, refreshable.
Form state SSOT: 매 RHF / Formik 의 form object — 매 controlled input 의 derive.
Global UI state SSOT: 매 Zustand / Jotai store — 매 cross-component shared.
매 응용
TanStack Query — server SSOT mirror.
RSC (React Server Components) — server 의 SSOT 그대로 stream.
URL-driven state (nuqs, TanStack Router) — 매 shareable SSOT.
CRDT (Yjs, Automerge) — 매 distributed SSOT.
💻 패턴
Server SSOT via TanStack Query
// ❌ duplication — local state mirrors server
const[user,setUser]=useState<User|null>(null);useEffect(()=>{fetchUser().then(setUser);},[]);// ✅ SSOT — server is truth, query is cached projection
const{data: user}=useQuery({queryKey:['user',userId],queryFn:()=>fetchUser(userId),});
URL as SSOT (nuqs)
import{useQueryState,parseAsInteger}from'nuqs';functionProductList() {const[page,setPage]=useQueryState('page',parseAsInteger.withDefault(1));const[sort,setSort]=useQueryState('sort');// URL ?page=2&sort=price is the truth — refresh-safe, shareable
return<Listpage={page}sort={sort}/>;}
Derived state (no duplicate store)
// ❌ duplicating filtered list in state
const[items,setItems]=useState(allItems);const[filter,setFilter]=useState('');useEffect(()=>{setItems(allItems.filter(i=>i.name.includes(filter)));},[filter]);// ✅ derive on render — items isn't state
const[filter,setFilter]=useState('');constvisible=useMemo(()=>allItems.filter(i=>i.name.includes(filter)),[filter]);
Form SSOT via RHF
const{register,watch,handleSubmit}=useForm<FormData>({defaultValues:{email:'',plan:'free'},});constplan=watch('plan');// derived from form SSOT
return(<formonSubmit={handleSubmit(submit)}><input{...register('email')}/>{plan==='pro'&&<input{...register('teamSize')}/>}</form>);
Zustand store SSOT
import{create}from'zustand';typeCartStore={items: CartItem[];add:(item: CartItem)=>void;remove:(id: string)=>void;};exportconstuseCart=create<CartStore>((set)=>({items:[],add:(item)=>set((s)=>({items:[...s.items,item]})),remove:(id)=>set((s)=>({items: s.items.filter((i)=>i.id!==id)})),}));// Total derived — not stored
constuseCartTotal=()=>useCart((s)=>s.items.reduce((a,i)=>a+i.price,0));
CRDT SSOT (collaborative)
import*asYfrom'yjs';import{WebrtcProvider}from'y-webrtc';constydoc=newY.Doc();newWebrtcProvider('room-id',ydoc);constytext=ydoc.getText('content');ytext.observe(()=>{// single source — all peers converge
render(ytext.toString());});
매 결정 기준
데이터 종류
SSOT location
영속 entity (user, order)
Backend DB → TanStack Query cache
Shareable view state
URL params (nuqs)
Form draft
RHF / Formik form object
Cross-component UI
Zustand / Jotai
Component-local
useState (그 component 자체 가 SSOT)
Collaborative
CRDT (Yjs)
기본값: 매 server 의 SSOT, 매 client 의 cache projection.
언제: 매 state architecture design / data flow audit / bug-prone "two stores out of sync" 발견 시.
언제 X: 매 truly independent UI ephemeral state (hover, focus) — 매 local 이 자체 SSOT.
❌ 안티패턴
Mirror state: 매 server 의 data 를 useState 로 복제. → 매 stale + 두 source 의 drift.
Multiple stores 의 same field: Redux + Context + URL 모두 selectedId 보유. 매 update 의 race.
Premature derivation cache: 매 derived value 를 별도 state 로 저장. 매 useMemo 충분.
localStorage 의 silent SSOT: 매 sync 없는 cross-tab — 매 BroadcastChannel 또는 storage event 필요.
🧪 검증 / 중복
Verified (Redux docs — "Three Principles", Dan Abramov; Martin Fowler — SSOT).