d8a80f6272
이름만 다른(표기 변형) [[위키링크]]를 대상 문서의 canonical 제목으로 치환해 끊겼던 1,200개 링크를 연결. 제목/파일명 정규화 일치만 적용하고 별칭 매칭은 과병합 위험으로 제외(애매성 가드). 원본은 _link_reconcile_backup/ 에 백업. 도구: Datacollect/scripts/link_reconcile_apply.mjs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.9 KiB
5.9 KiB
id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
| id | title | category | status | canonical_id | aliases | duplicate_of | source_trust_level | confidence_score | verification_status | tags | raw_sources | last_reinforced | github_commit | tech_stack | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| wiki-2026-0508-one-way-data-flow | One-way Data Flow | 10_Wiki/Topics | verified | self |
|
none | A | 0.9 | applied |
|
2026-05-10 | pending |
|
One-way Data Flow
매 한 줄
"매 state 매 down, event 매 up". Unidirectional data flow 매 React (Flux 영감) 의 core mental model — parent props 으로 child 에 데이터 전달, child callback 으로 parent 에 event 통보. 매 reasoning 의 단순화, debugging 의 traceability, time-travel 의 가능성. Vue/Solid/Svelte 도 매 동일 원칙.
매 핵심
매 핵심 규칙
- State 매 single owner (component or store).
- Owner 만 mutate 가능.
- Child 매 read-only props 만 받음.
- Child 의 변경 요청 매 callback / event / dispatch.
매 왜 unidirectional
- Predictability: state 변경 source 매 명확.
- Debugging: render output 매 props 의 함수.
- Time-travel: state snapshot 만 으로 UI 재현.
- Concurrency: 매 React Concurrent 매 mutable 2-way 매 deadlock-prone.
매 응용
- React props/callback 패턴.
- Redux / Zustand / Jotai store dispatch.
- Vue
props down, emit up. - Form: lift state up.
- Event sourcing / CQRS frontend.
💻 패턴
Lifting state up (React)
function Parent() {
const [name, setName] = useState("");
return (
<>
<NameInput value={name} onChange={setName} />
<Greeting name={name} />
</>
);
}
function NameInput({ value, onChange }: { value: string; onChange: (v: string) => void }) {
return <input value={value} onChange={(e) => onChange(e.target.value)} />;
}
function Greeting({ name }: { name: string }) {
return <p>Hello, {name || "stranger"}!</p>;
}
Reducer (action up, state down)
type Action = { type: "add"; item: string } | { type: "remove"; idx: number };
type State = { items: string[] };
function reducer(s: State, a: Action): State {
switch (a.type) {
case "add": return { items: [...s.items, a.item] };
case "remove": return { items: s.items.filter((_, i) => i !== a.idx) };
}
}
function TodoApp() {
const [state, dispatch] = useReducer(reducer, { items: [] });
return (
<>
<AddBox onAdd={(item) => dispatch({ type: "add", item })} />
<List items={state.items} onRemove={(idx) => dispatch({ type: "remove", idx })} />
</>
);
}
Zustand (store as single source)
import { create } from "zustand";
interface CartStore {
items: { id: string; qty: number }[];
add: (id: string) => void;
remove: (id: string) => void;
}
export const useCart = create<CartStore>((set) => ({
items: [],
add: (id) => set((s) => ({ items: [...s.items, { id, qty: 1 }] })),
remove: (id) => set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
}));
function ProductCard({ id }: { id: string }) {
const add = useCart((s) => s.add);
return <button onClick={() => add(id)}>Add</button>;
}
Vue 3 props down + emit up
<!-- Parent.vue -->
<template>
<Counter :value="count" @increment="count++" />
</template>
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<!-- Counter.vue -->
<template>
<button @click="$emit('increment')">{{ value }}</button>
</template>
<script setup>
defineProps<{ value: number }>();
defineEmits<{ increment: [] }>();
</script>
Server-driven (React Query + URL state)
import { useQuery } from "@tanstack/react-query";
import { useSearchParams } from "react-router";
function ProductList() {
const [params, setParams] = useSearchParams();
const category = params.get("category") ?? "all";
const { data } = useQuery({
queryKey: ["products", category],
queryFn: () => fetch(`/api/products?cat=${category}`).then((r) => r.json()),
});
return (
<>
<CategoryPicker value={category} onChange={(v) => setParams({ category: v })} />
{data?.map((p) => <Card key={p.id} product={p} />)}
</>
);
}
Forbidden 2-way leak (anti-pattern shown)
// ❌ child mutates parent's prop directly via mutable ref
function BadChild({ obj }: { obj: { count: number } }) {
return <button onClick={() => obj.count++}>+</button>; // parent never re-renders
}
매 결정 기준
| 상황 | Approach |
|---|---|
| 2-3 components share state | Lift state up |
| Cross-tree state | Context / Zustand / Redux |
| Server data | React Query / SWR (single source) |
| URL state | useSearchParams / Next router |
| Form-heavy local | useReducer + dispatch |
| Vue | props + emit (or Pinia) |
기본값: state 매 highest common ancestor 또는 매 store. 매 props down + callback up.
🔗 Graph
- 부모: React
- 변형: 프론트엔드 및 UIUX 표준 · Zustand · Pinia
- 응용: useReducer · Event Sourcing
- Adjacent: Reactive-Streams · Signals
🤖 LLM 활용
언제: React/Vue/Solid component design, form lifting, store architecture. 언제 X: 매 highly local input (e.g. simple text field) — 매 controlled-input over-engineering 회피, uncontrolled OK.
❌ 안티패턴
- Mutate props in child: 매 silent re-render miss.
- Shared mutable ref: 매 React diff 의 invariant 위반.
- Two-way binding in big tree: 매 cycle / cascade.
- Prop drilling 10층: 매 Context / store 으로 cut.
- Local form state in 매 sync 매 server: 매 stale conflict — server 매 source of truth + optimistic update.
🧪 검증 / 중복
- Verified (React docs — Sharing State Between Components, Vue docs — Component Basics, Redux principles).
- 신뢰도 A.
🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — lifting/reducer/Zustand/Vue emit/server-driven 패턴 |