// frontend framework

프로그레시브
Vue.js 완전 정복

점진적으로 도입 가능한 프레임워크, Vue.js의 핵심 개념부터 실전 패턴까지 단계별로 학습합니다. Options API와 Composition API를 모두 다룹니다.

Vue 3.x Composition API TypeScript 지원 Vite 기반

Vue.js란 무엇인가?

Vue.js는 사용자 인터페이스를 구축하기 위한 프로그레시브(Progressive) JavaScript 프레임워크입니다. 기존 HTML에 조금씩 추가해 나갈 수도 있고, 대규모 SPA를 빌드하는 데도 활용할 수 있습니다.

Vue의 장점

  • 낮은 학습 곡선
  • 직관적인 단일 파일 컴포넌트(SFC)
  • 뛰어난 성능 (가상 DOM)
  • 유연한 점진적 도입
  • 활발한 생태계

주요 특징

  • 반응형 데이터 바인딩
  • 컴포넌트 기반 아키텍처
  • 공식 라우터 & 상태관리
  • TypeScript 완전 지원
  • SSR (Nuxt.js)
💡 Options API vs Composition API Vue 3는 두 가지 스타일을 모두 지원합니다. 이 문서는 현재 권장 방식인 <script setup> 구문(Composition API)을 중심으로 설명합니다.

설치 & 환경설정

Vite로 프로젝트 생성

Node.js 18 이상이 설치되어 있어야 합니다.

bash
# npm
npm create vue@latest my-app

# yarn
yarn create vue my-app

# pnpm
pnpm create vue my-app

생성 후 디렉토리로 이동하고 의존성을 설치합니다.

bash
cd my-app
npm install
npm run dev

CDN으로 빠르게 시작

html
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<div id="app">{{ message }}</div>

<script>
  const { createApp, ref } = Vue
  createApp({
    setup() {
      const message = ref('Hello Vue!')
      return { message }
    }
  }).mount('#app')
</script>

프로젝트 구조

text
my-app/
├── public/
├── src/
│   ├── assets/          # 정적 리소스
│   ├── components/      # 재사용 컴포넌트
│   ├── composables/     # 재사용 로직
│   ├── views/           # 페이지 컴포넌트
│   ├── router/          # Vue Router 설정
│   ├── stores/          # Pinia 스토어
│   ├── App.vue          # 루트 컴포넌트
│   └── main.js          # 진입점
├── index.html
└── vite.config.js

템플릿 문법

Vue 템플릿은 유효한 HTML에 특수 문법을 추가한 형태입니다.

텍스트 보간 (Mustache)

html
<span>메시지: {{ msg }}</span>
<span>{{ number + 1 }}</span>
<span>{{ ok ? 'YES' : 'NO' }}</span>

속성 바인딩 (v-bind)

html
<!-- 전체 문법 -->
<a v-bind:href="url">링크</a>

<!-- 단축 문법 -->
<a :href="url">링크</a>
<img :src="imageSrc" :alt="imageAlt">

<!-- 동적 클래스 바인딩 -->
<div :class="{ active: isActive, error: hasError }"></div>

<!-- 인라인 스타일 -->
<div :style="{ color: textColor, fontSize: size + 'px' }"></div>

이벤트 핸들링 (v-on)

html
<!-- 단축 문법 @ -->
<button @click="increment">+1</button>
<input @keyup.enter="submit">
<button @click.stop="doThis">전파 중단</button>

반응성 시스템

Vue 3의 반응성은 ref()reactive()를 중심으로 동작합니다.

ref() — 원시값 & 단일 값

vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
const name  = ref('Vue')

function increment() {
  count.value++   // .value로 접근
}
</script>

<template>
  <button @click="increment">카운트: {{ count }}</button>
</template>

reactive() — 객체 반응성

vue
<script setup>
import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: { name: 'Alice', age: 30 }
})

state.count++          // .value 불필요
state.user.name = 'Bob'
</script>
⚠️ reactive() 주의사항 reactive() 객체를 구조분해하면 반응성이 깨집니다. 구조분해가 필요하다면 toRefs()를 사용하거나 ref()를 사용하세요.

v-model — 양방향 바인딩

vue
<script setup>
const text = ref('')
const checked = ref(false)
const selected = ref('A')
</script>

<template>
  <input v-model="text" placeholder="입력...">
  <input type="checkbox" v-model="checked">
  <select v-model="selected">
    <option>A</option>
    <option>B</option>
  </select>
</template>

컴포넌트

Vue 애플리케이션은 독립적이고 재사용 가능한 단일 파일 컴포넌트(SFC)로 구성됩니다.

기본 SFC 구조

vue — MyButton.vue
<script setup>
import { ref } from 'vue'

// defineProps로 부모 → 자식 데이터 수신
const props = defineProps({
  label:    { type: String, default: '클릭' },
  disabled: { type: Boolean, default: false }
})

// defineEmits로 자식 → 부모 이벤트 전달
const emit = defineEmits(['click'])

const count = ref(0)

function handleClick() {
  count.value++
  emit('click', count.value)
}
</script>

<template>
  <button
    :disabled="disabled"
    @click="handleClick"
    class="btn"
  >{{ label }} ({{ count }})</button>
</template>

<style scoped>
.btn {
  padding: 8px 16px;
  background: #42b883;
  color: white;
  border-radius: 6px;
}
</style>

컴포넌트 사용

vue — App.vue
<script setup>
import MyButton from './components/MyButton.vue'

function onClicked(count) {
  console.log(`버튼 클릭 횟수: ${count}`)
}
</script>

<template>
  <MyButton
    label="저장"
    @click="onClicked"
  />
</template>

슬롯 (Slots)

vue
<!-- Card.vue -->
<template>
  <div class="card">
    <header><slot name="header" /></header>
    <main><slot /></main>    <!-- 기본 슬롯 -->
    <footer><slot name="footer" /></footer>
  </div>
</template>

<!-- 사용 -->
<Card>
  <template #header>제목</template>
  본문 내용입니다.
  <template #footer>푸터</template>
</Card>

디렉티브

디렉티브 역할 예시
v-if / v-else조건부 렌더링v-if="isVisible"
v-showdisplay 토글v-show="isOpen"
v-for리스트 렌더링v-for="item in list"
v-model양방향 바인딩v-model="value"
v-bind (:)속성 바인딩:href="url"
v-on (@)이벤트 핸들링@click="fn"
v-once1회 렌더링v-once
v-htmlHTML 삽입v-html="rawHtml"

v-for 상세

html
<!-- 배열 -->
<li v-for="(item, index) in items" :key="item.id">
  {{ index }}. {{ item.name }}
</li>

<!-- 객체 -->
<div v-for="(val, key) in obj" :key="key">
  {{ key }}: {{ val }}
</div>

computed & watch

computed — 파생 상태

vue
<script setup>
import { ref, computed } from 'vue'

const items = ref([
  { name: '사과', price: 1200, qty: 3 },
  { name: '바나나', price: 800, qty: 5 }
])

// 의존하는 ref가 바뀔 때만 재계산
const totalPrice = computed(() =>
  items.value.reduce((sum, i) => sum + i.price * i.qty, 0)
)
</script>

<template>
  <p>합계: {{ totalPrice.toLocaleString() }}원</p>
</template>

watch — 사이드 이펙트

vue
import { ref, watch, watchEffect } from 'vue'

const query = ref('')

// 특정 ref 감시
watch(query, async (newVal, oldVal) => {
  if (newVal.length > 2) {
    results.value = await fetchData(newVal)
  }
}, { immediate: true, deep: true })

// 의존성 자동 추적
watchEffect(() => {
  document.title = `검색: ${query.value}`
})

생명주기 훅

vue
<script setup>
import {
  onMounted, onUpdated, onUnmounted,
  onBeforeMount, onBeforeUpdate, onBeforeUnmount
} from 'vue'

onMounted(() => {
  // DOM 접근 가능. API 호출, 이벤트 리스너 등록
  console.log('마운트 완료')
})

onUnmounted(() => {
  // 정리 작업: 타이머, 이벤트 리스너 해제
  clearInterval(timer)
})
</script>
시점
onBeforeMountDOM 마운트 직전
onMountedDOM 마운트 완료 ✨
onBeforeUpdateDOM 업데이트 직전
onUpdatedDOM 업데이트 완료
onBeforeUnmount언마운트 직전
onUnmounted언마운트 완료 ✨

Composables — 로직 재사용

Composables는 상태 로직을 함수로 캡슐화하여 여러 컴포넌트에서 재사용하는 패턴입니다.

js — composables/useFetch.js
import { ref, watchEffect } from 'vue'

export function useFetch(url) {
  const data    = ref(null)
  const error   = ref(null)
  const loading = ref(false)

  watchEffect(async () => {
    loading.value = true
    error.value   = null
    try {
      const res  = await fetch(url.value ?? url)
      data.value  = await res.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  })

  return { data, error, loading }
}
vue — 사용
<script setup>
import { useFetch } from '@/composables/useFetch'

const { data, loading, error } = useFetch('https://api.example.com/posts')
</script>

<template>
  <div v-if="loading">로딩 중...</div>
  <div v-else-if="error">오류: {{ error.message }}</div>
  <ul v-else>
    <li v-for="post in data" :key="post.id">{{ post.title }}</li>
  </ul>
</template>

Vue Router

js — router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/',         component: () => import('@/views/Home.vue') },
    { path: '/about',    component: () => import('@/views/About.vue') },
    { path: '/user/:id', component: () => import('@/views/User.vue'),
      meta: { requiresAuth: true } }
  ]
})

// 네비게이션 가드
router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isLoggedIn())
    return { path: '/login' }
})

export default router
vue — 라우터 사용
<script setup>
import { useRouter, useRoute } from 'vue-router'

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

const userId = route.params.id   // URL 파라미터

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

<template>
  <RouterLink to="/"></RouterLink>
  <RouterView />
</template>

Pinia — 상태관리

Pinia는 Vue의 공식 상태 관리 라이브러리로, Vuex보다 훨씬 간결합니다.

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

export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0)

  // getters
  const double = computed(() => count.value * 2)

  // actions
  function increment() { count.value++ }
  function reset()     { count.value = 0 }

  return { count, double, increment, reset }
})
vue — 스토어 사용
<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
</script>

<template>
  <p>카운트: {{ counter.count }}</p>
  <p>2배: {{ counter.double }}</p>
  <button @click="counter.increment">증가</button>
  <button @click="counter.reset">초기화</button>
</template>
🎉 학습을 마쳤습니다! Vue.js 공식 문서(vuejs.org)에서 더 깊은 내용을 탐색해 보세요. Nuxt.js를 통한 SSR, Vitest를 활용한 테스트, Vite 플러그인 개발 등 방대한 생태계가 기다리고 있습니다.