minstudio

Vue 3 소개와 Composition API의 혁신

과거 Vue 2는 Options API라는 방식을 사용했습니다. 이는 배우기 쉽다는 장점이 있었지만, 앱이 커지면 연관된 코드가 data, methods, computed 등 여러 곳에 흩어져 유지보수가 힘든 단점이 있었습니다. Vue 3의 Composition API (<script setup>)는 기능 단위로 코드를 한데 모을 수 있는 혁명적인 패러다임입니다.

🎨 Options API vs Composition API 과거: Options API data() computed: methods: 🚨 기능이 파편화되어 스크롤 지옥 발생 현재: Composition API 🔴 검색 기능 (Feature A) ref() + computed() + function 🔵 정렬 기능 (Feature B) ref() + function ✅ 연관된 코드끼리 한곳에 모임!

<script setup>의 마법

  • 코드의 극적인 간소화: 귀찮은 export default { setup() { ... return {} } } 껍데기가 몽땅 사라졌습니다. 변수나 함수를 선언하기만 하면 템플릿에서 즉시 사용 가능합니다.
  • React Hooks와 유사한 구조: 상태 선언, 이벤트 처리 로직이 위에서 아래로 물 흐르듯 자연스럽게 작성되어 가독성이 폭발적으로 상승합니다.
  • 타입스크립트(TS) 완벽 호환: Vue 2의 최대 단점이었던 조악한 TS 지원이 해결되었고, 네이티브 수준의 완벽한 타입 추론을 제공합니다.
<!-- ==========================================
// 📂 App.vue (Vue 3 Composition API 예제)
// ========================================== -->

<script setup>
// 이 안에 선언된 모든 변수와 함수는 템플릿(<template>)에서 즉시 사용 가능합니다!
// 귀찮게 return {} 을 적어줄 필요가 1도 없습니다.
import { ref } from 'vue';

// [1. 상태(State) 선언]
// ref()를 사용하여 변수를 반응형으로 만듭니다. (값이 바뀌면 화면도 자동으로 바뀜)
const count = ref(0);

// [2. 함수(Method) 선언]
// 그냥 평범한 자바스크립트 함수를 만들면 됩니다.
const increment = () => {
  // script 안에서 ref 값을 변경할 때는 반드시 '.value'를 붙여야 합니다!
  count.value += 1;
};
</script>

<template>
  <div class="p-8 max-w-md mx-auto text-center border rounded-xl shadow-lg mt-10">
    <h1 class="text-3xl font-bold mb-6 text-gray-800">Vue 3 시작하기 🚀</h1>
    
    <!-- 템플릿(HTML) 영역에서는 .value 없이 변수 이름만 쓰면 됩니다! (Vue가 알아서 까줌) -->
    <p class="text-6xl font-black text-emerald-500 mb-8">{{ count }}</p>
    
    <!-- 버튼 클릭 이벤트는 @click (또는 v-on:click) 으로 연결합니다. -->
    <button 
      @click="increment" 
      class="px-8 py-3 bg-emerald-500 text-white rounded-full font-bold hover:bg-emerald-600 transition"
    >
      카운트 증가!
    </button>
  </div>
</template>
반응형 상태 관리 (ref vs reactive)

Vue 3에서 변수의 값이 바뀌었을 때 화면도 자동으로 바뀌게(반응형) 만들려면 refreactive라는 마법의 상자에 값을 담아야 합니다. 두 가지 모두 역할은 비슷하지만, 담을 수 있는 데이터 종류와 사용법이 약간 다릅니다.

📦 ref() vs reactive() 비교 ref() (추천! 👍) 문자열, 숫자, 배열, 객체 (모두 가능) .value 0 count.value = 1 단점: .value 를 꼭 붙여야 함 reactive() 객체(Object) 전용 (문자, 숫자 불가) Proxy Object { age: 25 } user.age = 26 단점: 구조 분해 할당 시 반응성 파괴
💡 초보자를 위한 실전 요약: 언제 뭘 써야 할지 헷갈리신다구요? 무조건 ref()만 쓰시면 됩니다! Vue 공식 문서에서도 초보자에게는 모든 데이터 타입에 일관성 있게 사용할 수 있는 ref의 사용을 강력히 권장하고 있습니다.
<!-- ==========================================
// 📂 StateManagement.vue (ref와 reactive 비교 예제)
// ========================================== -->

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

// ------------------------------------------------
// 1. ref()의 사용 (강력 추천!)
// 원시값(숫자, 문자)부터 객체, 배열까지 다 담을 수 있는 만능 상자.
// ------------------------------------------------
const message = ref('안녕하세요!'); // 문자열
const counter = ref(0);         // 숫자

const updateRef = () => {
  // 스크립트 내부에서 값을 읽거나 바꿀 때는 상자 뚜껑을 열어주는 '.value'를 반드시 적어야 합니다.
  message.value = '반갑습니다!';
  counter.value++;
};

// ------------------------------------------------
// 2. reactive()의 사용
// 오직 "객체(Object)나 배열"만 담을 수 있습니다. .value가 필요 없는 대신 제약이 많습니다.
// ------------------------------------------------
const user = reactive({
  name: '김민수',
  age: 20
});

const updateReactive = () => {
  // .value 없이 속성에 직접 접근합니다.
  user.age++;
  
  // 🚨 [주의] 아래처럼 통째로 갈아끼우면 반응성(화면 갱신)이 영원히 박살납니다!
  // user = { name: '이영희', age: 30 };
};
</script>

<template>
  <div class="p-8 max-w-lg mx-auto space-y-8 bg-white text-gray-800 rounded-xl shadow-lg mt-10">
    
    <!-- 템플릿(HTML)에서는 .value를 적을 필요가 전혀 없습니다! -->
    <section class="border-b pb-6">
      <h2 class="text-xl font-bold text-blue-600 mb-2">ref() 결과</h2>
      <p class="text-lg">메시지: {{ message }}</p>
      <p class="text-lg">카운터: {{ counter }}</p>
      <button @click="updateRef" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded">
        ref 업데이트
      </button>
    </section>

    <section>
      <h2 class="text-xl font-bold text-pink-600 mb-2">reactive() 결과</h2>
      <p class="text-lg">유저 이름: {{ user.name }}</p>
      <p class="text-lg">유저 나이: {{ user.age }}</p>
      <button @click="updateReactive" class="mt-4 px-4 py-2 bg-pink-500 text-white rounded">
        reactive 업데이트
      </button>
    </section>

  </div>
</template>
템플릿 문법과 필수 디렉티브

HTML 태그 안에 마법의 주문(디렉티브)을 걸어 화면을 동적으로 조작할 수 있습니다. Vue의 디렉티브(Directive)는 모두 v- 로 시작하며, 초보자가 반드시 알아야 할 핵심 디렉티브 4가지를 그림으로 정리했습니다.

✨ 필수 디렉티브 핵심 컨닝 페이퍼 v-bind (속성 연결) HTML 속성(src, class 등)에 변수를 꽂아 넣습니다. 기존: <img src="image.jpg"> 축약형: :src="imageUrl" v-on (이벤트 연결) 클릭, 키 입력 등 사용자 이벤트를 감지합니다. 기존: <button onclick="..."> 축약형: @click="saveData" v-if / v-else (조건부 렌더링) 조건이 참(True)일 때만 화면에 그립니다. <div v-if="isLoggedIn">환영합니다!</div> v-for (반복문) 배열 데이터를 바탕으로 HTML 태그를 반복 생성합니다. <li v-for="item in array" :key="item.id">

그리고 궁극의 마법, v-model (양방향 바인딩)

회원가입 폼을 만들 때 사용자가 <input>에 글씨를 입력하는 즉시, 스크립트 내부의 변수(ref) 값도 자동으로 똑같이 바뀌게 하고 싶다면 v-model 하나만 쓰면 됩니다. React처럼 value={text} onChange={...} 처럼 복잡하게 코드를 짤 필요가 전혀 없습니다.

<!-- ==========================================
// 📂 TodoList.vue (필수 디렉티브 4종 세트 실전 예제)
// ========================================== -->

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

// 입력창과 연결할 변수 (v-model 전용)
const newTodo = ref('');

// 할 일 목록 배열 (v-for 전용)
const todos = ref([
  { id: 1, text: 'Vue 3 문법 복습하기', done: false },
  { id: 2, text: '운동 다녀오기', done: true }
]);

// 할 일 추가 함수 (v-on 전용)
const addTodo = () => {
  if (newTodo.value.trim() === '') return;
  
  todos.value.push({
    id: Date.now(),
    text: newTodo.value,
    done: false
  });
  
  newTodo.value = ''; // 입력창 비우기
};
</script>

<template>
  <div class="p-8 max-w-md mx-auto bg-white rounded-xl shadow mt-10">
    <h2 class="text-2xl font-bold mb-4">할 일 목록 (Todo)</h2>
    
    <div class="flex gap-2 mb-6">
      <!-- ✨ [v-model]: 입력창과 변수를 하나로 묶어버림 (양방향 연결) -->
      <!-- ✨ [v-on]: @keyup.enter 로 엔터키를 쳤을 때 함수 실행 -->
      <input 
        v-model="newTodo"
        @keyup.enter="addTodo"
        placeholder="할 일을 입력하세요..."
        class="border p-2 rounded flex-1"
      />
      
      <!-- ✨ [v-bind]: :disabled 로 조건에 따라 버튼 비활성화 속성 제어 -->
      <button 
        @click="addTodo" 
        :disabled="newTodo.length === 0"
        class="bg-blue-500 text-white px-4 rounded disabled:bg-gray-300"
      >
        추가
      </button>
    </div>

    <!-- ✨ [v-if]: 목록이 텅 비어있을 때만 이 <div>를 보여줌 -->
    <div v-if="todos.length === 0" class="text-gray-400 text-center py-4">
      등록된 할 일이 없습니다!
    </div>

    <!-- ✨ [v-else]: 목록이 비어있지 않으면 <ul> 태그를 보여줌 -->
    <ul v-else class="space-y-2">
      <!-- ✨ [v-for]: todos 배열을 뱅글뱅글 돌면서 <li> 태그를 마구 생성함 -->
      <!-- [주의] v-for를 쓸 때는 요소 구분을 위해 :key 를 반드시 넣어주세요! -->
      <li 
        v-for="item in todos" 
        :key="item.id"
        class="flex gap-2 items-center p-2 border rounded"
      >
        <input type="checkbox" v-model="item.done" />
        
        <!-- ✨ [v-bind]: 체크 여부에 따라 취소선 클래스를 동적으로 붙임 -->
        <span :class="{ 'line-through text-gray-400': item.done }">
          {{ item.text }}
        </span>
      </li>
    </ul>
  </div>
</template>
계산된 속성과 감시자 (computed & watch)

어떤 변수의 값이 변할 때, 그에 따라 자동으로 변하는 또 다른 파생된 값을 만들거나, 값이 변할 때마다 특정 동작(API 호출 등)을 실행하게 만들고 싶을 때가 있습니다. 이때 사용하는 핵심 도구가 바로 computedwatch입니다.

🧠 computed vs watch 완벽 가이드 computed (계산된 속성) 원본 데이터 (ref) 가공된 새로운 데이터 ✅ 값을 반환(return) 해야 함 ✅ 결과를 캐싱(저장)하여 성능 우수 watch (감시자) 감시할 데이터 (ref) 값이 변하면? 특정 동작 (Side Effect) ✅ 값을 반환할 필요 없음 ✅ API 호출, DOM 조작에 적합
<!-- ==========================================
// 📂 SearchFilter.vue (computed와 watch 실전 예제)
// ========================================== -->

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

// [1. 상태 선언]
const searchQuery = ref('');
const items = ref(['사과', '바나나', '포도', '딸기', '수박']);
const apiLogs = ref([]);

// ------------------------------------------------
// [💡 computed 사용법] - 원본 데이터를 가공해서 보여줄 때 (캐싱됨)
// ------------------------------------------------
const filteredItems = computed(() => {
  // 반드시 return 이 있어야 합니다!
  // searchQuery 나 items 의 값이 바뀔 때만 이 함수가 다시 실행됩니다.
  return items.value.filter(item => item.includes(searchQuery.value));
});

// ------------------------------------------------
// [💡 watch 사용법] - 데이터가 변할 때 특정 행동을 해야 할 때
// ------------------------------------------------
watch(searchQuery, (newValue, oldValue) => {
  // searchQuery 값이 변할 때마다 이 함수가 실행됩니다.
  // 이곳에서 백엔드 API를 호출하거나 무거운 작업을 수행하기 좋습니다.
  console.log(`검색어가 ${oldValue} 에서 ${newValue} 로 변경됨!`);
  
  apiLogs.value.push(`API 호출: ${newValue} 검색 결과 요청 중...`);
});
</script>

<template>
  <div class="p-8 max-w-md mx-auto bg-gray-50 rounded-xl shadow mt-10">
    <h2 class="text-2xl font-bold mb-4">과일 검색기</h2>
    
    <!-- 사용자가 검색어를 입력하면 searchQuery의 값이 실시간으로 바뀜 -->
    <input 
      v-model="searchQuery" 
      placeholder="과일 이름을 입력하세요"
      class="w-full border border-blue-300 p-3 rounded-lg mb-6 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
    />
    
    <div class="bg-white p-4 rounded-lg border border-gray-200 mb-6">
      <h3 class="font-bold text-gray-700 mb-2">검색 결과 (computed)</h3>
      <!-- computed로 가공된 filteredItems 배열을 반복하여 보여줌 -->
      <ul class="space-y-1">
        <li v-for="fruit in filteredItems" :key="fruit" class="text-blue-600 font-semibold">
          🍎 {{ fruit }}
        </li>
      </ul>
      <p v-if="filteredItems.length === 0" class="text-gray-400">결과가 없습니다.</p>
    </div>

    <div class="bg-black p-4 rounded-lg">
      <h3 class="font-bold text-green-400 mb-2">시스템 로그 (watch)</h3>
      <ul class="text-xs text-gray-300 font-mono space-y-1 h-20 overflow-y-auto">
        <li v-for="(log, idx) in apiLogs" :key="idx">> {{ log }}</li>
      </ul>
    </div>
  </div>
</template>
컴포넌트 통신 기초 (Props & Emit)

Vue 애플리케이션은 작은 컴포넌트(부품)들의 조립으로 이루어집니다. 부모 컴포넌트가 자식에게 데이터를 물려줄 때는 Props(프롭스)를 사용하고, 자식이 부모에게 어떤 사건(이벤트)이 일어났다고 알릴 때는 Emit(에밋)을 사용합니다. 이를 단방향 데이터 흐름이라고 합니다.

📡 부모와 자식 컴포넌트 간의 통신 규칙 부모 컴포넌트 (Parent.vue) const message = ref("안녕!") const showAlert = (msg) => alert(msg) Props (데이터 전달) Emit (이벤트 알림) 자식 컴포넌트 (Child.vue) defineProps(['text']) defineEmits(['notify']) 🚨 절대 규칙: 자식은 부모가 준 Props 데이터를 직접 수정(변경)할 수 없습니다!
<!-- ==========================================
// 📂 1. 부모 컴포넌트 (Parent.vue)
// ========================================== -->
<script setup>
import { ref } from 'vue';
// 자식 컴포넌트를 import 합니다.
import ChildComponent from './ChildComponent.vue';

const parentMsg = ref('부모가 용돈 5만원을 줌');

// 자식이 이벤트를 던졌을 때(Emit) 실행할 함수
const handleChildEvent = (replyMsg) => {
  alert(`자식의 응답: ${replyMsg}`);
};
</script>

<template>
  <div class="p-8 border-4 border-blue-300 rounded-xl m-4">
    <h2 class="text-2xl font-bold mb-4">👨‍👩‍👧 부모 컴포넌트</h2>
    
    <!-- 
      :message="parentMsg" => Props 로 데이터 내려주기
      @reply="handleChildEvent" => Emit 으로 올라오는 이벤트 받기 
    -->
    <ChildComponent 
      :message="parentMsg" 
      @reply="handleChildEvent" 
    />
  </div>
</template>

<!-- ==========================================
// 📂 2. 자식 컴포넌트 (ChildComponent.vue)
// ========================================== -->
<script setup>
// [1. Props 받기]
// 부모가 전달한 속성 이름('message')을 배열에 명시합니다.
const props = defineProps(['message']);

// [2. Emit 준비하기]
// 부모에게 쏠 이벤트 이름('reply')을 배열에 명시합니다.
const emit = defineEmits(['reply']);

const sendToParent = () => {
  // 부모에게 'reply'라는 이벤트를 쏘고, 뒤에 전달할 데이터도 같이 보냅니다.
  emit('reply', '감사합니다! 다 썼어요!');
  
  // 🚨 삐빅! Props로 받은 값은 절대 내 맘대로 수정할 수 없습니다 (읽기 전용).
  // props.message = '내가 다 씀'; // 에러 발생!
};
</script>

<template>
  <div class="p-6 border-4 border-green-300 rounded-xl bg-green-50">
    <h3 class="text-xl font-bold mb-2">👶 자식 컴포넌트</h3>
    
    <!-- 부모가 준 Props를 그대로 화면에 표시 -->
    <p class="text-lg text-gray-700 mb-4">부모님이 주신 것: {{ message }}</p>
    
    <button 
      @click="sendToParent"
      class="bg-green-500 text-white px-4 py-2 rounded font-bold shadow hover:bg-green-600"
    >
      부모님께 응답 보내기
    </button>
  </div>
</template>
컴포넌트 심화: 슬롯(Slots)과 Provide/Inject

Props와 Emit만으로 모든 것을 해결하려다 보면 문제가 생깁니다. 부모가 자식의 내부 디자인(HTML)을 통째로 갈아 끼우고 싶거나, 할아버지 컴포넌트가 저 아래 까마득한 손자 컴포넌트에게 데이터를 주고 싶을 때는 어떻게 할까요? 이때 SlotsProvide/Inject가 등장합니다.

🧩 Slots & Provide/Inject 구조 1. 슬롯 (Slots) : 레이아웃 뚫어놓기 Modal.vue (자식) <slot name="header"> <slot name="footer"> 구멍(Slot)을 뚫어두면 부모가 HTML을 채워 넣음 2. Provide / Inject : 직통 데이터 App.vue (할아버지) Provide Layout.vue (아버지) Button.vue (손자) Inject Props 릴레이(Drilling) 없이 데이터를 직통 배송
<!-- ==========================================
// 📂 App.vue (Provide와 Slots 사용 예제)
// ========================================== -->
<script setup>
import { provide, ref } from 'vue';
import Modal from './Modal.vue';
import GrandChild from './GrandChild.vue';

// [1. Provide] 할아버지가 데이터를 제공합니다.
// 이 데이터는 자식, 손자, 증손자 등 하위 컴포넌트 어디서든 바로 꺼내 쓸 수 있습니다.
const themeColor = ref('dark');
provide('theme', themeColor); 
</script>

<template>
  <div class="p-8 bg-gray-50 max-w-lg mx-auto">
    <!-- 손자 컴포넌트를 그냥 불러옵니다. Props를 안 넘겨줘도 됩니다! -->
    <GrandChild />

    <!-- [2. Slots] 자식 컴포넌트의 구멍(slot)에 HTML 끼워넣기 -->
    <Modal>
      <!-- 자식 컴포넌트의 <slot name="header"> 위치에 들어갈 HTML -->
      <template #header>
        <h1 class="text-2xl text-red-600 font-bold">경고: 결제 실패</h1>
      </template>

      <!-- 자식 컴포넌트의 기본 <slot> 위치에 들어갈 HTML -->
      <p class="text-gray-700 my-4">잔액이 부족하여 결제가 취소되었습니다.</p>

      <!-- 자식 컴포넌트의 <slot name="footer"> 위치에 들어갈 HTML -->
      <template #footer>
        <button class="bg-gray-800 text-white px-4 py-2 rounded">닫기</button>
      </template>
    </Modal>
  </div>
</template>

<!-- ==========================================
// 📂 GrandChild.vue (Inject 예제)
// ========================================== -->
<script setup>
import { inject } from 'vue';

// 할아버지가 Provide로 뿌린 'theme' 이라는 데이터를 직통으로 꺼내옵니다.
// Props로 여러 번 전달받을 필요가 없습니다! (Prop Drilling 해결)
const theme = inject('theme');
</script>

<template>
  <div :class="theme === 'dark' ? 'bg-black text-white' : 'bg-white text-black'">
    현재 테마는 {{ theme }} 입니다!
  </div>
</template>
Vue Router를 활용한 라우팅 (SPA 구축)

여러 페이지를 가진 웹사이트처럼 보이지만, 실제로는 단 하나의 HTML 파일에서 자바스크립트로 화면만 갈아 끼우는 방식을 SPA(Single Page Application)라고 합니다. Vue에서 이를 완벽하게 구현해 주는 공식 도구가 바로 Vue Router입니다.

🛣️ Vue Router 작동 원리 (SPA) 브라우저 URL 주소 / (Home) /about (소개) /user/:id (동적) 매핑 Vue 컴포넌트 렌더링 <router-view /> URL에 맞는 컴포넌트가 이 상자 안으로 쏙! 교체됩니다.
💡 핵심 포인트: 일반적인 웹사이트는 링크를 누르면 화면 전체가 하얗게 깜빡이며 새로고침 되지만, Vue Router를 사용하면 브라우저 주소창만 바뀌고 필요한 컴포넌트 조각만 부드럽게 교체되어 네이티브 앱 같은 쾌적한 속도를 냅니다.
<!-- ==========================================
// 📂 1. router/index.js (라우터 설정 파일)
// ⚠️ 주의: 실제 .js 파일에서는 <script> 태그를 작성하지 않습니다.
// ========================================== -->
<script>
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
import UserView from '../views/UserView.vue';

const router = createRouter({
  history: createWebHistory(), // 주소창에 '#' 이 안 생기게 하는 최신 방식
  routes: [
    {
      path: '/',
      name: 'home',
      component: HomeView
    },
    {
      // [동적 라우팅] /user/123 처럼 들어오는 ID 값을 캡처합니다.
      path: '/user/:id',
      name: 'user',
      component: UserView
    }
  ]
});

export default router;
</script>

<!-- ==========================================
// 📂 2. App.vue (라우터 적용 화면)
// ========================================== -->
<template>
  <div class="max-w-2xl mx-auto mt-10">
    <nav class="flex gap-4 mb-8 border-b pb-4">
      <!-- <a> 태그 대신 무조건 <router-link>를 쓰세요! (새로고침 방지) -->
      <router-link to="/" class="text-blue-600 font-bold hover:underline">홈으로</router-link>
      <router-link to="/user/777" class="text-blue-600 font-bold hover:underline">내 정보(777)</router-link>
    </nav>

    <!-- 매칭된 컴포넌트가 이 구멍 안에 렌더링됩니다. -->
    <main class="p-6 bg-white shadow rounded-lg">
      <router-view /> 
    </main>
  </div>
</template>

<!-- ==========================================
// 📂 3. UserView.vue (동적 라우팅 컴포넌트)
// ========================================== -->
<script setup>
import { useRoute } from 'vue-router';

// 현재 URL 정보를 담고 있는 route 객체를 꺼내옵니다.
const route = useRoute();
</script>

<template>
  <div>
    <h1 class="text-2xl font-bold">회원 정보 상세 페이지</h1>
    <!-- URL에서 '/user/:id' 위치에 입력된 값을 꺼내서 씁니다. -->
    <p class="mt-4 text-lg">조회하신 회원님의 번호는 <strong class="text-red-500">{{ route.params.id }}</strong>번 입니다.</p>
  </div>
</template>
Pinia를 활용한 전역 상태 관리

컴포넌트 개수가 10개, 20개로 늘어나고 깊이가 깊어지면 Props와 Emit만으로 데이터를 주고받기엔 한계가 옵니다. 이때, 모든 컴포넌트가 언제든 꺼내 쓸 수 있는 공용 데이터 창고(전역 상태 관리)가 필요한데, Vue 3의 공식 표준 창고가 바로 Pinia(피니아)입니다. (과거 Vuex를 대체합니다.)

🍍 Pinia 공용 데이터 창고 구조 Pinia Store (창고) State (변수: ref) Getters (계산: computed) Actions (함수: function) A 화면 (장바구니) B 화면 (결제창) 어디서든 자유롭게!
<!-- ==========================================
// 📂 1. stores/counter.js (Pinia Store 정의)
// ⚠️ 주의: 실제 .js 파일에서는 <script> 태그를 작성하지 않습니다.
// ========================================== -->
<script>
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

// [Setup Store 방식] - Composition API 와 똑같은 문법으로 Store를 만듭니다!
export const useCounterStore = defineStore('counter', () => {
  // 1. State (데이터) => ref() 로 만듭니다.
  const count = ref(0);

  // 2. Getters (가공된 데이터) => computed() 로 만듭니다.
  const doubleCount = computed(() => count.value * 2);

  // 3. Actions (데이터 수정 함수) => 일반 function() 으로 만듭니다.
  function increment() {
    count.value++;
  }

  // 밖에서 쓸 것들을 몽땅 묶어서 return 해줍니다.
  return { count, doubleCount, increment };
});
</script>

<!-- ==========================================
// 📂 2. ComponentA.vue (Pinia 사용처)
// ========================================== -->
<script setup>
import { useCounterStore } from '@/stores/counter';

// Store 객체를 가져옵니다.
const store = useCounterStore();
</script>

<template>
  <div class="p-6 bg-purple-50 rounded-lg border-2 border-purple-200 text-center">
    <h2 class="text-xl font-bold text-purple-700">전역 카운터 앱</h2>
    
    <!-- store 객체 안의 상태와 함수를 바로 갖다 씁니다. -->
    <p class="text-3xl my-4">현재 카운트: {{ store.count }}</p>
    <p class="text-gray-500 mb-4">2배 곱한 값: {{ store.doubleCount }}</p>
    
    <button @click="store.increment" class="bg-purple-600 text-white px-6 py-2 rounded-full font-bold shadow hover:bg-purple-700">
      +1 증가시키기
    </button>
  </div>
</template>
컴포저블(Composables) 함수로 로직 재사용하기

여러 컴포넌트에서 완전히 똑같은 기능(예: 마우스 커서 위치 추적, 다크 모드 토글, API 데이터 패칭)을 작성하고 있다면 어떨까요? Vue 3에서는 이 중복된 기능(상태+로직)을 싹둑 잘라내어 별도의 함수로 빼낼 수 있습니다. 이를 컴포저블(Composables)이라고 부르며, 관례적으로 use...() 라는 이름으로 시작합니다.

✂️ 컴포저블(Composables) 로직 재사용 useMouse.js const x = ref(0) const y = ref(0) window.addEventListener return { x, y } Map.vue const {x, y} = useMouse() Game.vue const {x, y} = useMouse() 각 컴포넌트마다 완벽히 독립적인 상태(x,y)를 가짐!
<!-- ==========================================
// 📂 1. composables/useMouse.js (마우스 좌표 추적 훅)
// ⚠️ 주의: 실제 .js 파일에서는 <script> 태그를 작성하지 않습니다.
// ========================================== -->
<script>
import { ref, onMounted, onUnmounted } from 'vue';

export function useMouse() {
  // [1. 상태]
  const x = ref(0);
  const y = ref(0);

  // [2. 이벤트 핸들러 로직]
  function update(event) {
    x.value = event.pageX;
    y.value = event.pageY;
  }

  // [3. 생명주기를 활용한 자동 설정]
  // 컴포넌트가 화면에 뜰 때 마우스 추적 시작
  onMounted(() => window.addEventListener('mousemove', update));
  // 컴포넌트가 사라질 때 이벤트 정리 (메모리 누수 방지)
  onUnmounted(() => window.removeEventListener('mousemove', update));

  // [4. 결과 반환]
  return { x, y };
}
</script>

<!-- ==========================================
// 📂 2. App.vue (컴포저블 가져다 쓰기)
// ========================================== -->
<script setup>
// 공통 로직을 한 줄로 불러옵니다.
import { useMouse } from './composables/useMouse';

// 이 컴포넌트 전용으로 독립된 마우스 x, y 값을 얻어냅니다.
const { x, y } = useMouse();
</script>

<template>
  <div class="h-screen w-full flex items-center justify-center bg-teal-50">
    <div class="text-center">
      <h1 class="text-3xl font-bold text-teal-800 mb-4">현재 마우스 위치</h1>
      <p class="text-4xl font-mono text-teal-600 bg-white p-4 rounded-xl shadow-lg border border-teal-200">
        X: {{ x }} / Y: {{ y }}
      </p>
    </div>
  </div>
</template>
내장 컴포넌트 마스터하기: 생명주기와 Teleport, Suspense

Vue 컴포넌트는 태어나고(Mount) 사라지는(Unmount) 생명주기(Lifecycle)를 가집니다. 이를 조작하여 컴포넌트가 뜰 때 백엔드 API를 호출할 수 있습니다. 추가로, Vue 3에서 제공하는 마법 같은 내장 컴포넌트인 <Teleport>를 사용하면, 복잡하게 중첩된 HTML 구조 속에서도 모달창이나 팝업을 화면 최상단(body)으로 순간이동 시켜 Z-Index 버그를 영구적으로 없앨 수 있습니다.

⏳ Lifecycle & 🚀 Teleport (순간이동) 주요 Lifecycle Hooks onMounted() 컴포넌트가 화면에 보일 때 발생. 이때 백엔드 API(fetch) 호출! onUpdated() 상태(ref)가 변해서 화면이 갱신될 때. onUnmounted() 컴포넌트가 화면에서 사라질 때. Teleport 순간이동 중첩된 뷰 (Z-index 버그위험) <Modal /> body 최상단 <Modal /> 안전한 렌더링!
<!-- ==========================================
// 📂 Dashboard.vue (onMounted 와 Teleport 조합 예제)
// ========================================== -->
<script setup>
import { ref, onMounted } from 'vue';

const isModalOpen = ref(false);
const userData = ref(null);
const isLoading = ref(true);

// [1. 생명주기 훅: onMounted]
// 화면이 브라우저에 부착(Mount)되는 즉시 실행됩니다.
onMounted(async () => {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users/1');
    userData.value = await response.json();
  } catch (e) {
    console.error("데이터 통신 에러!");
  } finally {
    isLoading.value = false;
  }
});
</script>

<template>
  <div class="p-8">
    <h1 class="text-3xl font-bold mb-6">마이페이지 대시보드</h1>
    
    <!-- 데이터 로딩 전 -->
    <div v-if="isLoading" class="text-gray-500 animate-pulse">
      데이터를 불러오는 중입니다...
    </div>
    
    <!-- 데이터 로딩 후 -->
    <div v-else class="space-y-4">
      <p class="text-xl">환영합니다, <span class="font-bold text-blue-600">{{ userData.name }}</span> 님!</p>
      
      <button @click="isModalOpen = true" class="bg-red-500 text-white px-6 py-2 rounded shadow">
        탈퇴 경고문 열기
      </button>
    </div>

    <!-- 
      [2. 텔레포트: <Teleport to="body">]
      이 코드가 아무리 깊은 <div> 숲 속에 있어도,
      브라우저가 그릴 때는 이 모달을 무조건 최상단 <body> 태그 바로 아래로 쏴버립니다.
      덕분에 CSS z-index가 꼬여서 모달이 잘리는 현상이 절대 발생하지 않습니다.
    -->
    <Teleport to="body">
      <!-- v-if 로 모달 렌더링 제어 -->
      <div v-if="isModalOpen" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
        <div class="bg-white p-8 rounded-xl shadow-2xl max-w-sm text-center transform transition-all">
          <h2 class="text-2xl font-bold text-red-600 mb-4">정말 탈퇴하시겠습니까?</h2>
          <p class="text-gray-600 mb-8">모든 정보가 삭제되며 복구할 수 없습니다.</p>
          
          <div class="flex gap-4 justify-center">
            <button @click="isModalOpen = false" class="px-4 py-2 bg-gray-200 rounded text-gray-800">취소</button>
            <button class="px-4 py-2 bg-red-600 rounded text-white font-bold">탈퇴</button>
          </div>
        </div>
      </div>
    </Teleport>
  </div>
</template>
Vue 3 소개와 Composition API의 혁신
반응형 상태 관리 (ref vs reactive)
템플릿 문법과 필수 디렉티브
계산된 속성과 감시자 (computed & watch)
컴포넌트 통신 기초 (Props & Emit)
컴포넌트 심화: 슬롯(Slots)과 Provide/Inject
Vue Router를 활용한 라우팅 (SPA 구축)
Pinia를 활용한 전역 상태 관리
컴포저블(Composables) 함수로 로직 재사용하기
내장 컴포넌트 마스터하기: 생명주기와 Teleport, Suspense

목차