옵션
집집 Skill 웹 개발 vue-patterns

vue-patterns

affaan-m/ECC affaan-m/ECC

Vue, Nuxt, Vite 또는 Pinia 프로젝트를 위한 Vue.js 3 컴포지션 API 패턴, 컴포넌트 아키텍처, 반응성 모범 사례, Pinia 상태 관리, Vue Router 탐색 및 Nuxt SSR 패턴을 제공합니다.

...모든 것을 확장하십시오
0
업데이트 된 시간 2026년 10월 2일

Vue.js 패턴 및 모범 사례

Composition API를 활용한 Vue.js 3 개발을 위한 종합 가이드(

표현형 컴포넌트 대 컨테이너형 컴포넌트

  • 컨테이너 컴포넌트: 데이터 가져오기, 상태 및 부수 효과를 직접 관리합니다. 프리젠테이션 컴포넌트를 렌더링합니다.
  • 표현형 컴포넌트: 프로프(props)를 수신하고 이벤트를 발생시킵니다. API 호출이나 스토어 접근이 없습니다. 순수한 렌더링만 수행합니다.

프로프 모범 사례

// 기본값이 지정된 타입 기반 props
interface Props {
  label: string;
  variant?: "primary" | "secondary";
  disabled?: boolean;
  items: Item[];
}

const props = withDefaults(defineProps(), {
  variant: "primary",
  disabled: false,
});
  • 항상 타입을 명시하고, 필요한 경우필수 속성 및기본값을 지정하세요.
  • 부울형 프로퍼티: isXxx, hasXxx, canXxx.
  • 프로퍼티를 절대 직접 수정하지 말고, 대신 이벤트를 발생시키세요.
  • v-model 바인딩의 경우, defineModel() (Vue 3.4 이상) 또는 modelValue + update:modelValue를 사용하세요.

이벤트

const emit = defineEmits<{
  submit: [];
  "update:modelValue": [value: string];
  select: [id: string, index: number];
}>();
  • 템플릿에서는 kebab-case를 사용하세요 (@update:model-value).
  • 스크립트에서는 camelCase를 사용하세요 (emit("update:modelValue", val)).

3. 컴포저블(재사용 가능한 로직)

구조

// composables/useDebounce.ts
export function useDebounce(value: MaybeRef, delay: number): Ref {
  const debounced = ref(toValue(value)) as Ref;

  let timer: ReturnType;
  watch(
    () => toValue(value),
    (newVal) => {
      clearTimeout(timer);
      timer = setTimeout(() => { debounced.value = newVal; }, delay);
    }
  );

  onUnmounted(() => clearTimeout(timer));
  return readonly(debounced);
}

규칙

  • 반드시 'use' 접두사로 시작해야 합니다.
  • 반응형 값(ref, computed, reactive)을 반환해야 하며, 절대 일반 기본형(primitives)을 반환해서는 안 됩니다.
  • MaybeRef / toRef() / toValue()를 통해 반응형 입력을 받아들여야 합니다.
  • onUnmounted 또는 watcher의 onCleanup에서 부수 효과를 정리해야 합니다.
  • 모듈 범위 내의 부수 효과는 허용되지 않습니다.

vs 믹신

Composables는 Vue 2의 믹신을 완전히 대체합니다:

  • 믹신: 불투명한 데이터 흐름, 단일 진실원 충돌, 이름 충돌.
  • 컴포저블: 명시적인 임포트, 명확한 반환 값, 조합 가능하며 트리 셰이킹이 가능합니다.

4. 상태 관리

언제 무엇을 사용할지

패턴 사용 사례
ref() / reactive() 로컬 컴포넌트 상태
프로프 + 에미트 부모-자식 간 통신
제공 / 주입 테마, 구성, 플러그인 API
Pinia 스토어 전역, 공유, 복합 상태
서버 상태 컴포저블 캐싱 기능이 포함된 API 데이터 ( fetch/TanStack Query 래핑)

Pinia 스토어 설정 (권장)

// stores/useCartStore.ts
export const useCartStore = defineStore("cart", () => {
  const items = ref([]);
  const isLoading = ref(false);

  const totalPrice = computed(() =>
    items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
  );
  const itemCount = computed(() =>
    items.value.reduce((sum, i) => sum + i.quantity, 0)
  );

  async function addItem(productId: string) {
    isLoading.value = true;
    try {
      const item = await fetchProduct(productId);
      const existing = items.value.find(i => i.id === item.id);
      if (existing) existing.quantity++;
      else items.value.push({ ...item, quantity: 1 });
    } finally {
      isLoading.value = false;
    }
  }

  return { items, isLoading, totalPrice, itemCount, addItem };
});
  • Setup Store 구문(Options Store가 아님)을 사용하세요.
  • 비즈니스 수준의 변경에는 액션을, 그룹화된 업데이트에는 $patch() 를 우선적으로 사용하세요.
  • 모든 비동기 액션: 로딩 + 성공 + 오류 처리.

5. Vue Router

경로 정의

const routes = [
  {
    path: "/users/:id",
    name: "user-detail",
    component: () => import("@/pages/UserDetail.vue"), // 지연 로딩
    props: true, // 매개변수를 props로 전달
    meta: { requiresAuth: true },
  },
];

네비게이션 가드

router.beforeEach((to, from) => {
  const { isLoggedIn } = useAuthStore();
  if (to.meta.requiresAuth && !isLoggedIn) {
    return { name: "login", query: { redirect: to.fullPath } };
  }
});

반응형 라우트 매개변수

컴포넌트가 마운트된 상태에서 라우트 매개변수가 변경될 때:

const route = useRoute();
const id = computed(() => route.params.id as string);
watch(id, (newId) => fetchItem(newId));

6. 템플릿 패턴

템플릿 구문


로딩 중...
오류: {{ error }}
{{ content }}
토글된 콘텐츠
{{ item.name }}
{{ item.name }}

7. 성능

기법 사용 시점
v-memo 변경 빈도가 낮은 목록 항목
v-once 한 번 렌더링된 후 영구적으로 고정되는 콘텐츠
shallowRef() 대규모 데이터 구조를 일괄적으로 교체
shallowReactive() 최상위 속성만 반응형입니다
v-if 대신v-show 빈번한 표시/숨기기 전환
토글된 뷰 캐싱
지연 로드 경로 중요하지 않은 경로의 경우() => import(...)
Suspense 폴백 기능을 갖춘 비동기 컴포넌트 로딩

8. 테스트

Stack

  • 단위 테스트 및 컴포넌트 테스트를 위한Vitest
  • 마운팅 및 상호작용을 위한Vue Test Utils
  • 스토어 모킹을 위한@pinia/testing
  • E2E 테스트용Playwright

컴포넌트 테스트 패턴

import { mount } from "@vue/test-utils";
import { createPinia, setActivePinia } from "pinia";
import UserCard from "./UserCard.vue";

beforeEach(() => { setActivePinia(createPinia()); });

it("렌더링되고 이벤트가 발생합니다", async () => {
  const wrapper = mount(UserCard, {
    props: { user: { id: "1", name: "Alice" } },
  });
  expect(wrapper.text()).toContain("Alice");
  await wrapper.find("button").trigger("click");
  expect(wrapper.emitted("select")![0]).toEqual(["1"]);
});

9. Nuxt 전용 패턴

자동 임포트

Nuxt는 ref, computed, watch, useFetch, useAsyncData 등을 자동으로 임포트합니다. 임포트 선언 없이 바로 사용하세요. Nuxt가 아닌 프로젝트에서는 항상 명시적으로 임포트해야 합니다.

useAsyncData / useFetch

const { data: user, pending, error, refresh } = await useAsyncData(
  "user", // 캐싱을 위한 고유 키
  () => $fetch(`/api/users/${id}`),
);

const { data: posts } = await useFetch("/api/posts", {
  query: { page: 1 },
  key: "posts-page-1", // 중복 요청 방지
});

서버 라우트

// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
  const { id } = await getValidatedRouterParams(event, z.object({
    id: z.string().uuid(),
  }).parse);
  // ... 데이터를 가져와 반환
});

런타임 구성

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // 서버 전용
    apiSecret: "",
    // 공개 (클라이언트에 노출)
    public: {
      apiBase: "https://api.example.com",
    },
  },
});

10. Vue 3.5+의 새로운 API

반응형 프로프스 해체

Vue 3.5에서는 반응형 프로프스 해체 기능이 안정화되었습니다. defineProps() 에서 해체된 변수는 자동으로 반응형이 됩니다:

// Vue 3.5 이상: 분해된 프로프스는 반응형입니다 (toRefs가 필요 없음)
const { count = 0, msg = "hello" } = defineProps<{
  count?: number;
  msg?: string;
}>();

// 제한 사항: 분해 할당된 프로퍼티를 직접 감시할 수 없음
watch(() => count, (newVal) => { ... }); // 통과, getter 필수

useTemplateRef()

템플릿 참조의 경우, 이름이 일치하는 일반 ref를 useTemplateRef() 로 대체하세요:

import { useTemplateRef } from "vue";
const inputEl = useTemplateRef("input");
// "input"은 변수 이름이 아닌 템플릿 내의 ref="input" 속성과 일치합니다

동적 ref ID 지원: useTemplateRef(dynamicRefId).

onWatcherCleanup()

전역에서 임포트 가능한 워처 정리 API(Vue 3.5 이상). 워처 콜백 내부에서 동기식으로 호출해야 합니다:

import { watch, onWatcherCleanup } from "vue";

watch(userId, async (newId) => {
  const controller = new AbortController();
  onWatcherCleanup(() => controller.abort());
  // ... 신호를 사용하여 데이터 가져오기
});

useId()

폼 요소 및 접근성을 위한 SSR 호환 고유 ID 생성:

import { useId } from "vue";
const id = useId();

defer Teleport

동일한 렌더링 사이클 내에서 렌더링된 대상 위치로 텔레포트할 수 있게 합니다:

콘텐츠

지연 하이드레이션 (SSR)

defineAsyncComponent() 가 이제 하이드레이션 전략을 지원합니다:

import { defineAsyncComponent, hydrateOnVisible } from "vue";
const AsyncComp = defineAsyncComponent({
  loader: () => import("./Comp.vue"),
  hydrate: hydrateOnVisible(),
});

안티패턴

안티패턴 왜 잘못된가 해결 방법
defineProps() 의 디스트럭처링 (Vue 3.5 미만) 스냅샷을 캡처하여 반응성을 상실함 props.xxx를 통해 접근하거나 toRefs() 사용
해체된 prop에watch() 적용 (Vue 3.5+) 컴파일 시간 오류 — 구조 해체된 props는 직접 감시할 수 없음 게터 래퍼 사용: watch(() => count, ...)
동일한 요소에v-if + v-for 적용 모호한 실행 순서 계산된 필터링된 배열 사용
v-for 키 = 인덱스 순서 변경 시 상태 오류 안정적인 데이터베이스 ID 사용
프로퍼티 변경 단방향 데이터 흐름 위반 이벤트 발생 또는 v-model 사용
사용자 콘텐츠가 포함된v-html XSS 취약점 DOMPurify를 사용하여 정제
Vue 3의 믹스인 불투명하고 충돌 발생 가능성이 높음 컴포저블로 대체
컴포저블 내 모듈 범위 부작용 인스턴스 간 공유 onMounted 및 onUnmounted 내의 스코프
교체 가능한 상태를 위한reactive() 교체 시 반응성이 깨짐 대신 ref() 사용
정리 기능이 없는 Watcher 메모리 누수, 경합 상태 onCleanup 또는 onWatcherCleanup() 사용 (Vue 3.5 이상)
새로운 Vue 3 코드의 Options API 생태계가 Composition API로 전환 중 사용법

모든 파일

1개 파일

vue-patterns 설치

스킬 파일을 다운로드한 후 .claude/skills/ 디렉터리에 압축을 풀어주세요.

ZIP 다운로드

저장소를 클론하고 스킬 파일을 프로젝트에 복사하세요.

git clone https://github.com/affaan-m/ECC/tree/main/skills/vue-patterns # Copy SKILL.md to your .claude/skills/ directory

복사 복사
빠른 설정: 스킬 폴더를 .claude/skills/로 복사하세요. Claude가 해당 스킬을 자동으로 감지하여 사용할 것입니다.
저장소 affaan-m/ECC

관련 스킬

github-code-search
업데이트 된 시간 2026년 6월 29일
drizzle-orm
업데이트 된 시간 2026년 6월 29일
clickhouse-io
업데이트 된 시간 2026년 6월 29일
prisma-client-api
업데이트 된 시간 2026년 6월 29일