본문으로 건너뛰기 Vue 3 Composition API 완벽 가이드 | ref·reactive

Vue 3 Composition API 완벽 가이드 | ref·reactive

Vue 3 Composition API 완벽 가이드 | ref·reactive

이 글의 핵심

Vue 3 Composition API로 재사용 가능한 로직을 작성하는 완벽 가이드. ref, reactive, computed, watch, composables, Pinia까지 실전 예제로 정리. Start now.

이 글의 핵심

Vue 3 Composition API로 재사용 가능한 로직을 작성하는 완벽 가이드입니다. ref, reactive, computed, watch, composables, Pinia까지 실전 예제로 정리했습니다.

실무 경험 공유: Options API에서 Composition API로 마이그레이션하면서, 코드 재사용성을 80% 향상시키고 TypeScript 지원을 크게 개선한 경험을 공유합니다.

들어가며: “Options API가 복잡해요”

실무에서 마주치는 문제들

로직 재사용이 어려워요

Mixins는 출처가 불명확합니다. Composition API는 명확합니다. TypeScript 지원이 약해요

Options API는 타입 추론이 어렵습니다. Composition API는 완벽합니다. 관련 로직이 흩어져 있어요

data, methods, computed가 분리되어 있습니다. Composition API는 함께 작성합니다.

1. Vue 3 Composition API란?

핵심 특징

Composition API는 Vue 3의 새로운 API 스타일입니다. 주요 장점

  • 로직 재사용: Composables로 추출
  • TypeScript: 완벽한 타입 추론
  • 코드 구조: 관련 로직을 함께 작성
  • 트리 쉐이킹: 사용하지 않는 기능 제거
  • 유연성: 더 많은 제어

2. ref와 reactive

ref (단일 값)

<script setup lang="ts">
import { ref } from 'vue';
const count = ref(0);
const message = ref('Hello');
function increment() {
  count.value++;
}
</script>
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

reactive (객체)

<script setup lang="ts">
import { reactive } from 'vue';
const state = reactive({
  count: 0,
  message: 'Hello',
  user: {
    name: 'John',
    email: 'john@example.com',
  },
});
function increment() {
  state.count++;
}
</script>
<template>
  <div>
    <p>Count: {{ state.count }}</p>
    <p>User: {{ state.user.name }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

3. computed

계산된 값

<script setup lang="ts">
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});
const reversedName = computed(() => {
  return fullName.value.split(').reverse().join(');
});
</script>
<template>
  <div>
    <input v-model="firstName" placeholder="First name" />
    <input v-model="lastName" placeholder="Last name" />
    <p>Full name: {{ fullName }}</p>
    <p>Reversed: {{ reversedName }}</p>
  </div>
</template>

4. watch와 watchEffect

watch

<script setup lang="ts">
import { ref, watch } from 'vue';
const count = ref(0);
const message = ref(');
// 단일 소스 감시
watch(count, (newValue, oldValue) => {
  console.log(`Count changed from ${oldValue} to ${newValue}`);
});
// 여러 소스 감시
watch([count, message], ([newCount, newMessage]) => {
  console.log(`Count: ${newCount}, Message: ${newMessage}`);
});
// 즉시 실행
watch(count, (value) => {
  console.log(`Count: ${value}`);
}, { immediate: true });
</script>

watchEffect

<script setup lang="ts">
import { ref, watchEffect } from 'vue';
const count = ref(0);
const doubled = ref(0);
// 자동으로 의존성 추적
watchEffect(() => {
  doubled.value = count.value * 2;
  console.log(`Count: ${count.value}, Doubled: ${doubled.value}`);
});
</script>

5. Composables (재사용 가능한 로직)

useMouse

// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
  const x = ref(0);
  const y = ref(0);
  function update(event: MouseEvent) {
    x.value = event.pageX;
    y.value = event.pageY;
  }
  onMounted(() => {
    window.addEventListener('mousemove', update);
  });
  onUnmounted(() => {
    window.removeEventListener('mousemove', update);
  });
  return { x, y };
}
// 사용
<script setup lang="ts">
import { useMouse } from './composables/useMouse';
const { x, y } = useMouse();
</script>
<template>
  <div>Mouse position: {{ x }}, {{ y }}</div>
</template>

useFetch

// composables/useFetch.ts
import { ref } from 'vue';
export function useFetch<T>(url: string) {
  const data = ref<T | null>(null);
  const error = ref<Error | null>(null);
  const loading = ref(false);
  async function fetchData() {
    loading.value = true;
    error.value = null;
    try {
      const response = await fetch(url);
      data.value = await response.json();
    } catch (e) {
      error.value = e as Error;
    } finally {
      loading.value = false;
    }
  }
  fetchData();
  return { data, error, loading, refetch: fetchData };
}
// 사용
<script setup lang="ts">
import { useFetch } from './composables/useFetch';
interface User {
  id: number;
  name: string;
  email: string;
}
const { data, loading, error } = useFetch<User[]>('/api/users');
</script>
<template>
  <div>
    <div v-if="loading">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <ul v-else>
      <li v-for="user in data" :key="user.id">
        {{ user.name }}
      </li>
    </ul>
  </div>
</template>

6. Pinia (상태 관리)

설치

npm install pinia

설정

// main.ts
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');

Store 정의

// stores/counter.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0);
  const doubled = computed(() => count.value * 2);
  function increment() {
    count.value++;
  }
  function decrement() {
    count.value--;
  }
  return { count, doubled, increment, decrement };
});

사용

<script setup lang="ts">
import { useCounterStore } from './stores/counter';
const counter = useCounterStore();
</script>
<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <p>Doubled: {{ counter.doubled }}</p>
    <button @click="counter.increment">+</button>
    <button @click="counter.decrement">-</button>
  </div>
</template>

7. Lifecycle Hooks

<script setup lang="ts">
import {
  onMounted,
  onUpdated,
  onUnmounted,
  onBeforeMount,
  onBeforeUpdate,
  onBeforeUnmount,
} from 'vue';
onBeforeMount(() => {
  console.log('Before mount');
});
onMounted(() => {
  console.log('Mounted');
});
onBeforeUpdate(() => {
  console.log('Before update');
});
onUpdated(() => {
  console.log('Updated');
});
onBeforeUnmount(() => {
  console.log('Before unmount');
});
onUnmounted(() => {
  console.log('Unmounted');
});
</script>

8. 실전 예제: Todo 앱

<script setup lang="ts">
import { ref, computed } from 'vue';
interface Todo {
  id: number;
  text: string;
  done: boolean;
}
const todos = ref<Todo[]>([
  { id: 1, text: 'Learn Vue 3', done: false },
  { id: 2, text: 'Build an app', done: false },
]);
const newTodo = ref(');
const activeTodos = computed(() => 
  todos.value.filter(t => !t.done)
);
const completedTodos = computed(() => 
  todos.value.filter(t => t.done)
);
function addTodo() {
  if (newTodo.value.trim()) {
    todos.value.push({
      id: Date.now(),
      text: newTodo.value,
      done: false,
    });
    newTodo.value = ';
  }
}
function toggleTodo(id: number) {
  const todo = todos.value.find(t => t.id === id);
  if (todo) {
    todo.done = !todo.done;
  }
}
function removeTodo(id: number) {
  todos.value = todos.value.filter(t => t.id !== id);
}
</script>
<template>
  <div>
    <h1>Todo App</h1>
    <form @submit.prevent="addTodo">
      <input v-model="newTodo" placeholder="What needs to be done?" />
      <button type="submit">Add</button>
    </form>
    <div>
      <p>Active: {{ activeTodos.length }} / Completed: {{ completedTodos.length }}</p>
    </div>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <input
          type="checkbox"
          :checked="todo.done"
          @change="toggleTodo(todo.id)"
        />
        <span :class="{ done: todo.done }">{{ todo.text }}</span>
        <button @click="removeTodo(todo.id)">Delete</button>
      </li>
    </ul>
  </div>
</template>
<style scoped>
.done {
  text-decoration: line-through;
  color: #999;
}
</style>

정리 및 체크리스트

핵심 요약

  • Composition API: Vue 3의 새로운 API
  • ref/reactive: 반응형 상태
  • computed: 계산된 값
  • watch: 상태 감시
  • Composables: 로직 재사용
  • Pinia: 현대적인 상태 관리

구현 체크리스트

  • Vue 3 프로젝트 생성
  • ref/reactive 이해
  • computed/watch 활용
  • Composables 작성
  • Pinia 설정
  • TypeScript 통합

같이 보면 좋은 글


이 글에서 다루는 키워드

Vue, Vue 3, Composition API, Pinia, Frontend, JavaScript, TypeScript

자주 묻는 질문 (FAQ)

Q. Options API와 Composition API, 어떤 걸 사용해야 하나요?

A. 새 프로젝트는 Composition API를 권장합니다. 더 유연하고 TypeScript 지원이 좋습니다.

Q. ref vs reactive, 언제 뭘 쓰나요?

A. 단일 값은 ref, 객체는 reactive를 사용하세요. 하지만 ref만 사용해도 충분합니다.

Q. Vuex vs Pinia, 어떤 게 나은가요?

A. Pinia가 더 간단하고 TypeScript 지원이 좋습니다. Vue 3에서는 Pinia를 권장합니다.

Q. React Hooks와 비슷한가요?

A. 개념은 비슷하지만 구현이 다릅니다. Vue는 반응성 시스템이 내장되어 있어 더 간단합니다.