본문으로 건너뛰기

Cursor에서 Vue.js 개발: 완전 가이드

Vue.js development in Cursor

Vue.js는 사용자 인터페이스 구축을 위한 가장 인기 있는 프로그레시브 JavaScript 프레임워크 중 하나가 되었습니다. Vue 3가 Composition API, <script setup>, 그리고 재작성된 반응성 시스템을 도입하면서, 개발자들은 모던 Vue 생태계를 깊이 이해하는 에디터가 필요하게 되었습니다. Cursor는 VS Code 호환 기반과 일류 AI 지원을 결합하여 Vue.js 개발을 위한 훌륭한 환경을 만듭니다.

이 가이드는 프로젝트 스캐폴딩부터 상태 관리, 라우팅, AI 지원 컴포넌트 생성까지 Cursor에서 생산적인 Vue.js 워크플로우를 설정하는 방법을 안내합니다. Options API 코드베이스를 Vue 3로 마이그레이션하든 새로운 SFC(단일 파일 컴포넌트) 프로젝트를 시작하든, 아래의 기술들은 코드를 깔끔하고 유지보수 가능하게 유지하면서 더 빠르게 작업할 수 있도록 도와줍니다.

Cursor에서 Vue.js 환경 설정​

전제 조건 및 Node.js 설정​

Vue 코드를 작성하기 전에 시스템이 올바른 기반을 갖추고 있는지 확인하세요. Cursor는 머신에 설치된 Node.js를 상속하므로 먼저 버전을 확인하세요.

node --version
npm --version

Vue 3와 Volar 도구 체인은 Node.js 18 이상이 필요합니다. 이전 버전을 사용 중이라면 nvm이나 fnm 같은 버전 관리자를 사용하여 업그레이드하세요:

# fnm 사용 (속도가 빨라 권장됨)
fnm install 20
fnm use 20

# nvm 사용
nvm install 20
nvm use 20
팁

Cursor의 통합 터미널은 모든 일반적인 셸 도구를 지원합니다. Ctrl+`(Windows/Linux) 또는 Cmd+`(macOS)로 열 수 있으며, 에디터를 떠나지 않고도 모든 CLI 명령을 실행할 수 있습니다.

Volar 확장 프로그램 설치​

Cursor에서 Vue.js 개발을 위한 가장 중요한 확장 프로그램은 Volar(공식 Vue.js 언어 지원, 현재 Vue - Official 확장 프로그램으로 배포됨)입니다. .vue 파일에 대한 타입 검사, IntelliSense, 포맷팅 및 진단을 제공합니다.

설치 방법:

  1. Ctrl+Shift+X(macOS에서는 Cmd+Shift+X)로 Cursor에서 확장 프로그램 보기를 엽니다.
  2. "Vue - Official"을 검색합니다.
  3. Install을 클릭합니다.
경고

이전 Vetur 확장 프로그램을 Volar와 함께 활성화된 상태로 두지 마세요. Vetur는 Vue 2용으로 설계되었으며 모던 도구 체인과 충돌합니다. 설치되어 있다면 중복 진단 및 잘못된 IntelliSense 제안을 방지하기 위해 비활성화하거나 제거하세요.

워크플로우를 보완하기 위해 다음 확장 프로그램을 선택적으로 설치할 수 있습니다:

  • ESLint — .vue 및 .js/.ts 파일 린트용.
  • Prettier — 일관된 포맷팅용.
  • TypeScript Vue Plugin (Volar) — 템플릿 내 타입 해석 개선.

새 Vue 3 프로젝트 스캐폴딩​

새 Vue 프로젝트를 만드는 권장 방법은 공식 create-vue 스캐폴딩 도구를 사용하는 것입니다. 이 도구는 Vue 코어 팀이 유지 관리하며 Vite, TypeScript, Router, Pinia를 선택적으로 통합합니다.

npm create vue@latest my-vue-app

대화형 프롬프트에서 기능을 선택할 수 있습니다:

Vue.js - The Progressive JavaScript Framework

? Project name: my-vue-app
? Add TypeScript? Yes
? Add JSX Support? No
? Add Vue Router for Single Page Application development? Yes
? Add Pinia for state management? Yes
? Add Vitest for Unit Testing? Yes
? Add ESLint for code quality? Yes
? Add Prettier for code formatting? Yes

스캐폴딩이 완료되면 Cursor에서 프로젝트를 엽니다:

cd my-vue-app
cursor .

의존성을 설치하고 개발 서버를 시작합니다:

npm install
npm run dev
팁

Cursor는 Vite 개발 서버를 자동으로 인식합니다. npm run dev를 실행하면 Cursor의 터미널에 로컬 URL(일반적으로 http://localhost:5173)이 표시됩니다. URL을 Ctrl+Click하여 기본 브라우저에서 실행 중인 앱을 열 수 있습니다.

Vue 3 Composition API 마스터하기​

<script setup> 구문 이해하기​

<script setup> 구문은 Vue 3 컴포넌트를 작성하는 권장 방법입니다. 컴파일 타임 구문적 설탕으로, Options API에 비해 보일러플레이트를 크게 줄여줍니다.

<script setup lang="ts">
import { ref, computed } from 'vue'

interface Props {
title: string
count?: number
}

const props = withDefaults(defineProps<Props>(), {
count: 0,
})

const emit = defineEmits<{
(e: 'update', value: number): void
}>()

const doubled = computed(() => props.count * 2)

function increment() {
const next = props.count + 1
emit('update', next)
}
</script>

<template>
<div class="counter">
<h2>{{ title }}</h2>
<p>Count: {{ count }} (doubled: {{ doubled }})</p>
<button @click="increment">Increment</button>
</div>
</template>

<style scoped>
.counter {
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
}
</style>
경고

defineProps와 defineEmits는 컴파일러 매크로입니다. 가져올 필요가 없으며 <script setup> 내에서만 사용할 수 있습니다. setup이 없는 일반 <script> 블록에서 사용하려고 하면 컴파일 에러가 발생합니다.

ref와 reactive를 사용한 반응형 상태​

Vue 3는 반응형 상태를 선언하는 두 가지 주요 방법을 제공합니다. 둘 중 어느 것을 선택할지는 초보자에게 흔한 혼란의 원인입니다.

<script setup lang="ts">
import { ref, reactive } from 'vue'

// ref는 원시값과 객체 모두에서 작동
const count = ref(0)
const user = ref({ name: 'Ada', age: 36 })

// reactive는 객체/배열에서만 작동
const settings = reactive({
theme: 'light',
notifications: true,
})

function toggleTheme() {
settings.theme = settings.theme === 'light' ? 'dark' : 'light'
}
</script>

<template>
<button @click="count++">{{ count }}</button>
<p>{{ user.name }} — theme: {{ settings.theme }}</p>
<button @click="toggleTheme">Toggle Theme</button>
</template>
팁

실용적인 경험칙: 원시값과 전체를 재할당할 수 있는 객체에는 ref를 사용하고, 제자리에서 변경하는 그룹화된 상태 객체에는 reactive를 사용하세요. 템플릿에서 ref는 자동으로 언래핑되므로 {{ count.value }}가 아닌 {{ count }}로 작성합니다.

재사용 가능한 로직을 위한 Composables​

Composables는 재사용 가능한 반응형 로직을 캡슐화하는 함수입니다. useSomething 명명 규칙을 따릅니다. Cursor의 AI 어시스턴트는 자연어 설명에서 composables를 생성하는 데 탁월합니다.

// composables/useFetch.ts
import { ref, watchEffect, type Ref } from 'vue'

export function useFetch<T>(url: Ref<string>) {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<string | null>(null)
const loading = ref(false)

watchEffect(async () => {
loading.value = true
error.value = null
try {
const response = await fetch(url.value)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
data.value = (await response.json()) as T
} catch (err) {
error.value = err instanceof Error ? err.message : 'Unknown error'
} finally {
loading.value = false
}
})

return { data, error, loading }
}

컴포넌트에서 사용:

<script setup lang="ts">
import { ref } from 'vue'
import { useFetch } from '../composables/useFetch'

interface Post {
id: number
title: string
body: string
}

const endpoint = ref('https://jsonplaceholder.typicode.com/posts/1')
const { data: post, error, loading } = useFetch<Post>(endpoint)
</script>

<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<article v-else-if="post">
<h2>{{ post.title }}</h2>
<p>{{ post.body }}</p>
</article>
</template>

Volar 확장 프로그램 활용​

타입 검사 템플릿​

Volar의 가장 강력한 기능 중 하나는 <template> 블록 내에서 타입 검사를 수행하는 것입니다. 이를 통해 컴포넌트 props, 이벤트 이름 및 표현식 오류를 런타임이 아닌 컴파일 타임에 잡을 수 있습니다.

<script setup lang="ts">
import Counter from './Counter.vue'
</script>

<template>
<!-- prop이 존재하지 않으면 Volar가 표시합니다 -->
<Counter title="Clicks" :count="5" @update="handleChange" />
</template>

Counter가 count prop을 선언하지 않은 경우, Volar는 속성 아래에 빨간 물결선을 표시하고 문제 패널에 진단 메시지를 표시합니다.

팁

CI 파이프라인의 일부로 vue-tsc --noEmit을 실행하여 전체 프로젝트에서 템플릿 타입 검사를 적용하세요. package.json 스크립트에 추가합니다:

{
"scripts": {
"type-check": "vue-tsc --noEmit"
}
}

워크스페이스 구성​

.vscode/settings.json 파일(Cursor와 공유)을 생성하여 팀 전체의 Volar 환경설정을 고정합니다:

{
"editor.formatOnSave": true,
"editor.defaultFormatter": "Vue.volar",
"vue.codeActions.askNewComponentName": false,
"vue.inlayHints.missingProps": true,
"vue.inlayHints.inlineHandlerLeading": true,
"typescript.tsdk": "node_modules/typescript/lib"
}

Take Over Mode (독립형 TypeScript)​

Volar는 내장 TypeScript 확장 프로그램을 "인계받아" .ts 및 .vue 파일 모두에서 더 나은 Vue 인식 IntelliSense를 제공할 수 있습니다. 워크스페이스에 .vscode/settings.json을 생성합니다:

{
"typescript.preferences.importModuleSpecifier": "relative",
"vue.server.includeLanguages": ["vue"]
}
경고

Take Over Mode는 해당 워크스페이스의 표준 TS 확장 프로그램을 비활성화합니다. Vue가 아닌 TypeScript 라이브러리가 포함된 혼합 프로젝트에서 작업하는 경우, 외부 타입 해석이 여전히 예상대로 작동하는지 신중하게 테스트하세요.

Pinia를 사용한 상태 관리​

Vuex 대신 Pinia를 선택하는 이유​

Pinia는 Vue 3의 공식 상태 관리 라이브러리입니다. Vuex의 후속작이며 여러 이점을 제공합니다:

  • mutations 없이 더 간단하고 타입 안전한 API (상태를 직접 수정합니다).
  • 전체 자동 완성을 갖춘 일류 TypeScript 지원.
  • 모듈성 — 각 store는 독립적으로 정의됩니다.
  • 뛰어난 DevTools 통합.

Store 정의​

// stores/counter.ts
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
count.value++
}

async function incrementAsync() {
await new Promise((resolve) => setTimeout(resolve, 200))
count.value++
}

return { count, doubled, increment, incrementAsync }
})

컴포넌트에서 Store 사용​

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '../stores/counter'

const counter = useCounterStore()

// 반응형 상태를 안전하게 구조 분해
const { count, doubled } = storeToRefs(counter)
// actions는 직접 구조 분해 가능
const { increment, incrementAsync } = counter
</script>

<template>
<div class="counter">
<p>Count: {{ count }} / Doubled: {{ doubled }}</p>
<button @click="increment">+1</button>
<button @click="incrementAsync">+1 (async)</button>
</div>
</template>
경고

일반 const { count } = counter로 store 상태를 구조 분해하지 마세요 — 반응성을 잃게 됩니다. state와 getters에는 항상 storeToRefs를 사용하고, actions는 일반 함수이므로 직접 구조 분해할 수 있습니다.

여러 Store 구성​

대규모 애플리케이션의 경우 도메인별로 store를 분할합니다:

src/stores/
├── auth.ts
├── cart.ts
├── products.ts
└── ui.ts

크로스 store 패턴을 통해 한 store가 다른 store를 호출할 수 있습니다:

// stores/cart.ts
import { defineStore } from 'pinia'
import { useProductsStore } from './products'

export const useCartStore = defineStore('cart', () => {
const items = ref<{ id: number; qty: number }[]>([])

function addToCart(productId: number) {
const products = useProductsStore()
if (!products.exists(productId)) {
throw new Error('Product not found')
}
items.value.push({ id: productId, qty: 1 })
}

return { items, addToCart }
})

Vue Router를 사용한 라우팅​

라우트 구성​

// router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import HomeView from '../views/HomeView.vue'

const routes: RouteRecordRaw[] = [
{ path: '/', name: 'home', component: HomeView },
{
path: '/about',
name: 'about',
component: () => import('../views/AboutView.vue'),
},
{
path: '/users/:id',
name: 'user-detail',
component: () => import('../views/UserDetailView.vue'),
props: true,
},
]

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
})

export default router

네비게이션 및 라우트 매개변수​

<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
import { computed } from 'vue'

const route = useRoute()
const router = useRouter()

const userId = computed(() => route.params.id as string)

function goHome() {
router.push({ name: 'home' })
}
</script>

<template>
<div>
<p>Viewing user #{{ userId }}</p>
<button @click="goHome">Back to Home</button>
</div>
</template>
팁

즉시 필요하지 않은 라우트 세그먼트에 대해 지연 로딩 컴포넌트(() => import(...))를 사용하세요. 이는 더 작은 초기 번들과 더 빠른 첫 페인트를 생성하며, 모바일 네트워크에서 특히 중요합니다.

네비게이션 가드​

router.beforeEach((to, from) => {
const isAuthenticated = localStorage.getItem('token') !== null
if (to.meta.requiresAuth && !isAuthenticated) {
return { name: 'login', query: { redirect: to.fullPath } }
}
})

라우트별로 meta 필드를 적용합니다:

{
path: '/dashboard',
component: () => import('../views/DashboardView.vue'),
meta: { requiresAuth: true },
}

Cursor에서 AI 지원 컴포넌트 생성​

프롬프트에서 SFC 생성​

Cursor의 AI 어시스턴트는 평범한 영어 설명에서 완전한 단일 파일 컴포넌트를 생성할 수 있습니다. 명령 팔레트(Ctrl+Shift+P)를 열고 Cursor: New Chat을 선택하거나, Ctrl+K로 인라인 편집을 사용합니다.

프롬프트 예:

Create a Vue 3 component using script setup and TypeScript for a paginated
data table. It should accept a columns prop (array of { key, label }),
a rows prop (array of records), and emit a page-change event with the new
page number. Include scoped styles and show empty-state messaging.

Cursor는 세 가지 블록 모두를 포함하는 스캐폴딩된 .vue 파일을 생성합니다. 출력을 검토하고 반복적으로 다듬습니다 — 필요에 따라 로딩 스켈레톤, 정렬 또는 고정 헤더 추가를 요청합니다.

팁

프롬프트에서 Vue 버전과 구문 스타일을 구체적으로 지정하세요. "Vue 3 with <script setup lang="ts"> and Composition API"를 지정하면 모델이 Options API 코드나 Vue 2 패턴을 출력하는 것을 방지할 수 있습니다.

기존 컴포넌트 리팩토링​

Cursor는 이전 Options API 컴포넌트를 Composition API 동등물로 리팩토링할 수 있습니다. 컴포넌트를 강조 표시하고 Ctrl+K를 누른 후 프롬프트를 입력합니다:

Convert this Options API component to script setup using the Composition API.
Keep all existing props, emits, and methods. Use composables for any logic
that could be reused elsewhere.

AI는 동작을 보존하면서 파일을 다시 작성합니다. 이후 항상 테스트 스위트를 실행하여 동등성을 확인하세요.

AI로 테스트 생성​

Vue 컴포넌트를 Cursor가 생성한 Vitest 테스트와 페어링합니다:

// Counter.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'

describe('Counter.vue', () => {
it('renders the title prop', () => {
const wrapper = mount(Counter, { props: { title: 'Clicks', count: 0 } })
expect(wrapper.text()).toContain('Clicks')
})

it('emits update with incremented value on click', async () => {
const wrapper = mount(Counter, { props: { title: 'Clicks', count: 5 } })
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('update')?.[0]).toEqual([6])
})
})

Cursor에 프롬프트: "Generate Vitest unit tests for Counter.vue covering the increment behavior and empty-state rendering."

Cursor에서 Vue 앱 디버깅 및 프로파일링​

Vue DevTools 통합​

Vue.js devtools 브라우저 확장 프로그램을 설치하거나 독립 실행형 Electron 앱을 사용하세요. Cursor 자체는 DevTools UI를 호스팅하지 않지만, Volar 확장 프로그램은 컴포넌트 진단을 인라인으로 표시합니다.

Pinia store 상태를 디버깅하려면 Vue DevTools의 "Pinia" 탭이 각 store, 현재 상태, actions 타임라인을 표시합니다 — 오래된 상태 버그를 추적하는 데 매우 유용합니다.

소스 맵과 Vite​

Vite는 기본적으로 개발용 소스 맵을 생성합니다. vite.config.ts에서 활성화 상태를 유지하세요:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
plugins: [vue()],
build: {
sourcemap: true,
},
})
경고

소스 맵을 프로덕션에 제공하면 원본 소스 코드가 노출될 수 있습니다. 프로덕션 빌드의 경우 sourcemap: 'hidden'을 설정하거나 호스팅된 소스 맵 서비스를 사용하고, .map 파일을 공개 저장소에 커밋하지 마세요.

성능 프로파일링​

Chrome 성능 탭을 Cursor와 함께 사용하세요. vite-plugin-vue-inspector(또는 unplugin-vue-source)를 사용하면 브라우저에서 요소를 클릭하여 Cursor의 소스 줄로 직접 이동할 수 있습니다 — 대규모 코드베이스 탐색에 있어 엄청난 생산성 향상을 제공합니다.

npm install -D vite-plugin-vue-devtools
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueDevTools from 'vite-plugin-vue-devtools'

export default defineConfig({
plugins: [vue(), VueDevTools()],
})

Cursor에서 Vue.js를 위한 모범 사례​

프로젝트 구조 규칙​

확장 가능한 레이아웃은 다음과 같습니다:

src/
├── assets/
├── components/
│ ├── common/
│ └── forms/
├── composables/
├── layouts/
├── router/
├── stores/
├── views/
├── App.vue
└── main.ts

린트 및 포맷팅​

package.json에 ESLint 및 Prettier 버전을 고정하여 전체 팀이 동일한 규칙을 공유하도록 합니다:

{
"devDependencies": {
"eslint": "^9.0.0",
"eslint-plugin-vue": "^9.0.0",
"prettier": "^3.0.0",
"@vue/eslint-config-typescript": "^13.0.0"
}
}

.eslintrc.cjs를 생성합니다:

module.exports = {
root: true,
env: { browser: true, node: true },
extends: [
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript',
],
rules: {
'vue/multi-word-component-names': 'off',
'vue/component-tags-order': ['error', { order: ['script', 'template', 'style'] }],
},
}
팁

settings.json에 다음을 추가하여 저장 시 Cursor가 린트 에러를 자동 수정하도록 설정합니다:

{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}

Vue 워크플로우 키보드 단축키​

  • Ctrl+K — 인라인 AI 편집/리팩토링.
  • Ctrl+Shift+P → "Cursor: New Chat" — 컴포넌트 생성을 위한 AI 채팅 열기.
  • Alt+Click 컴포넌트 태그 — 정의로 이동 (Volar 구동).
  • Ctrl+. — Volar 진단을 위한 빠른 수정 메뉴.

일반적인 함정 및 피하는 방법​

구조 분해 후 반응성 상실​

Vue 3에서 가장 흔한 실수는 반응형 객체나 ref를 직접 구조 분해하는 것입니다:

// 잘못됨 — 반응성 상실
const { count } = useCounterStore()

// 올바름
const { count } = storeToRefs(useCounterStore())

Props 변경​

Props는 읽기 전용입니다. 자식이 값을 변경해야 하는 경우 대신 이벤트를 발생시키세요:

<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ (e: 'update:modelValue', v: string): void }>()

// v-model 패스스루를 위한 computed setter 사용
import { computed } from 'vue'
const proxy = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val),
})
</script>

v-for에서 :key 누락​

Vue가 노드를 효율적으로 추적할 수 있도록 항상 고유한 :key를 바인딩하세요:

<template>
<li v-for="item in items" :key="item.id">{{ item.label }}</li>
</template>
경고

배열 인덱스를 :key로 사용하면 목록이 재정렬되거나 필터링될 때 미묘한 버그를 일으킬 수 있습니다. 데이터베이스 ID와 같은 안정적인 고유 식별자를 선호하세요.

요약​

Cursor는 공식 Vue 도구 체인과 잘 어울리는 모던하고 AI 증강된 작업 공간을 Vue.js 개발자에게 제공합니다. 타입 안전한 작성을 위한 Volar, 깔끔한 상태 관리를 위한 Pinia, 네비게이션을 위한 Vue Router, 컴포넌트 생성 및 리팩토링을 위한 Cursor의 AI를 결합함으로써 더 빠르고 더 적은 버그로 Vue 3 애플리케이션을 출시할 수 있습니다. create-vue로 프로젝트를 스캐폴딩하고 ESLint 및 포맷팅 규칙을 고정한 다음, Cursor의 AI가 반복적인 스캐폴딩을 처리하는 동안 제품 로직과 사용자 경험에 집중하세요.