99

최근 30분간 동시 방문자 수를 표시합니다. (~)

최고 동시 방문자 수 -
어제: 0명 / 오늘: 0명

Claude Code로 Vue 개발하기

Claude Code로 Vue 개발하기

이 글은 Claude Code의 기본 사용법을 알고 있다고 전제합니다.
설치, 승인 모드, Hooks, Skills 같은 개별 기능의 세부 옵션은 Claude Code 핵심 정리를 참고하세요.

Claude Code가 만든 Vue 코드는 대체로 동작하지만, 파일마다 방식이 달라질 수 있습니다.
어떤 컴포넌트는 Composition API를 사용하고 어떤 컴포넌트는 Options API를 사용할 수 있습니다.
ref()reactive()는 같은 상태를 다르게 선언할 수 있고, 옵션 스토어와 셋업 스토어가 한 프로젝트에 섞일 수 있습니다.
AI의 학습 데이터에 다양한 구현 방식이 섞여 있기 때문에, 지침이 없으면 같은 프로젝트에서 다른 결과가 나오기 쉽습니다.

Claude Code를 사용해서 Vue 개발을 할 때 규칙을 어디에 두고 어떻게 검증할지, 버그와 리팩토링, 리뷰, 성능, 배포까지 같은 기준으로 이어 가는 방법을 살펴봅시다.

# 프로젝트 준비

글 전체에서는 영화를 검색해서 목록으로 보여 주는 앱 하나를 예제로 사용합니다.
Vite로 만든 Vue + TypeScript 프로젝트에 ESLint와 Prettier, 경로 별칭까지 구성한 상태에서 시작합니다.
이 구성 과정은 Vue 프로젝트 시작하기 w. Vite를 참고하세요.

프로젝트 루트에서 Claude Code를 실행하고 /init 명령을 입력합니다.
현재 프로젝트를 분석해 CLAUDE.md 파일의 초안을 만들어 줍니다.

PROMPT
1
/init

/init은 프로젝트 구조와 사용 중인 도구를 정리해 주지만, 앞에서 살펴본 작성 방식 문제까지 해결하지는 못합니다.
어떤 방식을 사용할 것인지는 다음 섹션에서 직접 작성합니다.

그전에 검증 명령부터 확보합니다.
규칙을 잘 써 두어도 결과를 확인할 방법이 없으면 어긋난 코드를 걸러 낼 수 없습니다.
특히 <template> 안의 오류는 편집기를 열지 않으면 드러나지 않으므로, 타입 검사를 별도의 스크립트로 만듭니다.

/package.json
JSON
1
2
3
4
5
6
7
8
9
10
{ "scripts": { "dev": "vite", "build": "vue-tsc -b && vite build", "preview": "vite preview", "typecheck": "vue-tsc --build", "lint": "eslint .", "test:unit": "vitest run" } }
검증 스크립트 추가

tsc*.vue 파일을 읽지 못하므로 타입 검사는 vue-tsc를 사용합니다.
<script>뿐 아니라 <template> 안의 표현식까지 검사합니다.
Vite 템플릿의 build 스크립트에 이미 vue-tsc -b가 들어 있지만, 빌드 없이 검사만 하도록 typecheck를 따로 관리합니다.

이 명령들은 Claude가 승인 없이 실행할 수 있도록 프로젝트 설정에 등록합니다.
매번 권한을 물어보면 검증 루프의 흐름이 끊어집니다.

/.claude/settings.json
JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{ "permissions": { "allow": [ "Bash(npm run typecheck)", "Bash(npm run lint)", "Bash(npm run test:unit)" ], "deny": [ "Read(.env)", "Read(.env.local)", "Read(.env.*.local)" ] } }

deny에 등록한 경로는 Claude가 읽지 못합니다.
API 키가 포함된 파일을 제한하면 값이 대화 기록과 로그에 남지 않습니다.

거부 규칙은 읽기뿐 아니라 같은 경로의 쓰기도 막습니다.
Read(.env*)처럼 넓게 작성하면 이름만 작성하는 견본 파일 .env.example도 만들지 못하므로, 값이 실제로 들어가는 이름만 선택해서 제한합니다.

.claude/settings.json은 팀이 공유하는 설정이므로 Git에 커밋합니다.
개인 설정은 .claude/settings.local.json에 두고 .gitignore에 추가하세요.

# Vue 규칙 정의

CLAUDE.md는 매 세션 시작 시 자동으로 읽히는 프로젝트 지침 파일입니다.
여기에서 추가할 내용은 프로젝트 소개가 아닌, 선택지가 여러 개인 상황에서 무엇을 선택할 것인지에 관한 내용입니다.

Vue에서 결정이 필요한 상황은 다음과 같습니다.

달라지는 부분 선택지 지침이 없을 때
컴포넌트 작성 방식 Options API / Composition API 파일마다 섞임
스크립트 블록 <script> / <script setup> export default가 섞여 들어옴
반응형 선언 ref() / reactive() 같은 성격의 상태가 다르게 선언됨
스토어 정의 옵션 스토어 / 셋업 스토어 스토어마다 구조가 달라짐
로직 재사용 컴포저블 / 믹스인 믹스인이 등장함
스타일 격리 scoped / 전역 / CSS Modules 전역 선택자가 새어 나감
경로 참조 상대 경로 / 별칭 ../../../가 늘어남

각 항목을 하나씩 결정하고 규칙 문장으로 작성합니다.
이유를 길게 설명하기보다 무엇을 사용하고 무엇을 사용하지 않는지 짧게 작성하는 것이 좋습니다.

/CLAUDE.md
MD
1
2
3
4
5
6
7
8
9
10
11
12
## Vue 코딩 규칙 - 모든 Vue 컴포넌트는 `<script setup lang="ts">`를 사용한다. Options API는 사용하지 않는다. - 반응형 상태는 `ref()`를 기본으로 사용한다. `reactive()`는 특수한 상황이 아니면 사용하지 않는다. - 파생 값은 `computed()`로 만든다. `watch()`로 다른 상태를 갱신해 파생 값을 만들지 않는다. - 로직 재사용은 컴포저블로 한다. 믹스인(mixin)은 사용하지 않는다. - 컴포저블 파일은 `/src/composables/useXxx.ts`에 두고, 함수 이름은 `use` 접두사로 시작한다. - `<style>`에는 항상 `scoped`를 붙인다. 전역 스타일은 `/src/assets/main.css`에만 작성한다. - 컴포넌트 파일 이름은 PascalCase 다중 단어로 짓는다. (`MovieCard.vue`) - `import``@/` 별칭을 사용한다. 상대 경로는 같은 폴더 안에서만 허용한다. - Props는 `defineProps<T>()` 형태의 타입 기반 선언만 사용한다. - 컴포넌트 사이 통신은 Props와 Emits로 한다. `provide`/`inject`는 레이아웃 수준에서만 사용한다.
Vue 코딩 규칙 섹션

예를 들어, ref()로 통일하는 규칙은 단순히 취향일 수도 있지만 다른 이유가 있습니다.
reactive()는 원시형에서 반응성을 잃고, 구조 분해에서도 반응성이 끊어집니다.
두 함수가 섞이면 어느 쪽 제약이 걸리는지 매번 판단해야 하고, 판단이 틀리면 화면이 갱신되지 않는 버그가 됩니다.
한쪽으로 고정하면 판단해야 하는 상황이 상당히 줄어듭니다.

서로 모순되는 지침이 있으면 Claude는 둘 중 하나를 임의로 선택합니다.
"ref()를 사용한다""객체 상태는 reactive()가 편하다" 가 같이 있으면 규칙이 없는 것과 같습니다.

규칙이 늘어나서 CLAUDE.md가 길어지면, Vue 규칙과 스토어 규칙, 라우팅 규칙처럼 성격이 다른 묶음을 .claude/rules/ 폴더로 나눕니다.
이 폴더의 마크다운 파일은 CLAUDE.md와 같은 프로젝트 범위 지침으로 자동 로드되므로, CLAUDE.md에서 @경로 방식으로 따로 참조할 필요가 없습니다.

1
2
3
4
5
6
7
├─.claude/ │ ├─rules/ │ │ ├─vue.md │ │ ├─pinia.md │ │ └─router.md │ └─settings.json └─CLAUDE.md
지침 파일 배치

CLAUDE.md에는 프로젝트 개요와 명령만 남깁니다.

/CLAUDE.md
MD
1
2
3
4
5
6
7
8
9
10
11
# 영화 검색 앱 Vite + Vue 3 + TypeScript 기반의 영화 검색 애플리케이션입니다. Vue, 스토어, 라우팅 규칙은 `.claude/rules/` 폴더에 있습니다. ## 자주 사용하는 명령 - `npm run dev`: 개발 서버 실행 - `npm run typecheck`: 타입 검사 (`*.vue` 템플릿 포함) - `npm run lint`: ESLint 검사 - `npm run test:unit`: 단위 테스트

규칙 파일은 paths 프론트매터로 적용 범위를 좁힐 수 있습니다.
Vue 규칙은 *.vue 파일을 작업할 때만 필요하므로, 조건을 지정하면 다른 작업에서는 로드되지 않습니다.

/.claude/rules/vue.md
MD
1
2
3
4
5
6
7
8
9
10
--- paths: - "src/**/*.vue" - "src/composables/**/*.ts" --- # Vue 코딩 규칙 - 모든 컴포넌트는 `<script setup lang="ts">`를 사용한다. - 반응형 상태는 `ref()`를 기본으로 사용한다.
조건부로 로드되는 규칙

paths가 없는 규칙 파일은 조건 없이 매 세션 로드되며, .claude/CLAUDE.md와 같은 우선순위를 가집니다.

CLAUDE.md 파일 하나는 100~200줄 이하로 유지하는 것이 좋습니다.
길어질수록 컨텍스트를 더 많이 사용하고, 지침을 지키는 수준도 떨어집니다.

포매팅 규칙은 CLAUDE.md에 작성하지 않습니다.
세미콜론이나 따옴표 스타일은 Prettier로 이미 결정한 내용이므로, 중복해서 작성하면 토큰만 더 사용하고 충돌 여지가 생깁니다.
도구로 강제할 수 있는 것은 도구에게 맡기고, 지침에는 도구가 판단할 수 없는 설계 결정만 작성합니다.

# SFC 작업 지시

Vue의 SFC(Single File Component)는 AI와 작업할 때 유리한 구조입니다.
템플릿과 스크립트, 스타일이 하나의 파일 안에 있어서 컴포넌트 하나만 확인하면 맥락을 모두 파악할 수 있습니다.
스타일 파일과 타입 파일을 따로 찾을 필요가 없으니 읽어야 할 파일 수와 토큰이 줄어듭니다.

반대로 파일이 커지면 작은 수정에도 전체를 다시 쓰려는 경향이 생깁니다.
<template>만 바꾸면 되는데 <script setup>까지 재작성되면 검토 비용이 커지므로, 어느 블록을 수정할 것인지 명시합니다.

PROMPT
1
MovieCard 컴포넌트에 개봉 연도를 표시해 줘.
블록을 지정하지 않은 요청
PROMPT
1
2
@/components/MovieCard.vue의 <template>만 수정해서 제목 아래에 개봉 연도를 표시해 줘. <script>과 <style>은 그대로 두고, Props 타입도 수정하지마!
블록을 지정한 요청

새 컴포넌트를 만들 때는 인터페이스를 먼저 정해서 전달하는 게 좋습니다.
Props와 Emits의 형태가 정해져 있으면 구현의 자유도가 줄어들고, 나중에 상위 컴포넌트와 일치시키는 수정이 최소화됩니다.

PROMPT
1
2
3
4
5
6
7
8
9
10
11
@/components/MovieCard.vue를 만들어 줘. Props: movie: { imdbID: string; Title: string; Poster: string; Year: string } Emits: select: (imdbID: string) => void - 포스터 이미지, 제목, 연도를 표시한다. - 카드를 클릭하면 select 이벤트로 imdbID를 전달한다. - Poster 값이 'N/A'이면 대체 이미지를 표시한다. - CLAUDE.md의 Vue 코딩 규칙을 따른다.
인터페이스를 먼저 주는 요청

색이나 간격 등의 스타일을 프롬프트로 매번 설명하면 컴포넌트마다 다른 값이 나올 수 있고, 화면이 쌓였을 때 디자인 조화가 깨집니다.
프로젝트가 사용하는 값을 CSS 변수로 한 곳에 정의하고 그 변수만 사용하도록 규칙에 작성하면 스타일도 대부분 고정됩니다.

/src/assets/main.css
CSS
1
2
3
4
5
6
7
8
9
10
11
:root { --color-bg: #ffffff; --color-text: #1f2328; --color-primary: #42b883; --color-border: #d0d7de; --radius: 8px; --space-1: 4px; --space-2: 8px; --space-3: 16px; --space-4: 24px; }
/.claude/rules/vue.md
MD
1
2
3
4
5
- 색과 간격, 모서리 반경은 `/src/assets/main.css`의 CSS 변수만 사용한다. 값을 직접 적지 않는다. - 필요한 색이나 간격이 없으면 변수를 먼저 추가하고 사용한다. - 레이아웃은 Flex를 기본으로 하고, 2차원 배치가 필요할 때만 Grid를 사용한다. - 중단점은 768px과 1024px 두 가지만 사용한다. - 컴포넌트 안에서 여백을 바깥쪽으로 밀어내지 않는다. 컴포넌트 사이 간격은 부모가 정한다.
스타일 규칙

이러한 규칙이 있으면 영화 카드 컴포넌트를 만들어 줘라는 짧은 요청만으로도 나머지 화면과 같은 스타일을 유지할 수 있습니다.
디자인 시안이 있다면 매번 첨부하지 말고, 시안에서 추출한 값(디자인 토큰)을 변수로 먼저 정의하는 것이 좋습니다.

여러 파일이 함께 바뀌는 작업은 Plan 모드로 시작합니다.
Shift + Tab이나 /plan 명령을 사용해 Plan 모드에 들어가면 파일을 확인하고 계획만 세울 뿐 편집하지는 않습니다.
검색 입력과 목록, 카드, 컴포저블을 한꺼번에 만드는 작업이라면, 파일 목록과 데이터 흐름을 먼저 확인하는 것이 안전합니다.

PROMPT
1
2
3
영화 검색 화면을 만들려고 해. 검색 입력, 로딩 표시, 결과 목록, 빈 결과 처리까지 포함해서 어떤 파일을 만들고 각각 어떤 역할을 맡을지 계획만 세워 줘.
Plan 모드에서의 요청

계획에서 확인할 것은 코드가 아니라 경계입니다.
검색어 상태를 어느 컴포넌트가 관리하는지, API 호출을 컴포넌트와 컴포저블 중 어디에서 하는지만 맞으면 나머지 구현은 대체로 규칙대로 나옵니다.

데이터를 가져오는 로직은 컴포저블로 분리하도록 규칙에 추가하면, 컴포넌트가 비대해지는 것을 막을 수 있습니다.
컴포저블 작성 패턴은 Vue Composition API 핵심 패턴을 참고하세요.

/src/composables/useMovieSearch.ts
TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { ref, computed } from 'vue' import { useQuery } from '@tanstack/vue-query' import type { Movie } from '@/types/movie' export function useMovieSearch() { const searchText = ref('') const queryOptions = computed(() => ({ queryKey: ['movies', searchText.value], queryFn: async () => { const res = await fetch(`https://omdbapi.com?apikey=7035c60c&s=${searchText.value}`) const { Search: movies = [] } = await res.json() return movies as Movie[] }, enabled: !!searchText.value })) const { data: movies, isFetching } = useQuery(queryOptions) function search(text: string) { searchText.value = text.trim() } return { movies, isFetching, search } }
컴포저블로 분리한 검색 로직

TanStack Vue Query는 쿼리 옵션을 computed로 감싸 전달해야 옵션의 반응성이 감지됩니다.
이런 라이브러리별 사용 방식은 규칙 파일에 한 줄로 작성하면 매번 설명하지 않아도 됩니다.

# 스토어와 라우터 규칙

Pinia 스토어와 Vue Router 라우트는 구조가 반복되는 코드입니다.
매번 프롬프트로 설명하는 대신 규칙과 슬래시 명령으로 고정합니다.
각각의 문법은 Pinia 핵심 정리Vue Router 핵심 정리를 참고하세요.

Pinia에서 먼저 정할 것은 옵션 스토어와 셋업 스토어 중 하나입니다.
둘은 문법이 다를 뿐 기능은 같지만, 섞이면 스토어를 열 때마다 구조를 다시 파악해야 합니다.

/.claude/rules/pinia.md
MD
1
2
3
4
5
6
7
8
# Pinia 스토어 규칙 - 스토어는 옵션 스토어(`state`, `getters`, `actions`)로 정의한다. 셋업 스토어는 사용하지 않는다. - 파일 위치는 `/src/stores/이름.ts`, 훅 이름은 `use이름Store`, 스토어 ID는 파일 이름과 같은 소문자로 한다. - `state`는 반드시 팩토리 함수로 작성한다. - `getters`에서 `this`를 사용할 때는 화살표 함수 대신 일반 함수로 정의한다. - 여러 상태를 한 번에 바꿀 때는 개별 할당 대신 `$patch`를 사용한다. - 비동기 요청과 DOM 조작은 `actions`에서만 수행한다. `getters`에서는 하지 않는다.

마지막 두 줄은 자주 어긋나는 부분입니다.
게터는 읽기 전용 계산값이어서 비동기 요청이나 부수 효과가 들어가면 안 되지만, 지침이 없으면 데이터를 가져오는 코드가 게터에 추가될 수 있습니다.

이 규칙을 매번 확인하는 대신, 스토어 생성 자체를 슬래시 명령으로 만들 수 있습니다.
.claude/skills/ 아래에 폴더를 만들고 SKILL.md를 작성하면 /스킬명으로 호출할 수 있습니다.

/.claude/skills/new-store/SKILL.md
MD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--- name: new-store description: | 프로젝트 규칙에 맞는 Pinia 스토어 파일을 생성합니다. 새 스토어를 추가하거나 상태 관리 모듈이 필요할 때 사용하세요. pinia, store, state. argument-hint: 스토어 이름 (예: movie) disable-model-invocation: true --- # Pinia 스토어 생성 `$ARGUMENTS`를 스토어 이름으로 사용해 다음을 수행합니다. 1. `/src/stores/$ARGUMENTS.ts` 파일을 생성한다. 1. 기본적으로 `.claude/rules/pinia.md`의 규칙을 그대로 따른다. 1. 셋업 스토어로 상태, 게터, 액션의 뼈대만 만들고 비즈니스 로직은 비워 둔다. 1. 필요한 타입은 `/src/types/`에서 가져오고, 없으면 새로 정의한다. 1. 생성 후 `npm run typecheck`를 실행해 통과를 확인한다.
PROMPT
1
/new-store movie
슬래시 명령으로 스토어 생성

$ARGUMENTS에는 명령 뒤에 입력한 값이 그대로 치환되므로, 위 예제에서는 movie가 들어갑니다.
argument-hint는 슬래시 명령을 입력할 때 보이는 안내 문구일 뿐이고 값을 전달하지는 않습니다.

disable-model-invocation: true는 자동 호출을 끄고 슬래시 명령으로만 실행되게 하는 옵션입니다.
스토어 생성처럼 사용자가 의도한 시점에만 실행되어야 하는 작업에 적합합니다.

라우터도 같은 방식으로 규칙을 작성합니다.
이 프로젝트는 Vue Router 5의 파일 기반 라우팅(File-based Routing)을 사용합니다.
/src/pages 폴더의 파일 경로가 그대로 접근 경로가 되므로 라우트 배열을 직접 작성하지 않지만, 대신 규칙으로 정할 것이 라우트 정보를 어디에 작성하는지로 옮겨 갑니다.

/.claude/rules/router.md
MD
1
2
3
4
5
6
7
8
9
10
11
12
# 라우팅 규칙 - 페이지 컴포넌트는 `/src/pages`에 두고, 파일 경로가 그대로 접근 경로가 되게 한다. - 라우터 인스턴스는 `/src/routes/index.ts`에서 `vue-router/auto-routes``routes`로 생성한다. `routes` 배열을 직접 작성하지 않는다. - 레이아웃은 `/src/routes/layouts/`, 가드는 `/src/routes/guards/`에 둔다. - 라우트 이름과 `meta`는 페이지 컴포넌트에서 `definePage` 매크로로 지정한다. - 인증이 필요한 라우트는 `meta.auth: true`, 로그인한 사용자가 접근하면 안 되는 라우트는 `meta.guestOnly: true`를 지정한다. - 레이아웃 선택은 `meta.layout`으로 지정한다. - 새로운 `meta` 속성은 `RouteMeta` 인터페이스에 먼저 정의한 뒤 사용한다. - `definePage``beforeEnter`를 작성하지 않는다. 라우트별 가드는 지원하지 않으므로 `meta`와 전역 가드로 처리한다. - 가드는 `meta` 값만 보고 판단한다. 가드 안에 경로 문자열을 직접 비교하는 코드를 작성하지 않는다. - 페이지 지연 로딩은 자동으로 적용되므로 따로 작성하지 않는다.

definePage 규칙은 파일 기반 라우팅으로 바꾼 프로젝트에서 특히 중요합니다.
학습 데이터에는 routes 배열에 라우트 객체를 직접 작성하던 코드가 훨씬 많아서, 지침이 없으면 이미 자동으로 생성되는 라우트를 다시 정의하는 코드가 나올 수 있습니다.

/src/pages/movies.vue
VUE
1
2
3
4
5
6
7
<script setup lang="ts"> definePage({ meta: { auth: true } }) </script>
definePage로 지정하는 라우트 정보

가드가 경로 문자열을 직접 비교하지 않게 하는 규칙은 특히 효과가 큽니다.
이 규칙이 없으면 라우트를 추가할 때마다 가드 파일도 함께 고쳐야 합니다.
meta만 보게 하면 라우트를 추가해도 가드는 그대로입니다.

/src/routes/guards/requiresAuth.ts
TS
1
2
3
4
5
6
7
8
9
import type { RouteGuard } from '.' export const requiresAuth: RouteGuard = { guard(to) { if (!to.meta.auth) return true return !!localStorage.getItem('accessToken') }, redirect: to => ({ path: '/signin', query: { redirectTo: to.fullPath } }) }
meta만 보는 가드

다만 to.meta의 속성은 기본적으로 타입이 정해져 있지 않아서, authisAuth처럼 잘못 써도 타입 검사에 걸리지 않습니다.
RouteMeta 인터페이스를 확장해 두면 규칙을 지침이 아니라 타입으로 강제할 수 있습니다.

/src/routes/router.d.ts
TS
1
2
3
4
5
6
7
8
9
import 'vue-router' declare module 'vue-router' { interface RouteMeta { auth?: boolean guestOnly?: boolean layout?: 'Default' | 'Empty' } }
meta 속성 타입 정의

이렇게 하면 정의하지 않은 meta 속성을 사용하는 순간 npm run typecheck에서 걸립니다.
지침으로만 작성한 규칙과 달리, 타입은 어길 수 없습니다.

RouteMeta 확장은 한 곳에서만 관리하세요.
Vue Router 핵심 정리를 따라 /src/routes/guards/index.ts에 이미 선언했다면, 그 선언을 이 파일로 옮겨 모읍니다.
선언이 여러 곳에 흩어져도 병합되어 동작하지만, 어떤 속성이 있는지 확인하려면 파일을 모두 열어 봐야 합니다.

# 검증 루프

지금까지 지시하는 개념을 살펴봤는데, 이제는 결과를 확인하는 개념에 대해서 살펴봅시다.
AI가 만든 Vue 코드에서 다음과 같이 타입, 규칙, 동작을 검증할 수 있습니다.

검증 명령 잡아내는 것
타입 npm run typecheck 템플릿 표현식, Props 타입, 스토어 상태 타입
규칙 npm run lint Vue 스타일 규칙, 사용하지 않는 변수, 안티패턴
동작 npm run test:unit 렌더링 결과, 이벤트, 상태 변화

대부분의 경우 타입 검사가 가장 중요합니다.
<template> 안의 오류는 편집기를 열지 않으면 드러나지 않기 때문입니다.
존재하지 않는 속성 참조, 문자열 자리에 들어간 ref 객체, 잘못 작성한 Props 이름이 모두 여기서 걸립니다.

BASH
1
2
3
4
npm run typecheck ## src/components/MovieCard.vue:14:20 - error TS2339: ## Property 'year' does not exist on type 'Movie'. Did you mean 'Year'?
템플릿 오류가 드러나는 지점

단위 테스트는 Vitest와 Vue Test Utils로 작성합니다.
mount가 DOM을 필요로 하므로 패키지 설치와 함께 테스트 환경도 지정해야 합니다.

BASH
1
npm i -D vitest @vue/test-utils jsdom
테스트 패키지 설치
/vite.config.ts
TS
1
2
3
4
5
6
7
8
9
10
11
12
import { defineConfig } from 'vitest/config' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], resolve: { alias: [{ find: '~', replacement: '/src' }] }, test: { environment: 'jsdom' } })
테스트 환경 지정

vitest/configdefineConfig는 Vite 설정을 그대로 확장하므로, 기존 vite 패키지에서 가져오던 것을 바꿔도 나머지 설정은 그대로 동작합니다.

컴포넌트의 렌더링 결과와 이벤트 발생을 확인하는 수준이면 충분하고, 이 정도는 AI에게 맡겨도 결과가 안정적인 편입니다.

/src/components/MovieCard.test.ts
TS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { mount } from '@vue/test-utils' import { describe, it, expect } from 'vitest' import MovieCard from './MovieCard.vue' const movie = { imdbID: 'tt0111161', Title: 'The Shawshank Redemption', Poster: 'N/A', Year: '1994' } describe('MovieCard', () => { it('제목과 연도를 표시한다', () => { const wrapper = mount(MovieCard, { props: { movie } }) expect(wrapper.text()).toContain('The Shawshank Redemption') expect(wrapper.text()).toContain('1994') }) it('클릭하면 imdbID와 함께 select 이벤트를 발생시킨다', async () => { const wrapper = mount(MovieCard, { props: { movie } }) await wrapper.trigger('click') expect(wrapper.emitted('select')?.[0]).toEqual(['tt0111161']) }) })
Vue Test Utils 기반 단위 테스트

검증을 매번 사람이 요청하지 않도록 Hooks로 자동화할 수 있습니다.
PostToolUse 훅은 도구 호출이 성공한 뒤에 실행되므로, Claude가 *.vue 파일을 편집한 직후 검사를 실행하기에 적합합니다.

/.claude/settings.json
JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "node", "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-vue.mjs"] } ] } ] } }

훅은 stdin으로 JSON을 받습니다.
아래 스크립트는 그 JSON에서 편집된 파일 경로를 추출해서 확장자에 따라 ESLint를 실행하고, 규칙 위반이 있으면 0이 아닌 종료 코드로 Claude에게 결과를 되돌려 줍니다.

/.claude/hooks/check-vue.mjs
JS
1
2
3
4
5
6
7
8
9
10
import { spawnSync } from 'node:child_process' let raw = '' for await (const chunk of process.stdin) raw += chunk const filePath = JSON.parse(raw).tool_input?.file_path ?? '' if (!/\.(vue|ts)$/.test(filePath)) process.exit(0) const { status } = spawnSync(`npx eslint "${filePath}"`, { shell: true, stdio: 'inherit' }) process.exit(status ?? 0)

셸 스크립트(.sh)가 아니라 Node.js 스크립트를 사용했습니다.
Node.js는 여러 운영체제에서 같은 방식으로 동작하므로 팀원의 운영체제가 섞여 있어도 그대로 사용할 수 있습니다.

파일을 편집할 때마다 전체 타입 검사를 실행하면 대기 시간이 길어집니다.
훅에서는 편집된 파일만 대상으로 하는 빠른 검사를 실행하고, 타입 검사는 작업이 끝나는 시점에 한 번 실행하도록 지침에 작성하는 것이 낫습니다.

화면 동작까지 확인하려면 브라우저를 연결합니다.
검색어를 입력했을 때 목록이 갱신되는지, 빈 결과에서 안내 문구가 나오는지는 코드만 봐서는 판단하기 어렵습니다.

Claude Code에는 브라우저를 조작하는 기능이 없으므로 MCP 서버를 먼저 등록합니다.
크로스 브라우저 자동화에는 Playwright MCP를, 성능 지표나 네트워크 진단에는 Chrome DevTools MCP를 사용합니다.
둘 다 Anthropic 공식 마켓플레이스에 플러그인으로 올라와 있어서 슬래시 명령으로 설치할 수 있습니다.

PROMPT
1
2
/plugin install playwright@claude-plugins-official /plugin install chrome-devtools-mcp@claude-plugins-official
플러그인으로 설치

claude-plugins-official은 기본으로 등록된 마켓플레이스라 따로 추가하지 않아도 됩니다.
/plugin 명령만 입력하면 목록을 둘러보면서 고를 수도 있습니다.

패키지 이름이나 실행 옵션을 직접 지정하려면 claude mcp add 명령을 사용합니다.

BASH
1
2
claude mcp add playwright -- npx @playwright/mcp@latest claude mcp add chrome-devtools -- npx chrome-devtools-mcp@latest
MCP 서버를 직접 등록

-- 뒤에 실행할 명령을 작성합니다.
구분자를 생략하면 뒤따르는 옵션을 Claude Code 자신의 옵션으로 해석해서 등록에 실패합니다.
등록 결과는 /mcp 명령으로 확인할 수 있고, 개발 서버는 MCP가 대신 실행해 주지 않으므로 npm run dev를 미리 실행합니다.

PROMPT
1
2
3
개발 서버가 5173 포트에서 실행 중이야. 브라우저로 접속해서 검색창에 "matrix"를 입력하고, 목록이 렌더링되는지와 콘솔 오류가 없는지 확인해 줘.
브라우저로 결과 확인

# 공식 스킬

스킬을 직접 만들기 전에 사용 중인 라이브러리가 공식 스킬을 배포하는지 확인합니다.
API는 만든 쪽이 정리한 것이 가장 정확하고, 문서가 바뀌면 스킬도 함께 갱신됩니다.

Vue 생태계는 React 쪽과 배포 방식이 다릅니다.
React는 vercel-labs/agent-skills처럼 모범 사례를 별도의 저장소 하나에 모아 두지만, Vue 쪽은 각 라이브러리가 자신의 저장소 안에 skills/ 폴더를 두고 함께 배포합니다.

제공 스킬 설치
VueUse vueuse-functions npx skills add vueuse/skills
Nuxt UI nuxt-ui npx skills add nuxt/ui

공식 스킬이 있는지는 해당 저장소의 skills/ 폴더를 확인하면 알 수 있습니다.
이 방식을 알아 두면 다른 라이브러리에도 그대로 적용할 수 있습니다.

vuejs/corevuejs/router, vuejs/pinia에는 아직 skills/ 폴더가 없습니다.
프레임워크 문법 자체를 가르치는 공식 스킬은 없고, 개별 라이브러리 단위로만 제공되는 상태입니다.

VueUse 스킬은 함수 선택을 돕습니다.
함수가 200개가 넘어서 목록을 모르면 이미 있는 기능을 직접 구현할 수 있는데, 스킬을 사용하면 요구사항에 맞는 함수를 찾을 수 있습니다.

PROMPT
1
검색 입력에 디바운스를 적용해 줘.
VueUse 스킬이 설치된 상태의 요청

이 요청에서 setTimeout으로 직접 구현하는 대신 refDebounceduseDebounceFn을 쓰도록 유도하는 것이 스킬의 역할입니다.

이 스킬에는 함수마다 호출 규칙이 붙어 있습니다.

호출 규칙 의미
AUTO 해당되는 상황이면 자동으로 사용
EXTERNAL 필요한 외부 의존성이 이미 설치된 경우에만 사용
EXPLICIT_ONLY 사용자가 명시적으로 요청할 때만 사용

프롬프트나 지침 파일이 이 규칙을 덮어쓸 수 있다고 스킬 본문에 명시되어 있습니다.
프로젝트 규칙이 외부 스킬보다 우선한다는 뜻입니다.

Nuxt UI 스킬은 Nuxt 전용이 아니라 Vue + Vite 프로젝트에서도 동작합니다.
SKILL.md에는 개요만 작성하고, 컴포넌트별 Props와 슬롯, 이벤트 같은 상세 API는 MCP 서버에서 제공합니다.
컴포넌트 125개의 API를 본문에 전부 추가하면 컨텍스트를 감당할 수 없기 때문입니다.
판단 기준은 스킬에 담고 대량의 레퍼런스는 외부에 두는 설계입니다.

/.mcp.json
JSON
1
2
3
4
5
6
7
8
{ "mcpServers": { "nuxt-ui": { "type": "http", "url": "https://ui.nuxt.com/mcp" } } }
Nuxt UI 문서 MCP 연결

공식 스킬이 없는 영역은 커뮤니티 스킬로 보완할 수 있습니다.
Vue 3 전반을 다루는 vuejs-ai/skills 같은 모음이 있고, Composition API와 Vue Router, Pinia, 테스트를 각각 별도의 스킬로 나눠 제공합니다.

조직 계정 이름이 공식처럼 보여도 실제로는 아닌 경우가 있습니다.
vuejs-ai/skills는 README에 Vue 팀과 무관하다고 명시되어 있습니다.
설치 전에 README에서 공식 여부와 마지막 갱신 시점을 확인하세요.

외부 스킬은 프로젝트 규칙과 충돌할 수 있습니다.
스킬이 셋업 스토어를 권하는데 프로젝트 규칙은 옵션 스토어로 고정해 두었다면, 앞에서 살펴본 모순되는 지침 상황이 됩니다.
설치 후에는 스킬 본문을 한 번 확인하고, 어긋나는 부분이 있으면 CLAUDE.md에 무엇을 따를 것인지 작성합니다.

npx skills add는 프로젝트 루트에 skills-lock.json을 만들어 설치한 스킬의 출처와 해시를 기록합니다.
팀원 모두가 같은 스킬을 사용하도록 이 파일은 Git에 커밋합니다.

공식 스킬이 없는 라이브러리는 문서 MCP로 보완합니다.
Context7 같은 서버를 연결하면 Vue와 Pinia, Vue Router의 최신 문서를 참고해서 답변하도록 할 수 있습니다.
버전에 따라 권장 방식이 바뀌는 API는 기억에 의존한 답변보다 문서를 참고한 답변이 정확합니다.

# 스킬 작성

공식 스킬이 채우지 못하는 프로젝트 고유의 규칙과 절차는 직접 만듭니다.

스킬은 폴더 하나와 SKILL.md 파일 하나로 만듭니다.
프론트매터에 namedescription만 있으면 동작하고, 본문에는 수행할 절차를 작성합니다.

/.claude/skills/check/SKILL.md
MD
1
2
3
4
5
6
7
8
9
10
11
12
--- name: check description: 타입 검사와 린트, 단위 테스트를 순서대로 실행합니다. --- # 검증 실행 다음 명령을 순서대로 실행하고, 실패한 명령이 있으면 그 출력을 그대로 보고합니다. 1. `npm run typecheck` 1. `npm run lint` 1. `npm run test:unit`
가장 단순한 형태의 스킬

파일을 저장하면 폴더 이름이 그대로 슬래시 명령이 됩니다.

PROMPT
1
/check
슬래시 명령으로 실행

매번 "검증해 줘"라고 설명하고 결과를 확인하는 대신, 같은 절차를 같은 순서로 실행할 수 있습니다.

규칙과 명령이 늘어나면 어떤 내용을 어디에 배치할 것인지 정해야 합니다.
다음 세 가지 위치는 성격이 다릅니다.

위치 로드 시점 담을 내용
CLAUDE.md 항상 프로젝트 전체에 적용되는 짧은 결정
.claude/rules/ 항상, paths 지정 시 해당 파일 작업할 때만 영역별 규칙 묶음
.claude/skills/ 이름과 설명은 항상, 본문은 사용할 때 절차가 있는 작업, 슬래시 명령

CLAUDE.md와 규칙 파일은 매 세션 컨텍스트를 차지하므로 짧게 유지합니다.
반면 스킬은 본문이 실제로 사용할 때만 로드되므로 길어도 부담이 적습니다.
절차가 여러 단계이거나 참조 문서가 필요한 작업은 스킬로 분리합니다.

Vue 프로젝트에서 스킬로 만들 만한 후보는 다음과 같습니다.

  • 프로젝트 초기 구성: Vite 생성부터 ESLint, Prettier, 경로 별칭, 폴더 구조까지 한 번에
  • 페이지 추가: 페이지 컴포넌트 생성, definePage 작성, 레이아웃 연결까지 한 묶음
  • 컴포넌트 리뷰: 규칙 위반 여부를 읽기만 하고 지적

이 중 리뷰 스킬은 사용할 도구를 제한해, 검토 중에 파일이 바뀌지 않도록 만들 수 있습니다.

/.claude/skills/vue-review/SKILL.md
MD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
--- name: vue-review description: | Vue 컴포넌트가 프로젝트 규칙을 지키는지 파일 수정 없이 검토합니다. 컴포넌트 리뷰를 요청하거나 규칙 위반을 확인할 때 사용하세요. vue review, SFC, 코드 리뷰. argument-hint: 검토할 경로 (예: src/components) allowed-tools: Read, Grep, Glob disallowed-tools: Edit, Write --- # Vue 컴포넌트 리뷰 인수로 경로를 받으면 그 경로 아래의 `*.vue` 파일만 검토하고, 인수가 없으면 `/src` 전체를 검토합니다. 파일을 읽기만 하고 수정하지 않습니다. 다음 항목을 순서대로 확인하고 위반 사항만 보고합니다. 1. `<script setup lang="ts">`를 사용하는가. 1. `reactive()`나 믹스인을 사용하지 않는가. 1. `<style>``scoped`가 있는가. 1. Props가 타입 기반 `defineProps<T>()`로 선언되었는가. 1. `watch()`로 파생 값을 만들고 있지 않은가. (`computed()`로 대체 가능한지) 1. 데이터 요청 로직이 컴포넌트 안에 직접 들어 있지 않은가. 1. `import``@/` 별칭 대신 상위 폴더 상대 경로를 사용하지 않는가. 각 위반은 `파일:줄번호` 형식으로 위치를 함께 제시합니다.

allowed-tools는 승인 없이 바로 사용할 수 있는 도구를 미리 지정하는 목록이지, 나머지 도구를 제한하는 설정이 아닙니다.
편집을 실제로 차단하려면 disallowed-tools나 권한 설정의 deny 규칙을 사용해야 합니다.

리뷰처럼 파일을 많이 읽는 작업은 Sub Agent에 맡기는 방법도 있습니다.
Sub Agent는 별도의 컨텍스트에서 실행되고 결과만 메인 세션에 전달하므로, 컴포넌트 20개를 확인해도 메인 대화에는 요약만 남습니다.
이때 확인할 항목은 프롬프트에 직접 작성해서 전달합니다.

PROMPT
1
2
3
4
5
6
7
8
9
Explore 에이전트로 src/components 아래 모든 컴포넌트를 훑어 줘. 확인할 항목은 다음 네 가지야. - <script setup lang="ts">를 사용하지 않은 파일 - reactive()나 믹스인을 사용한 파일 - <style>에 scoped가 없는 파일 - 데이터 요청 로직이 컴포넌트 안에 직접 들어 있는 파일 위반한 파일만 `파일:줄번호` 형식으로 정리해 줘.
Sub Agent에 리뷰 위임

# 버그 원인 파악

검색이 안 돼라고만 전달하면 코드를 확인하고 원인을 추측합니다.
맞을 때도 있지만, 틀리면 문제가 없던 코드까지 함께 바뀌고 무엇이 원인이었는지도 남지 않습니다.
원인을 좁히려면 관찰한 사실을 그대로 전달해야 합니다.

전달할 것은 네 가지입니다.

항목 내용
재현 절차 어떤 화면에서 무엇을 했을 때 발생하는지
기대와 실제 무엇을 기대했고 실제로는 무엇이 나왔는지
원문 콘솔 오류, 터미널 출력, 네트워크 응답을 요약하지 않고 그대로
범위 관련 파일 경로와 최근에 바뀐 부분

이 중 에러 메시지의 원문을 요약하지 않는 것이 특히 중요합니다.
타입 오류가 났어와 달리 error TS2339: Property 'year' does not exist on type 'Movie' 같이 에러 내용과 파일, 줄 번호, 잘못 쓴 속성 이름 등이 들어 있을 수 있기에 훨씬 더 정확하게 문제를 파악할 수 있습니다.

PROMPT
1
검색이 안 돼. 고쳐 줘.
증상만 전달한 요청
PROMPT
1
2
3
4
5
6
7
8
@/pages/movies.vue에서 검색창에 "matrix"를 입력하고 엔터를 눌렀을 때, 목록이 갱신되지 않고 이전 결과가 그대로 남아 있어. 콘솔에는 다음 경고가 출력돼. [Vue warn]: Property "movies" was accessed during render but is not defined on instance. 네트워크 탭에서는 omdbapi.com 요청이 200으로 성공했고 응답에 Search 배열이 들어 있어. 관련 파일은 @/composables/useMovieSearch.ts와 @/components/MovieList.vue야.
관찰한 사실을 전달한 요청

Vue에서 반복해서 나오는 증상은 원인의 후보가 정해져 있습니다.
이 목록을 규칙 파일에 작성하면 매번 설명하지 않아도 탐색 범위가 좁아집니다.

증상 확인할 곳
값을 바꿨는데 화면이 갱신되지 않음 reactive() 객체의 구조 분해, ref.value 누락
스토어 값이 화면에 반영되지 않음 storeToRefs() 없이 스토어를 구조 분해
템플릿에서 값이 undefined Props 이름 불일치, 비동기 데이터가 도착하기 전 접근
목록을 정렬하거나 삭제하면 상태가 섞임 v-forkey에 인덱스 사용
라우트가 바뀌어도 화면이 그대로 같은 컴포넌트를 재사용하는 경로에서 onMounted만 사용

수정보다 진단을 먼저 요구하는 것이 낫습니다.
원인 후보를 몇 가지로 좁히고 각각을 어떻게 확인할 것인지 물어보면, 코드를 바꾸지 않고도 범위를 확인할 수 있습니다.

PROMPT
1
2
3
아직 코드를 고치지 마. 위 증상의 원인 후보를 3개까지 좁히고, 각각을 어떻게 확인할 수 있는지 알려 줘. 확인 방법은 코드를 바꾸지 않고 콘솔 출력이나 Vue Devtools로 볼 수 있는 것으로 제시해 줘.
수정 없이 원인만 요청

브라우저를 연결하면 이 관찰을 직접 하게 할 수 있습니다.
Playwright MCP나 Chrome DevTools MCP를 연결하면 화면을 조작하고 콘솔과 네트워크 응답을 확인하므로, 사람이 오류를 복사해서 전달하는 단계가 사라집니다.

PROMPT
1
2
3
개발 서버가 5173 포트에서 실행 중이야. 브라우저로 접속해서 검색창에 "matrix"를 입력하고 엔터를 눌러 줘. 그다음 콘솔 오류와 omdbapi.com 요청의 응답을 읽고, 목록이 나오지 않는 원인을 알려 줘.
브라우저로 직접 재현

원인을 확인했으면 고치기 전에 테스트를 먼저 만듭니다.
그 조건에서 실패하는 테스트가 있어야 수정이 실제로 그 문제를 해결했는지 확인할 수 있고, 같은 버그가 다시 들어오는 것도 막을 수 있습니다.

PROMPT
1
2
원인을 확인했으면 그 조건에서 실패하는 테스트를 먼저 작성해 줘. 테스트가 실패하는 것을 확인한 다음에 수정하고, 마지막으로 단위 테스트를 실행해 줘.
재현 테스트를 먼저 작성

증상이 여러 개일 때 한 번에 모두 전달하지 마세요.
원인이 서로 다르면 수정이 뒤섞여서 어떤 변경이 무엇을 고쳤는지 알 수 없게 됩니다.

# 리팩토링

AI와 함께 작업하면 코드가 빠르게 쌓이고, 그중 일부는 규칙에서 조금씩 벗어난 상태로 남습니다.
어긋난 코드를 그대로 두면 다음 작업에서 참고 대상이 되어 같은 방식이 복제되므로, 쌓이기 전에 되돌리는 편이 비용이 적습니다.
리팩토링은 이 어긋남을 주기적으로 정리하는 작업입니다.

리팩토링은 동작을 바꾸지 않는 변경입니다.
동작이 그대로인지 확인할 수단이 없으면 리팩토링과 기능 변경을 구분할 수 없으므로, 순서가 정해져 있습니다.

  1. 대상의 현재 동작을 테스트로 고정합니다.
  2. Plan 모드로 무엇을 어디까지 바꿀지 확정합니다.
  3. 변경합니다.
  4. typecheck, lint, test:unit을 모두 통과시킵니다.

첫 단계를 건너뛰면 리팩토링 후에 화면을 눈으로 확인하는 것 말고는 방법이 없습니다.

PROMPT
1
2
3
@/components/MovieCard.vue를 리팩토링하기 전에, 지금 동작을 확인하는 테스트를 먼저 작성해 줘. 렌더링 결과와 select 이벤트 발생까지 확인하면 충분해. 테스트가 통과하는 것을 확인한 다음에 멈추고, 리팩토링은 아직 하지 마.
현재 동작을 테스트로 고정

무엇을 고칠 것인지는 이미 규칙 파일에 작성되어 있습니다.
.claude/rules/vue.md의 각 줄이 그대로 검사 항목이 되므로, 앞에서 만든 리뷰 스킬로 위반 목록을 추출하고 그 목록을 작업 목록으로 사용하면 됩니다.

PROMPT
1
2
3
4
/vue-review src/components 보고된 위반 중 reactive() 사용과 scoped 누락만 먼저 고쳐 줘. 파일 하나를 고칠 때마다 npm run typecheck를 실행하고, 통과하면 다음 파일로 넘어가.
위반 목록을 작업 목록으로

Vue 프로젝트에서 반복해서 나오는 리팩토링은 대체로 다음 다섯 가지입니다.

대상 바꾸는 이유
reactive()로 선언된 상태 원시형에서 반응성을 잃고 구조 분해에서 끊어짐
watch()로 만든 파생 값 상태가 늘고 두 값의 동기화 책임이 사람에게 넘어옴
컴포넌트 안의 데이터 요청 컴포넌트가 커지고 같은 요청을 재사용할 수 없음
상위 폴더로 올라가는 상대 경로 파일을 옮길 때마다 경로가 깨짐
export default로 작성된 컴포넌트 나머지 파일과 구조가 달라 읽는 비용이 늘어남

이 중 커진 컴포넌트를 나누는 작업은 판단이 필요합니다.
줄 수를 기준으로 나누면 의미 없는 조각이 생기므로, 어디를 분리할 것인지는 사람이 정하고 그 경계를 지시에 포함합니다.

PROMPT
1
2
3
4
5
6
7
8
9
10
11
12
@/pages/movies.vue가 300줄을 넘었어. 검색 입력 부분만 SearchBar 컴포넌트로 분리해 줘. Props: modelValue: string Emits: update:modelValue: (value: string) => void search: () => void - 검색어 상태는 movies.vue가 계속 관리한다. - SearchBar는 입력과 제출만 담당하고 API를 직접 호출하지 않는다. - 분리 후 기존 테스트가 그대로 통과해야 한다.
경계를 정해 분리 요청

전체적으로 리팩토링해 줘 같은 요청은 피하세요.
한 번에 많은 파일이 바뀌면 검토가 어려워지고, 문제가 생겼을 때 되돌릴 위치도 사라집니다.
최소 단위로 수정하고 검증한 뒤 커밋하는 단위를 유지하는 것이 좋습니다.

# PR 리뷰

리뷰 스킬이 파일 하나가 규칙을 지키는지 본다면, PR 리뷰는 기준이 다릅니다.
변경된 파일들이 서로 어떻게 연결되는지, 상태를 들고 있는 위치와 컴포넌트 경계가 이번 변경으로 흐트러지지 않았는지를 봅니다.
파일마다 규칙을 지키면서도 전체 구조가 어긋나는 경우가 있기 때문입니다.

브랜치와 PR, 병합의 일반적인 흐름은 사례로 이해하는 GitHub Flow를 참고하세요.
여기서는 Vue 프로젝트의 변경분을 무엇을 기준으로 볼지에 집중합니다.

PR을 올리기 전에 로컬에서 변경분만 먼저 확인합니다.
작업 브랜치와 main의 차이를 대상으로 삼으면 이번에 건드리지 않은 파일은 리뷰에서 빠집니다.

PROMPT
1
2
3
4
5
6
7
main과 현재 브랜치의 차이를 확인하고 이번 변경만 리뷰해 줘. 파일은 수정하지 말고 다음 네 가지를 순서대로 정리해 줘. 1. 이번 변경으로 추가되거나 이동한 상태가 어디에 있는지 2. 컴포넌트 사이의 데이터 흐름이 Props와 Emits를 벗어나는 곳이 있는지 3. .claude/rules/의 규칙을 어긴 부분 4. 테스트가 없는 새 컴포넌트나 컴포저블
변경분 리뷰

지적 사항을 반영한 뒤에는 검증 명령을 다시 실행합니다.
반영 과정에서 새로운 타입 오류가 생길 수 있으므로, 반영과 검증을 한 번의 지시로 묶는 것이 안전합니다.
반영하지 않을 항목은 이유와 함께 남겨 두면 리뷰를 다시 받을 때 같은 지적이 반복되지 않습니다.

PROMPT
1
2
3
1번과 3번 지적을 반영해 줘. 2번은 이번 작업 범위가 아니니 그대로 두고, 왜 남겨 두는지 PR 본문에 한 줄로 적어 줘. 반영이 끝나면 typecheck, lint, test:unit을 순서대로 실행해서 통과를 확인해 줘.
반영과 재검증

검증까지 통과하면 PR을 만듭니다.
Claude가 gh 명령을 사용하므로 GitHub CLI를 먼저 설치하고 로그인해야 합니다.

BASH
1
2
3
4
5
6
7
8
## macOS brew install gh ## Windows winget install --id GitHub.cli --source winget gh auth login gh auth status
GitHub CLI 설치와 로그인

gh auth login은 브라우저와 대화형 입력을 사용하므로 Claude에게 맡기지 말고 직접 실행합니다.
로그인하지 않으면 gh pr create가 요청을 보내기 전에 실패합니다.
매번 승인을 묻지 않도록 gh 명령도 프로젝트 설정에 등록합니다.

/.claude/settings.json
JSON
1
2
3
4
5
{ "permissions": { "allow": ["Bash(gh:*)"] } }
gh 명령 허용
PROMPT
1
2
현재 브랜치의 커밋을 정리해서 PR을 만들어 줘. 제목은 한 줄로 쓰고, 본문에는 바뀐 화면과 새로 생긴 파일의 역할, 검증 명령의 실행 결과를 적어 줘.
PR 생성

PR 설명은 업로드 전에 꼭 직접 읽어야 합니다.
커밋 내역만 확인해서 작성하므로, 실제로 하지 않은 작업이 설명에 들어가는 경우가 발생할 수 있습니다.

# 성능 측정

화면이 완성되면 성능을 숫자로 확인합니다.
Lighthouse는 페이지를 실제로 한 번 열어 보고 성능, 접근성, 권장 사항, SEO를 점수로 매기는 도구입니다.

개발 서버(npm run dev)에서 측정한 점수는 의미가 없습니다.
소스맵과 HMR 코드가 함께 로드되고 번들도 압축되지 않은 상태이기 때문입니다.
npm run build > npm run preview 결과에서 측정하세요!

성능 점수는 몇 가지 지표를 합산한 값입니다.
Vue 프로젝트에서 실제로 손댈 수 있는 지표는 다음 세 가지입니다.

지표 의미 자주 나오는 원인
LCP 가장 큰 콘텐츠가 표시되기까지의 시간 첫 화면 번들 크기, 포스터 이미지 용량
CLS 화면이 밀리는 정도 이미지에 크기를 지정하지 않음, 로딩 후 목록이 끼어듦
INP 입력에 화면이 반응하기까지의 시간 입력마다 큰 목록을 다시 계산

측정은 스킬로 자동화할 수 있습니다.
다음 스킬은 프로젝트 설정 파일에서 프레임워크를 감지하고, 개발 서버와 프로덕션 빌드 중 무엇을 측정할지 물어본 뒤 빌드와 프리뷰 서버 실행까지 처리합니다.

BASH
1
npx skills add ParkYoungWoong/skills --skill lighthouse
Lighthouse 스킬 설치
PROMPT
1
/lighthouse
슬래시 명령으로 측정

측정 결과를 그대로 전달하면 개선 항목을 정리해 줍니다.
이때 무엇을 바꿔도 되는지 범위를 함께 정해 주는 것이 좋습니다.

PROMPT
1
2
Lighthouse 결과에서 성능 점수를 떨어뜨리는 항목 세 가지를 고르고, 각각의 원인을 코드에서 찾아 줘. 고치기 전에 무엇을 바꿀지 먼저 알려 주고, 화면에 보이는 동작은 바꾸지 마.
측정 결과로 개선 요청

Vue 프로젝트에서 자주 적용하는 개선은 다음과 같습니다.
대부분은 규칙 파일에 미리 추가할 수 있는 내용이라, 나중에 고치기보다 처음부터 규칙으로 작성하는 것이 낫습니다.

개선 방법
첫 화면 번들 축소 파일 기반 라우팅의 자동 지연 로딩 유지, 첫 화면만 importMode로 예외 처리
이미지로 인한 밀림 제거 <img>widthheight 지정
화면 밖 이미지 지연 <img>loading="lazy" 지정
입력마다 발생하는 요청 축소 VueUse의 refDebounced로 검색어 디바운스
커진 번들의 원인 확인 npx vite-bundle-visualizer로 구성 확인

단순히 점수만 목표로 하면 화면 동작을 수정하는 경우가 발생할 수 있습니다.
성능 점수를 90점 이상으로 올려 줘보다 이 항목의 원인을 찾아서 개선해 줘가 훨씬 안전합니다.
개선 후에는 단위 테스트나 E2E 테스트로 동작이 변화가 없는지 확인해야 합니다.

# 배포

배포 단계에서는 주소를 직접 입력하거나 새로고침했을 때 404가 발생하지 않는지, 그리고 환경 변수가 빌드에 제대로 포함되었는지 등을 확인해야 합니다.

먼저 API Key처럼 코드에 직접 작성했던 값을 환경 변수로 분리해야 합니다.
Vite는 VITE_ 접두사가 붙은 변수만 클라이언트 코드에 노출하므로, 이름 규칙을 지시에 함께 작성합니다.

PROMPT
1
2
3
4
OMDb API Key를 환경 변수로 분리해 줘. 변수 이름은 VITE_OMDB_API_KEY로 하고 .env.local에 두되, .gitignore에 추가해 줘. 값은 내가 직접 넣을 테니 .env.example에는 이름만 적어 줘. import.meta.env의 타입도 함께 정의해 줘.
환경 변수 분리 요청

프로젝트 준비에서 등록한 deny 규칙 때문에 Claude는 .env.local을 읽지 못합니다.
그래서 값이 아니라 변수 이름을 지시에 작성해야 합니다.

거부 규칙은 Claude가 파일을 직접 열거나 고치는 것을 제한합니다.
반대로 Claude가 실행시킨 프로그램이 그 파일을 읽는 것까지는 제한하지 못합니다.

제한됨 제한되지 않음
Claude가 직접 읽기: Read, Grep, @파일 멘션, cat 같은 명령 실행한 스크립트가 읽기: node script.js
Claude가 직접 고치기: 같은 경로의 Edit, Write 빌드 도구가 값을 읽어 결과물에 포함: npm run build

거부 규칙은 Claude Code가 실행하는 도구와 명령을 대상으로 합니다.
프로세스 수준까지 제한하려면 샌드박스를 활성화해야 합니다.

/src/vite-env.d.ts
TS
1
2
3
4
5
6
7
8
9
/// <reference types="vite/client" /> interface ViteTypeOptions { strictImportMetaEnv: unknown } interface ImportMetaEnv { readonly VITE_OMDB_API_KEY: string }
환경 변수 타입 정의

ImportMetaEnv에는 기본적으로 모든 이름을 받아들이는 인덱스 시그니처가 있습니다.
그래서 타입을 선언해도 import.meta.env.VITE_OMDB_KEY처럼 이름을 잘못 작성한 코드가 any로 통과합니다.
ViteTypeOptionsstrictImportMetaEnv를 선언하면 이 시그니처가 사라지고, 정의하지 않은 이름을 사용하는 순간 타입 검사에서 걸립니다.

BASH
1
2
3
4
npm run typecheck ## error TS2551: Property 'VITE_OMDB_KEY' does not exist on type 'ImportMetaEnv'. ## Did you mean 'VITE_OMDB_API_KEY'?
이름을 잘못 썼을 때

ViteTypeOptions를 통한 엄격 검사는 Vite 6.3 버전부터 사용할 수 있습니다.

VITE_ 접두사가 붙은 변수는 빌드 결과물에 문자열 그대로 포함됩니다.
브라우저에서 그대로 확인할 수 있으므로 비밀 키를 작성하면 안 됩니다.

다음은 새로고침 문제입니다.
Vue Router의 HTML5 모드를 사용하려면 모든 요청을 index.html로 전달하는 서버 설정이 필요합니다.
이 설정이 없으면 목록에서 상세 페이지로 이동하는 것은 되지만, 상세 페이지 주소를 직접 열면 404가 납니다.

/vercel.json
JSON
1
2
3
{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }

빌드 설정도 배포 전에 확인합니다.
Vercel에서 프레임워크를 감지해 빌드 명령과 출력 폴더를 자동으로 입력하지만, package.json의 스크립트를 바꿨다면 어긋날 수 있습니다.

PROMPT
1
2
3
배포 전에 빌드가 정상인지 확인해 줘. npm run build를 실행하고 출력 폴더 이름과 결과물 크기, 경고가 있으면 알려 줘. 그리고 이 프로젝트에 맞는 Vercel 빌드 명령과 출력 폴더 값을 알려 줘.
빌드 설정 확인

배포는 GitHub 저장소를 Vercel 프로젝트에 연결하면 푸시할 때마다 자동으로 진행됩니다.

  1. Vercel에 GitHub 계정으로 로그인하고 Add New > Project를 선택합니다.
  2. 배포할 저장소를 가져오고, 감지된 프레임워크와 빌드 명령, 출력 폴더(dist)를 확인합니다.
  3. Environment VariablesVITE_OMDB_API_KEY와 값을 등록합니다.
  4. Deploy를 눌러 첫 배포를 진행합니다.

main 브랜치는 프로덕션으로, 나머지 브랜치와 PR은 프리뷰로 배포되므로 앞에서 만든 PR을 그대로 확인용 주소로 사용할 수 있습니다.
환경 변수는 프로덕션과 프리뷰, 로컬 개발에 각각 다른 값을 지정할 수 있습니다.
로컬의 .env.local은 저장소에 업로드되지 않으므로, 따로 등록하지 않으면 배포 환경에는 값이 존재하지 않습니다.

Vite의 환경 변수는 빌드 시점에 문자열로 치환됩니다.
Vercel에서 값을 바꿔도 다시 배포하기 전까지는 반영되지 않습니다.

배포 상태는 Vercel 대시보드의 Deployments에서 확인합니다.
각 배포에는 빌드 로그가 남으므로, 실패했을 때는 로그의 마지막 오류를 그대로 복사해서 전달하는 것이 가장 빠릅니다.
로컬에서는 통과하는데 배포에서만 실패한다면 원인은 대개 정해져 있습니다.

증상 원인
타입 오류로 빌드 실패 로컬에서 typecheck 없이 dev만 실행
대소문자가 다른 파일을 찾지 못함 macOS는 대소문자를 구분하지 않지만 배포 환경은 구분
환경 변수가 undefined Vercel에 값을 등록하지 않았거나 등록 후 재배포하지 않음
상세 경로에서만 404 vercel.json의 재작성 설정 누락

배포가 끝나면 확인도 맡길 수 있습니다.

PROMPT
1
2
3
배포된 주소로 접속해서 검색이 동작하는지 확인해 줘. 그리고 /movies/tt0111161 같은 상세 경로를 주소창에 직접 입력했을 때 404가 나지 않는지도 확인해 줘. 콘솔 오류가 있으면 요약하지 말고 원문 그대로 알려 줘.
배포 결과 확인

# 핵심 정리

  1. Vite로 Vue + TypeScript 프로젝트를 만들고 ESLint, Prettier, 경로 별칭을 구성합니다.
  2. typecheck, lint 스크립트를 추가하고 .claude/settings.json에 실행 권한을 등록합니다.
  3. /init으로 CLAUDE.md 초안을 만든 뒤, Vue 규칙을 직접 작성합니다.
  4. 규칙 없이 컴포넌트를 만들어 보고, 규칙을 넣은 뒤 같은 요청을 반복해 결과 차이를 확인합니다.
  5. Plan 모드로 영화 검색 화면의 파일 구성을 계획하고, 인터페이스를 먼저 정해 컴포넌트를 만듭니다.
  6. 검색 로직을 컴포저블로 분리하고, typecheck로 템플릿 오류를 잡아 봅니다.
  7. 규칙이 늘어나면 .claude/rules/로 나누고, Pinia 규칙과 라우터 규칙을 작성합니다.
  8. /new-store 슬래시 명령으로 스토어를 만들고, RouteMeta 확장으로 라우트 규칙을 타입에 반영합니다.
  9. Vue Test Utils로 단위 테스트를 작성하고, PostToolUse 훅으로 검사를 자동화합니다.
  10. npx skills add vueuse/skills로 공식 스킬을 설치하고, 설치 전후로 같은 요청을 보내 결과를 비교합니다.
  11. 리뷰 스킬을 만들어 지금까지 만든 컴포넌트 전체를 검토합니다.
  12. 버그를 하나 심어 두고, 증상만 전달했을 때와 관찰한 사실을 전달했을 때 응답이 어떻게 달라지는지 비교합니다.
  13. 리뷰 스킬이 뽑은 위반 목록을 작업 목록으로 삼아 리팩토링하고, 테스트가 그대로 통과하는지 확인합니다.
  14. 브랜치를 만들어 PR을 올리고, 변경분 리뷰와 반영 루프를 한 번 돌립니다.
  15. Lighthouse 스킬로 프로덕션 빌드를 측정하고, 지적된 항목 하나를 고쳐 다시 측정합니다.
  16. API 키를 환경 변수로 분리하고, Claude에게 .env.local을 읽어 달라고 시켜 거부 규칙이 실제로 동작을 제한하는지 확인합니다.
  17. Vercel에 배포한 뒤, 상세 경로를 주소창에 직접 입력해 확인합니다.

사람이 결정하고, 그 결정을 문서로 고정하고, 도구로 지켜졌는지 검사합니다.
결정과 검증 없이 지시만 늘어나면 코드는 빠르게 쌓이겠지만 방향은 달라지기 쉽습니다.