--- id: wiki-2026-0508-composables title: Composables (Vue) category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Vue Composables, useX functions] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [vue, composition-api, composables, frontend] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript framework: Vue 3 --- # Composables (Vue) ## 매 한 줄 > **"매 reactive logic 의 reusable function"**. Composable 은 Vue 3 Composition API 의 stateful logic 을 추출한 `useX()` 함수 — React Hooks 와 유사하나 매 reactivity primitive (`ref`, `reactive`, `computed`) 기반이라 caller 매 free of dependency arrays. ## 매 핵심 ### 매 정의 - `use*` prefix 매 convention. - Returns reactive refs / computed / methods. - Side-effects via `onMounted` / `onScopeDispose` (`watchEffect` cleanup). - `effectScope` 매 manual lifecycle (e.g., outside components). ### 매 vs React Hooks | Aspect | Vue Composable | React Hook | |---|---|---| | Re-run | 매 once on setup | every render | | Deps | reactive auto-track | manual array | | Cleanup | `onScopeDispose` | return fn | | Conditional call | 허용 (with caveats) | 금지 | ### 매 응용 1. `useFetch` / `useAsyncData` — async + cancellation. 2. `useElementSize`, `useEventListener` — DOM bindings (VueUse). 3. `useStore`, `useFeatureFlag` — cross-cutting state. ## 💻 패턴 ### Basic counter composable ```ts import { ref, computed } from 'vue'; export function useCounter(initial = 0) { const count = ref(initial); const isZero = computed(() => count.value === 0); const inc = () => count.value++; const dec = () => count.value--; return { count, isZero, inc, dec }; } ``` ### useFetch with abort + reactive URL ```ts import { ref, watchEffect, onScopeDispose, type MaybeRefOrGetter, toValue } from 'vue'; export function useFetch(url: MaybeRefOrGetter) { const data = ref(null); const error = ref(null); const loading = ref(false); let ctrl: AbortController | null = null; watchEffect(async () => { ctrl?.abort(); ctrl = new AbortController(); loading.value = true; try { const r = await fetch(toValue(url), { signal: ctrl.signal }); data.value = await r.json(); } catch (e) { if ((e as Error).name !== 'AbortError') error.value = e as Error; } finally { loading.value = false; } }); onScopeDispose(() => ctrl?.abort()); return { data, error, loading }; } ``` ### useEventListener (auto cleanup) ```ts import { onMounted, onScopeDispose, type Ref } from 'vue'; export function useEventListener( target: Window | Ref, event: K, handler: (e: WindowEventMap[K]) => void, ) { onMounted(() => { const el = 'value' in target ? target.value : target; el?.addEventListener(event, handler as EventListener); }); onScopeDispose(() => { const el = 'value' in target ? target.value : target; el?.removeEventListener(event, handler as EventListener); }); } ``` ### useLocalStorage (sync ref ↔ storage) ```ts import { ref, watch } from 'vue'; export function useLocalStorage(key: string, initial: T) { const stored = localStorage.getItem(key); const value = ref(stored ? JSON.parse(stored) : initial); watch(value, (v) => localStorage.setItem(key, JSON.stringify(v)), { deep: true }); return value; } ``` ### useDebouncedRef ```ts import { customRef } from 'vue'; export function useDebouncedRef(value: T, delay = 300) { let t: ReturnType; return customRef((track, trigger) => ({ get() { track(); return value; }, set(v) { clearTimeout(t); t = setTimeout(() => { value = v; trigger(); }, delay); }, })); } ``` ### Detached scope (composable outside component) ```ts import { effectScope } from 'vue'; const scope = effectScope(); scope.run(() => { const counter = useCounter(); // ... use anywhere }); // later scope.stop(); ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Component-local state | `ref` directly | | Logic reused in 2+ components | Composable | | Global state (auth, theme) | Pinia store (composable underneath) | | DOM API integration | VueUse composable or custom | **기본값**: 매 reusable reactive logic → composable. Single-use → inline. ## 🔗 Graph - 부모: [[Composition API]] · [[Vue 3]] - 응용: [[Pinia]] - Adjacent: [[Component-Composition]] ## 🤖 LLM 활용 **언제**: stateful logic 매 2+ components 에서 사용 / DOM·async 의 lifecycle wrapping. **언제 X**: 매 pure function (no reactivity) — 매 plain util 로 충분. ## ❌ 안티패턴 - **Returning reactive() with destructure**: loses reactivity → use `toRefs`. - **Global side-effects in composable body**: 매 `onMounted` 안에 넣을 것. - **Naming without `use` prefix**: 매 convention break, lint rule 매 fail. ## 🧪 검증 / 중복 - Verified (Vue.js docs / VueUse source). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — Vue 3 composables with patterns |