소소한 지식 저장소

[STUDYING] 14. Front-framework: Vue.js_Day2_핵심 정리 본문

STUDYING

[STUDYING] 14. Front-framework: Vue.js_Day2_핵심 정리

ch010104 2026. 8. 3. 20:53

Composition API 개요

Vue 3에서 도입된 현대적인 JavaScript 코딩 방식으로, 컴포넌트의 로직(데이터, 함수, 계산된 속성 등)을 하나의 세트로 조합(Composition)하여 작성하는 방법을 제공함.

Options API와의 비교

  • Vue 2에서는 Options API 방식을 사용함
    • 데이터, Computed, methods, Lifecycle Hook 등을 각각의 옵션 속성(객체 키)으로 분리하여 구성함
    • 관련 로직이 파일 안에 흩어져 있어 큰 컴포넌트에서 가독성이 떨어짐
  • Composition API는 연관된 로직을 한 곳에 모아 작성하므로 가독성이 높음

작성 방법

Vue 3.2부터 <script setup> 안에 작성하면 됨

<script setup>
// Composition API 코드 작성 위치
</script>

Vue 3 내장함수 카테고리

Vue 프레임워크에서 핵심 기능을 수행하는 함수들을 카테고리별로 제공함. 빨간색으로 표기된 함수가 특히 핵심적으로 자주 사용됨.

카테고리 주요 함수
애플리케이션 (Application) createApp, createSSRApp, app.*(), app.config.*
반응형 상태 관리 (Reactive State) ref, reactive, readonly, shallowRef, shallowReactive, shallowReadonly, toRef, toRefs, customRef, unref, toRaw, markRaw, isRef, isReactive, isReadonly
계산 및 감시 (Computed & Watchers) computed, watch, watchEffect
라이프사이클 훅 (Lifecycle Hooks) setup, onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount, onActivated, onDeactivated, onErrorCaptured, onRenderTracked, onRenderTriggered
컴포넌트 구성/메타 (Component Composition) defineComponent, defineProps, defineEmits, useAttrs, defineExpose, useSlots, withDefaults, getCurrentInstance
렌더링 제어 (Rendering & DOM) h, resolveComponent, withDirectives, renderList, renderSlot, mergeProps, nextTick, useCssModule, useCssVars
의존성 주입 (Dependency Injection) provide, inject, hasInjectionContext

주요 포인트

  • 반응형 상태 관리에서 핵심은 ref와 reactive — 이 둘이 Composition API의 데이터 선언 중심임
  • 계산 및 감시 카테고리의 computed, watch, watchEffect 세 함수가 핵심 감시·파생값 처리를 담당함
  • 라이프사이클 훅은 Options API의 created, mounted 등에 대응되는 함수형 버전임

Reactive State - ref()

원시 타입(Primitive Datatype)과 참조 자료형(Array, Object 등) 모두를 반응형 상태로 만들 수 있는 함수임.

사용 규칙

  • <script setup> 상단에서 ref를 import해야 함
  • <script setup> 내부에서는 .value로 접근함
  • <template> 내부에서는 .value 없이 변수명만으로 사용 가능함 (자동 언래핑)

ref() 예시

<!-- Reactive State - ref() Example -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
const name = ref('홍길동')
const isActive = ref(true)
const items = ref(['사과', '배'])
const user = ref({ name: '이순신', age: 30 })

const increaseRef = () => {
  count.value++         // script 내부: .value 필수
}
const changeUserName = () => {
  user.value.name = '장보고'
}
</script>

<template>
  <div class="practice-section">
    <h2>반응형 상태 ref() 기초</h2>
    <p>Ref 카운트: <strong>{{ count }}</strong></p>   <!-- template: .value 불필요 -->
    <p>이름: <input v-model="name" />{{ name }}</p>
    <p>활성 상태: {{ isActive ? '활성' : '비활성' }}</p>
    <p>과일 목록: {{ items.join(', ') }}</p>
    <p>사용자 정보: 이름 - {{ user.name }}, 나이 - {{ user.age }}</p>
    <button @click="increaseRef">Ref 변수 증가</button>
    <button @click="isActive = !isActive">토글</button>
    <button @click="items.push('귤')">과일 추가</button>
    <button @click="changeUserName">사용자 이름 변경</button>
  </div>
</template>
  • 숫자·문자열·boolean·배열·객체 등 모든 자료형에 ref() 적용 가능함
  • 객체 내부 속성 접근 시 user.value.name처럼 .value 이후 일반 객체처럼 접근함

Reactive State - reactive()

참조 자료형(객체, 배열, Map, Set)만 반응형 상태로 만드는 함수임.

사용 규칙

  • <script setup> 상단에서 reactive를 import해야 함
  • <script setup>과 <template> 모두에서 .value 없이 일반 객체처럼 접근함

reactive()의 반응성 단절 문제

반응형 객체를 통째로 교체하거나 구조 분해 할당하면 반응성 연결이 끊어짐.

// reactive() 반응성 단절 주의
let state = reactive({ count: 0 })

// ❌ 통째로 새 객체를 갈아끼우면 반응성 연결이 끊어진다.
state = { count: 5 }

// ✅ 내부의 알맹이 속성만 조심스럽게 변경해야 한다.
state.count = 5
  • 이 약점 때문에 현업에서는 객체·배열을 다룰 때도 ref()로 통일해서 쓰는 추세가 강함

reactive() 예시

<!-- Reactive State - reactive() Example -->
<script setup>
import { reactive } from 'vue'

// 객체(Object) reactive
const userReactive = reactive({ name: '이순신', age: 30 })
const celebrateReactive = () => { userReactive.age++ }

// 배열(Array) reactive
const items = reactive(['사과', '바나나'])
const addItem = () => { items.push(`과일${items.length+1}`) }
const removeItem = (index) => { items.splice(index, 1) }
</script>

<template>
  <div class="practice-section">
    <h2>반응형 상태 reactive() 특징 및 주의점</h2>
    <h3>1) 객체(Object) reactive</h3>
    <p>이름: {{ userReactive.name }} / 나이: {{ userReactive.age }}세</p>
    <button @click="celebrateReactive">reactive 나이 한 살 추가</button>

    <h3>2) 배열(Array) reactive</h3>
    <ul>
      <li v-for="(item, index) in items" :key="index">
        {{ item }}
        <button @click="removeItem(index)">삭제</button>
      </li>
    </ul>
    <button @click="addItem">과일 항목 추가</button>
  </div>
</template>

배열 reactive 사용 시 참고사항

  • reactive로 선언된 배열은 items = ['a', 'b']처럼 전체를 새 배열로 재할당하면 반응형 연결이 끊어짐
  • 배열 데이터 변경 시 push / splice 등 변이 메서드를 쓰거나, 재할당이 필요하면 ref()를 사용하는 것을 권장함

Reactive State - 기타 함수들

반응형 상태 관리에 사용되는 함수들을 분류별로 정리한 요약표임. ref와 reactive 외에도 다양한 보조 함수가 존재함.

분류 함수 설명
기본 반응형 선언 ref 기본형/객체 모두 감싸서 반응형으로 변환
  reactive 객체, 배열 등을 반응형으로 변환
읽기 전용 선언 readonly 반응형 객체를 읽기 전용으로 설정
얕은 반응형/읽기전용 shallowRef 내부는 반응형 X, 객체의 참조만 감지
  shallowReactive 객체의 첫 레벨만 반응형
  shallowReadonly 첫 레벨만 읽기 전용
Ref ↔ Reactive 변환 toRef reactive 객체의 속성 1개를 ref로 추출
  toRefs reactive 객체의 모든 속성을 ref로 추출
Ref 해제 / 원본 추출 unref ref에서 값만 꺼냄 (ref.value 대신 사용)
  toRaw reactive에서 원본 객체 추출
  markRaw 객체를 반응형 처리 대상에서 제외
반응형 여부 확인 isRef ref인지 확인
  isReactive reactive인지 확인
  isReadonly readonly인지 확인
커스텀 Ref 생성 customRef 사용자 정의 반응형 ref 생성

분류별 포인트

  • shallow* 계열은 성능 최적화 목적으로, 깊은 중첩 객체까지 추적하지 않고 최상위 레벨만 감시함
  • toRef / toRefs는 reactive 객체를 구조 분해할 때 반응성을 유지하기 위해 사용함 — 일반 구조 분해 할당만 하면 반응성이 끊어지므로 이 함수들로 감싸서 추출함
  • toRaw / markRaw는 Vue의 반응형 시스템 밖에서 객체를 다뤄야 할 때 활용함 (예: 서드파티 라이브러리 전달)
  • unref(x)는 isRef(x) ? x.value : x와 동일한 동작으로, ref 여부와 관계없이 안전하게 값을 꺼낼 때 편리함

Computed & Watchers - computed()

의존하는 반응형 데이터(ref, reactive)가 변경될 때 자동으로 다시 계산되는 파생 값을 만드는 함수임.

핵심 특징

  • 계산된 값은 메모리에 **캐싱(Caching)**되어 성능이 좋음
    • 일반 함수는 화면이 바뀔 때마다(리렌더링) 무조건 재실행됨
    • computed는 의존하는 반응형 데이터가 바뀔 때만 재연산하고, 그 외에는 이전 결과를 재사용함
  • computed()로 생성한 Computed Property는 기본적으로 읽기 전용 — 다른 값으로 재할당 불가

Syntax

// Computed & Watchers - computed() 기본 문법
import { computed } from 'vue';

const 식별자 = computed(() => { return 값 })
  • 인자로 콜백 함수를 넘기면, Vue 내부에서 Computed Ref 객체를 반환함 (ref()의 결과물과 구조가 비슷함)
  • <script setup> 영역에서는 .value를 붙여서 값을 읽어야 함
  • <template> 내에서는 .value 없이 사용 가능함 (자동 언래핑)

computed() 예시 — 캐싱 동작 비교

<!-- Computed & Watchers - computed() Example -->
<script setup>
import { ref, computed } from 'vue'

const count = ref(0)
const dummy = ref(0) // computed와 무관한 변수

// 1. 일반 함수: 화면이 조금이라도 리렌더링되면 무조건 재실행
const getMethodResult = () => {
  console.log('❌ 일반 함수 실행됨!')
  return count.value * 2
}

// 2. Computed: count가 바뀔 때만 재연산 (dummy가 바뀌면 이전 값 재사용)
const doubleCount = computed(() => {
  console.log('✅ Computed 연산 실행됨!')
  return count.value * 2
})
</script>

<template>
  <div class="practice-section">
    <h2>computed() 캐싱 동작 비교</h2>
    <p>count: {{ count }} | dummy: {{ dummy }}</p>
    <button @click="count++">count 증가 (의존성 변경)</button>
    <button @click="dummy++">dummy 증가 (무관한 변경)</button>
    <!-- dummy 버튼을 누를 때 콘솔 출력 차이를 확인 -->
    <p>일반 함수 결과: {{ getMethodResult() }}</p>
    <p>Computed 결과: {{ doubleCount }}</p>
  </div>
</template>

Vue Component 재렌더링 동작 원리

  • count가 증가하면 Vue 반응형 시스템이 DOM 재렌더링 필요성을 감지함
  • 템플릿을 다시 그릴 때 {{ getMethodResult() }}처럼 괄호로 직접 호출한 일반 함수는 조건 불문 무조건 재실행됨
  • {{ doubleCount }}는 count가 바뀔 때만 재연산, dummy만 바뀌면 캐싱된 이전 결과를 그대로 씀

Console 창으로 차이 확인

  • count 증가 클릭 → 일반 함수 로그 + computed 로그 모두 출력됨
  • dummy 증가 클릭 → 일반 함수 로그만 출력됨 (computed는 재연산 없이 캐시 재사용)
    → () 괄호가 있는 getMethodResult() 는 함수이기 때문에, 화면이 재렌더링될 때마다 실행함.

(참고) <script setup> 내 함수 정의 방식 3종 비교

분류 1) 함수 선언문 (Function Declaration) 2) 함수 표현식 (Function Expression) 3) 화살표 함수 (Arrow Function)
Syntax function 로직() { ... } const 로직 = function() { ... } const 로직 = () => { ... }
Hoisting 작동 (선언 전 어디서든 호출 가능) 작동 안 함 (선언된 아랫줄부터 호출 가능) 작동 안 함 (선언된 아랫줄부터 호출 가능)
실무 가끔 사용 (전통적 방식) 거의 안 씀 압도적 권장 (실무 표준)
// <script setup> 함수 정의 방식 3종 예시
import { ref } from 'vue'
const message = ref('버튼을 누르세요')

// 1) 함수 선언문 (호이스팅 가능 - 하단에 있어도 위에서 호출 가능)
function 함수명() {
  message.value = '1번 함수 선언문 방식 발동!'
}

// 2) 함수 표현식 (호이스팅 불가 - 선언 후 호출 가능)
const 변수명 = function() {
  message.value = '2번 함수 표현식 방식 발동!'
}

// 3) 화살표 함수 (실무 표준 - 호이스팅 불가, 가장 컴팩트함)
const 변수명 = () => {
  message.value = '3번 화살표 함수 방식 발동!'
}

Computed & Watchers - watch()

반응형으로 선언된 데이터의 값이 변경되었을 때, 후속 로직(비동기 통신, 데이터 저장 등)을 수행하도록 콜백 함수를 지정하는 함수임.

Syntax

// Computed & Watchers - watch() 기본 문법
import { watch } from 'vue';

watch(반응형데이터, (newVal, oldVal) => { 실행할 후속 로직 })
  • 첫 번째 인자로 감시할 데이터를 받고, 두 번째 인자로 변경 시 실행할 콜백 함수를 지정함
  • 콜백 함수의 인자로 변경된 새 값(newVal)과 변경되기 전의 값(oldVal)이 자동으로 전달됨

watch() 예시

<!-- Computed & Watchers - watch() Example -->
<script setup>
import { ref, watch } from 'vue'

const currentCity = ref('서울')
const logMessage = ref('아직 감시 시스템이 작동하지 않았습니다.')

// currentCity 변수를 유심히 감시하는 watch 시스템 가동
watch(currentCity, (newValue, oldValue) => {
// 값이 바뀌는 순간, 바뀐 알맹이(값) 두 개가 자동으로 주입됨
  logMessage.value = `👀 감시자 발동! [${oldValue}]에서 [${newValue}]로 변경됨.`
// 실무 활용처 시뮬레이션
  console.log(`🌐 [서버 요청 완료] 기상청 서버에서${newValue}의 날씨 API를 다시 조회합니다...`)
})
</script>

<template>
  <div class="practice-section">
    <h2>감시자 watch()의 원리와 실무 활용</h2>
    <h3>🗺 지역 선택 제어판</h3>
    <p>현재 선택된 도시: {{ currentCity }}</p>
    <button @click="currentCity = '서울'">서울 선택</button>
    <button @click="currentCity = '수원'">수원 선택</button>
    <button @click="currentCity = '부산'">부산 선택</button>
    <div class="monitor">
      <h3>👁 파수꾼(watch) 모니터링 시스템</h3>
      <p>{{ logMessage }}</p>
      <small style="color: gray">(버튼을 누른 후 브라우저 콘솔창 F12를 확인해 보세요)</small>
    </div>
  </div>
</template>

동작 원리 정리

  • currentCity가 바뀌는 순간 watch 콜백이 자동 실행되며 newValue(바뀐 값)와 oldValue(이전 값)가 주입됨
  • 실무에서는 이 콜백 안에서 API 재호출, 로컬스토리지 저장, 로그 기록 등 부수 효과(side effect) 처리에 활용함
  • computed가 "파생 값을 계산"하는 용도라면, watch는 "값 변화에 반응해 동작을 실행"하는 용도임

watch() - Multi-Source Watch

반응형으로 선언된 여러 데이터를 한꺼번에 감시할 때 쓰는 기법임.

Syntax

// Multi-Source Watch 기본 문법
import { watch } from 'vue';

watch([변수1, 변수2], ([새값1, 새값2], [옛값1, 옛값2]) => { 실행할 후속 로직 })
  • 첫 번째 인자에 감시할 대상들을 배열 [] 형태로 묶어 전달함
  • 콜백 함수의 인자 배열 순서는 첫 번째 인자 배열의 순서와 동일함 ([변수1, 변수2] → [새값1, 새값2], [옛값1, 옛값2])
  • 배열 내 어느 하나라도 변하면 감시자 콜백 함수가 즉시 발동함

Multi-Source Watch 예시

<!-- watch() Multi-Source Watch Example -->
<script setup>
import { ref, watch } from 'vue'

const city = ref('서울')
const dateType = ref('오늘')
const apiStatus = ref('대기 중...')

// 두 개의 ref 변수를 배열[] 형태로 묶어 동시에 감시
watch([city, dateType], ([newCity, newDate], [oldCity, oldDate]) => {
  apiStatus.value = `[변경 감지]${oldCity}(${oldDate}) →${newCity}(${newDate})`
// 실무 활용: 두 옵션 중 하나만 바뀌어도 통합 API 요청을 보냄
  console.log(`🌐 [통합 API 호출]${newCity}의${newDate} 날씨를 불러옵니다...`)
})
</script>

<template>
  <div class="practice-section">
    <h2>여러 개의 변수 동시 감시 (watch)</h2>
    <h3>날씨 조건 설정</h3>
    <label>도시: </label>
    <select v-model="city">
      <option value="서울">서울</option>
      <option value="수원">수원</option>
      <option value="부산">부산</option>
    </select>
    <label>날짜: </label>
    <label><input type="radio" value="오늘" v-model="dateType" /> 오늘</label>
    <label><input type="radio" value="내일" v-model="dateType" /> 내일</label>
    <label><input type="radio" value="주간예보" v-model="dateType" /> 주간예보</label>
    <div class="monitor">
      <h3>통합 모니터링 로그</h3>
      <p>현재 상태: {{ apiStatus }}</p>
    </div>
  </div>
</template>

watch() - Deep Watch

ref()로 선언된 객체나 배열 내부의 속성 변화를 감지할 때 쓰는 기법임.

기본 watch는 객체·배열의 주소값(참조값)만 추적하므로 내부 속성이 바뀌어도 감지하지 못함. 내부 값 변화까지 추적하려면 { deep: true } 옵션을 명시해야 함.

→ ref로 선언된 변수는 값이 변경될 때, .value의 주소값(참조값)이 바뀌는 것임

Syntax

// Deep Watch 기본 문법
import { watch } from 'vue';

watch(반응형데이터, (newValue) => { 실행할 후속 로직 }, { deep: true })

주의점

  • deep: true를 쓰면 newValue와 oldValue가 똑같이 최신 값으로 출력됨 — 주소값이 같기 때문에 이전 값 추적이 불가함
  • 이전 값까지 추적해야 하면 아래 "특정 속성만 감시하기" 방식을 사용해야 함
    → 특정 속성만 감시할 경우, 주소값이 아닌 글자(알맹이 값)만 따로 받아와서 비교하기 때문에 이전 값이 추적이 가능함

객체의 특정 속성만 감시하기

// 특정 속성만 감시 - getter 함수 방식 (★ oldValue 정상 수집 가능)
watch(() => 변수.value.속성, (새값, 옛값) => { ... })

이 방식을 쓰면 newValue와 oldValue가 정상적으로 구분되어 수집됨.

Deep Watch 예시

<!-- watch() Deep Watch Example -->
<script setup>
import { ref, watch } from 'vue'

const user = ref({
  name: '홍길동',
  age: 20,
})

const logDeep = ref('아직 반응 없음')
const logTarget = ref('아직 반응 없음')

// 실패하는 예시 (가장 많이 범하는 오류)
// watch(user, () => { console.log('이 로그는 영원히 안 찍힙니다.') })

// 해결책 1: deep 옵션을 켜서 객체 하위 속성 전체 감시
// ⚠ newVal과 oldVal이 똑같이 최신 값으로 나옴 (이전 값 추적 불가)
watch(user, (newVal) => {
    logDeep.value = `[deep 감지] 누군가 변경됨! 현재 이름:${newVal.name}, 나이:${newVal.age}`
  },
  { deep: true },
)

// 해결책 2: 화살표 함수로 특정 속성(age)만 콕 집어 감시 (★ 이전 값 추적 가능!)
watch(() => user.value.age, (newAge, oldAge) => {
    logTarget.value = `[타겟 감지] 나이가${oldAge}세 →${newAge}세로 변경됨!`
  },
)
</script>

<template>
  <div class="practice-section">
    <h2>ref 객체/배열 감시</h2>
    <h3>🧑 회원 데이터 조작 panel</h3>
    <p>이름: {{ user.name }} / 나이: {{ user.age }}세</p>
    <button @click="user.name = '이순신'">이름만 변경</button>
    <button @click="user.age++">나이만 변경 (age++)</button>

    <div class="monitor">
      <p>👁 1) deep: true 모니터 (전체 감시)</p>
      <p>{{ logDeep }}</p>
    </div>
    <div class="monitor target">
      <p>🎯 2) 화살표 함수 모니터 (나이만 타겟 감시)</p>
      <p>{{ logTarget }}</p>
    </div>
  </div>
</template>

두 방식 비교 요약

방식 감시 범위 oldValue 추적
watch(user, cb, { deep: true }) 객체 내부 전체 속성 불가 (항상 최신값)
watch(() => user.value.age, cb) 특정 속성 1개 가능

watch() - reactive 반응형 데이터 감시

reactive()로 선언된 객체나 배열은 ref()와 달리 별도의 { deep: true } 옵션 없이도 내부 속성 변화를 자동으로 감지함. 단, 이전 값 추적 불가 문제는 동일하게 발생함.

reactive() 전체 감시 vs 특정 속성 감시

방식 문법 oldValue 추적
변수명 그대로 감시 (자동 deep) watch(state, (newVal, oldVal) => {...}) 불가 (항상 최신값으로 동일하게 찍힘)
화살표 함수로 특정 속성만 감시 watch(() => state.속성, (new, old) => {...}) 가능
  • reactive 객체를 통째로 감시하면 deep이 자동 적용되어 내부 속성 변화를 감지하지만, newVal과 oldVal이 똑같이 최신값으로 출력됨
  • 특정 속성만 감시하려면 () => state.속성 형태의 getter 함수를 넘기면 이전 값 추적이 가능함
    → state.속성 은 반응형 객체가 아닌 단순 숫자 원시값이기 때문에, 첫번째 파라미터로 () => state.속성 와 같이 작성해야함.

reactive 반응형 데이터 watch() 예시

<!-- watch() reactive 반응형 데이터 Example -->
<script setup>
import { reactive, ref, watch } from 'vue'

// reactive로 선언한 묶음 상품 데이터
const state = reactive({
  productName: '노트북',
  price: 1000,
})

const logAutoDeep = ref('대기 중...')
const logTarget = ref('대기 중...')

// 1) 변수명 그대로 감시 (자동 deep: true 작동)
// ⚠ newVal.price와 oldVal.price가 똑같이 최신값으로 나옴
watch(state, (newVal, oldVal) => {
  logAutoDeep.value = `[자동 deep] 가격 변동! 이전가격인척하는:${oldVal.price}원 → 현재가격:${newVal.price}원`
})

// 2) 화살표 함수로 특정 속성만 감시 (이전 값 추적 가능!)
// ✅ 특정 알맹이 값만 추출했으므로 진짜 과거 가격이 정상 보존됨
watch(
  () => state.price,
  (newPrice, oldPrice) => {
    logTarget.value = `[타겟 조준] 가격이 진짜 올랐음! 옛날값:${oldPrice}원 → 바뀐값:${newPrice}원`
  },
)
</script>

<template>
  <div class="practice-section">
    <h2>reactive() 데이터 watch 감시 규칙</h2>
    <h3>🛒 상품 정보 관리 (reactive)</h3>
    <p>상품명: {{ state.productName }} / 가격: {{ state.price }}원</p>
    <button @click="state.price += 500">가격 500원 인상</button>

    <div class="monitor auto">
      <p>👁 1) state 변수 통째로 감시 (deep 자동화)</p>
      <p>{{ logAutoDeep }}</p>
      <small>※ 주의: 이전 값과 현재 값이 똑같이 찍힌다.</small>
    </div>
    <div class="monitor target">
      <p>🎯 2) () => state.price 콕 집어 감시 (과거 추적)</p>
      <p>{{ logTarget }}</p>
      <small>※ 성공: 과거의 원본 가격이 칼같이 보존된다.</small>
    </div>
  </div>
</template>

ref vs reactive watch 감시 비교 총정리

데이터 타입 전체 감시 oldValue 추적 특정 속성 감시
ref (객체) watch(변수, cb, { deep: true }) 불가 watch(() => 변수.value.속성, cb) → 가능
reactive (객체) watch(변수, cb) — deep 자동 적용 불가 watch(() => 변수.속성, cb) → 가능
  • reactive는 .value 없이 속성에 바로 접근하므로 getter 함수에서도 state.price로 씀 (ref는 변수.value.price)

watch() - 배열(Array) 감시 패턴

배열의 특정 인덱스 값이나 객체형 배열의 내부 속성을 감시할 때는 getter 함수 형태로 감시 대상을 지정해야 함.

기본형 배열의 특정 인덱스 값 감시

특정 인덱스의 값 자체(문자열, 숫자 등)를 감시할 때의 패턴임.

// ref()로 생성된 기본형 배열 - 특정 인덱스 감시
const teamMembers = ref(['홍길동', '이순신', '강감찬'])
watch(() => teamMembers.value[0], (새값, 옛값) => { ... })

// reactive()로 생성된 기본형 배열 - 특정 인덱스 감시
const todoList = reactive(['프로젝트 기획', '퍼블리싱', 'Vue 개발'])
watch(() => todoList[0], (새값, 옛값) => { ... })
  • ref 배열은 .value[인덱스], reactive 배열은 .value 없이 배열[인덱스]로 접근함
  • getter 함수 방식이므로 새값과 옛값 모두 정상 수집됨

객체형 배열의 특정 객체 내부 속성 감시

배열의 원소가 객체일 때, 특정 객체의 내부 속성 변화까지 추적하려면 { deep: true } 옵션을 함께 사용해야 함.

// ref()로 생성된 객체형 배열 - 특정 객체 내부 속성 감시
const cityWeather = ref([
  { name: '서울', temp: 25 },
  { name: '수원', temp: 22 }
])
watch(() => cityWeather.value[0], (새객체) => { ... }, { deep: true })

// reactive()로 생성된 객체형 배열 - 특정 객체 내부 속성 감시
const favorCities = reactive([
  { name: '수원', temp: 21 },
  { name: '부산', temp: 24 }
])
watch(() => favorCities[0], (새객체) => { ... }, { deep: true })
  • 객체 내부 속성(예: temp)이 바뀌는 것까지 감지하려면 { deep: true }가 필요함
  • 단, deep: true를 쓰면 oldValue 추적이 불가함 — 이전 속성값이 필요하면 속성까지 getter로 지정해야 함

배열 감시 패턴 정리

상황 ref 배열 reactive 배열
특정 인덱스 기본값 감시 () => 배열.value[i] () => 배열[i]
특정 인덱스 객체 내부 감시 () => 배열.value[i] + { deep: true } () => 배열[i] + { deep: true }

Computed & Watchers - watchEffect()

감시 대상을 명시하지 않아도 함수 내부에서 접근한 반응형 데이터를 자동으로 추적해서 값이 바뀔 때마다 재실행되는 함수임.

핵심 특징

  • 컴포넌트가 처음 태어날 때(최초 1회) 무조건 즉시 실행됨 — watch와의 가장 큰 차이점
  • 함수 내부에서 접근한 반응형 데이터만 자동으로 감시 리스트에 등록됨
  • 현재 시점의 값만 다루므로 이전 값(oldValue)은 제공하지 않음

의존성 자동 추적 원리 — Vue Proxy의 동작 방식

watchEffect가 "어떤 데이터를 감시할지 자동으로 안다"는 것은 Vue의 Proxy 기반 반응형 시스템 덕분임.

동작 순서는 다음과 같음.

  1. watchEffect가 시작되면 Vue는 내부적으로 "의존성 수집 모드" 스위치를 켬 — 지금 실행되는 코드가 건드리는 모든 반응형 데이터를 받아 적겠다는 신호임
  2. 함수 내부가 실행되면서 user.value.name에 접근하는 순간, 해당 객체를 감싸고 있는 Proxy의 Getter가 동작함
  3. Proxy가 Vue에게 "지금 watchEffect가 user.name을 읽어갔어!"라고 제보함
  4. Vue는 이를 감시 목록에 등록해 두었다가, 나중에 user.value.name = '이순신'으로 바뀌면(Setter) watchEffect를 다시 실행시킴

한 줄 요약: watchEffect 함수가 실행되는 그 순간에 실제로 접근된 속성들을 Proxy가 중간에서 낚아채서 감시 목록으로 자동 등록하는 구조임.

속성 수준 추적 — 특정 속성에만 접근한 경우

// watchEffect 의존성 추적 - 특정 속성만 접근
const user = ref({ name: '홍길동', age: 20 })

watchEffect(() => {
// 실행되는 순간 user.value.name 속성에 접근(Getter 실행)!
  console.log(user.value.name)
})
  • user.value.name이 바뀔 때만 watchEffect가 다시 실행됨
  • user.value.age = 21로 age를 바꿔도 watchEffect는 반응하지 않음 — 함수 실행 중에 age에 접근한 적이 없으므로 의존성으로 등록되지 않았기 때문임
  • 이전 값 추적이 안됨

Deep 추적 — 객체 전체를 다루는 경우

// watchEffect 의존성 추적 - 객체 전체 접근
const user = ref({ name: '홍길동', age: 20 })

watchEffect(() => {
// 객체 전체를 사용하거나, JSON.stringify 등으로 내부를 순회할 때
  console.log(user.value)
})
  • 콜백 함수가 실행되는 도중 접근하게 되는 모든 반응형 속성을 실시간으로 수집함
  • 객체 내부의 모든 속성에 접근하게 되면 해당 객체의 모든 변경 사항을 자동으로 추적하게 됨
  • watchEffect의 경우, 개별값 추적이어도 이전값 추적이 안됨!!

watchEffect() 예시

<!-- Computed & Watchers - watchEffect() Example -->
<script setup>
import { ref, watchEffect } from 'vue'

const username = ref('홍길동')
const age = ref(20)
const logMessage = ref('대기 중...')

// 감시 대상을 지정하는 파라미터가 없음!
// Vue가 이 내부 코드를 읽고 'username'과 'age'를 자동으로 감시 리스트에 등록함
watchEffect(() => {
  logMessage.value = `[자동 감지] 이름:${username.value} / 나이:${age.value}세`

// 화면이 처음 켜질 때 1등으로 즉시 실행되는 증거를 콘솔에서 확인함
  console.log('🌐 watchEffect가 내부 변수 변경을 감지하여 실행되었습니다.')
})
</script>

<template>
  <div class="practice-section">
    <h2>자동 감시자 watchEffect()</h2>
    <p>이름: {{ username }} / 나이: {{ age }}세</p>
    <button @click="username = '이순신'">이름을 '이순신'으로 변경</button>
    <button @click="age++">나이 한 살 추가 (age++)</button>
    <div class="monitor">
      <h3>👁 watchEffect 자동 모니터링 시스템</h3>
      <p>{{ logMessage }}</p>
      <small style="color: gray">※ 새로고침하자마자 버튼을 안 눌러도 로그가 이미 찍혀있는 특징을 주목하세요!</small>
    </div>
  </div>
</template>

watch vs watchEffect 비교

항목 watch watchEffect
감시 대상 지정 명시적으로 첫 번째 인자에 지정 함수 내부 접근 기준으로 자동 추적
최초 실행 기본적으로 값이 바뀔 때만 실행 컴포넌트 마운트 시 즉시 1회 실행
oldValue 제공 제공됨 제공되지 않음
의존성 등록 범위 지정한 대상만 실행 중 실제로 접근한 속성만
적합한 용도 특정 값의 변화 전후를 비교해야 할 때 여러 반응형 값을 한꺼번에 추적하고 초기 실행도 필요할 때

Computed & Watchers - 기타 함수들

Computed & Watchers 카테고리의 함수 전체 요약표임.

분류 함수 설명
계산된 속성 선언 computed 기존 반응형 상태를 기반으로 가공된 값을 계산 (캐싱 최적화 지원, 읽기 전용이 기본)
지정형 감시자 watch 특정 반응형 데이터(들)를 지정하여 변경을 감시하고 후속 콜백(이전/현재 값 제공) 실행
자동 의존성 감시자 watchEffect 내부에 사용된 반응형 데이터를 자동으로 추적하여 감시, 컴포넌트 생성 시 즉시 최초 1회 실행
감시자 실행 타이밍 제어 watchPostEffect watchEffect와 동일하나, DOM 업데이트(화면 렌더링)가 완료된 후에 콜백을 실행
  watchSyncEffect watchEffect와 동일하나, 데이터가 변경되는 즉시 동기적(Sync)으로 콜백을 즉각 실행

타이밍 제어 함수 보충

  • watchPostEffect는 DOM이 완전히 업데이트된 이후에 실행되어야 하는 로직(예: DOM 요소의 크기·위치 측정)에 적합함
  • watchSyncEffect는 Vue의 비동기 큐를 거치지 않고 즉시 실행되므로 성능에 영향을 줄 수 있어 특수한 경우에만 사용함
  • 일반적인 상황에서는 watchEffect로 충분하며, watchPostEffect / watchSyncEffect는 실행 타이밍을 세밀하게 제어해야 할 때 선택적으로 활용함

Component Overview

Component란?

Software공학에서 Component는 **독립적인 기능을 수행하며, 언제든지 다른 부품으로 교체할 수 있고, 다른 프로그램과 연결할 수 있는 '표준화된 소프트웨어 모듈(부품)'**을 뜻함. 가장 중요한 핵심은 독립성(Independency)과 교체 가능성(Replaceability)임.

Vue Component란?

웹페이지를 구성하는 독립적이고 재사용 가능한 '블록(부품)'을 말함.

  • HTML, CSS, JavaScript를 하나의 .vue 파일 안에 뭉쳐 놓은 하나의 화면 단위를 뜻함 (SFC)
  • 어플리케이션 하나를 완성하기 위해 여러 개의 컴포넌트를 사용함
  • 컴포넌트는 일정한 형식을 가지고 내부적으로 Tree 구조로 연결됨
<Root>
├── <Header>
├── <Main>
│   └── <Article> x2
└── <Aside>
    └── <Item> x3

Component Overview - Hierarchy

Parent-Child (부모-자식 관계)

  • 다른 컴포넌트를 품고 있는 상위 블록이 부모, 그 안에 박혀서 작동하는 하위 블록이 자식이 됨
  • 부모와 자식은 철저하게 독립되어 있어 자식은 부모의 변수를 가져다 쓸 수 없고, 부모 역시 자식의 내부를 들여다볼 수 없음

Sibling (형제 관계)

  • 동일한 부모 컴포넌트 아래에 나란히 조립된 자식 컴포넌트들끼리의 관계임
  • 형제끼리는 다이렉트로 대화하는 선이 없음 — 형제에게 말을 걸고 싶다면 반드시 부모를 거쳐서 올라갔다가 내려와야 함

Ancestors-Descendants (조상-후손 관계)

  • 컴포넌트가 거대해져서 자식의 자식, 그 자식의 자식까지 내려가는 다중 계층 구조를 말함

Component Overview - Component Local Registration

부모 컴포넌트가 자식 컴포넌트를 import하여 사용하는 방식임. 등록된 자식 컴포넌트는 <template> 영역에서 내장 태그처럼 쓸 수 있으며, PascalCasekebab-case 두 스타일 모두 사용 가능함.

<!-- Component Local Registration -->
<script setup>
import BaseButton from './components/BaseButton.vue'
</script>

<template>
  <div class="box">
    <h3>컴포넌트 조립 테스트</h3>
    <hr />
    <BaseButton />           <!-- PascalCase -->
    <base-button></base-button>  <!-- kebab-case -->
  </div>
</template>

Component Overview - Component Global Registration

전역 등록된 컴포넌트는 Vue 어플리케이션 모든 곳에서 별도 import 없이 사용할 수 있음. 전역 등록은 main.js 파일에서 진행함.

// main.js - Component Global Registration
import { createApp } from 'vue'
import App from './App.vue'

import BaseButton from './components/BaseButton.vue'
import BaseInput from './components/BaseInput.vue'

const app = createApp(App)

// app.component()를 사용해 등록: 인자 (<template>에서 호출할 태그 이름, import한 컴포넌트 변수명)
app.component('BaseButton', BaseButton)
app.component('BaseInput', BaseInput)

// 팁: 체이닝(Chaining) 형태로 줄여 쓸 수도 있다
// app.component('BaseButton', BaseButton).component('BaseInput', BaseInput)

app.mount('#app')

Component Overview - Vue 3 내장함수 (4. Vue Component 기준)

이전 챕터와 동일한 내장함수 표이나, 4. Vue Component 단원에서 새로 강조되는 함수들이 추가됨.

카테고리 주요 함수
애플리케이션 (Application) createApp, createSSRApp, app.*(), app.config.*
반응형 상태 관리 (Reactive State) ref, reactive, readonly, shallowRef, shallowReactive, shallowReadonly, toRef, toRefs, customRef, unref, toRaw, markRaw, isRef, isReactive, isReadonly
계산 및 감시 (Computed & Watchers) computed, watch, watchEffect
라이프사이클 훅 (Lifecycle Hooks) setup, onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount, onActivated, onDeactivated, onErrorCaptured, onRenderTracked, onRenderTriggered
컴포넌트 구성/메타 (Component Composition) defineComponent, defineProps, defineEmits, useAttrs, defineExpose, useSlots, withDefaults, getCurrentInstance
렌더링 제어 (Rendering & DOM) h, resolveComponent, withDirectives, renderList, renderSlot, mergeProps, nextTick, useCssModule, useCssVars
의존성 주입 (Dependency Injection) provide, inject, hasInjectionContext

이번 단원에서 새로 강조되는 함수: 라이프사이클 훅의 setup, onMounted, onUpdated, onUnmounted, 컴포넌트 구성의 defineProps, defineEmits, 의존성 주입의 provide, inject임.


Component Lifecycle

컴포넌트가 생성되고 파괴되기까지의 여러 단계를 의미함.

라이프사이클 단계 컴포넌트의 현재 상태 상세 내용
1. 생성 (Creation) 컴포넌트가 메모리상에 막 태어난 단계 (JavaScript 상에만 존재하고, 아직 HTML에는 안 붙음) 데이터(ref, reactive)와 computed, watch 센서들이 초기화되어 가동을 시작함
2. 부착 (Mounting) 가상으로 만들던 화면을 진짜 브라우저 HTML(DOM)에 붙인 단계 화면 요소(div, button 등)에 직접 접근할 수 있게 되며, 백엔드 API를 호출하여 초기 데이터를 받아오기에 가장 완벽한 타이밍
3. 갱신 (Updating) 반응형 데이터(ref 등)가 바뀌어서 화면을 싹 뜯어고치고 다시 그리는(Re-rendering) 단계 데이터가 변했을 때 화면이 새로 그려진 직후, 바뀐 HTML 요소들의 크기나 스크롤 위치를 다시 계산할 때 씀
4. 소멸 (Unmounting) v-if="false" 등으로 인해 컴포넌트가 화면에서 완전히 지워지고 파괴되는 단계 메모리 누수를 막기 위해 사용 중이던 실시간 타이머(setInterval)를 끄거나, 전역 이벤트 리스너를 청소(Clean-up)함

Component Lifecycle - Lifecycle Hooks

컴포넌트의 생명주기 각 단계에 맞춰 Vue 엔진이 자동으로 실행해 주는 콜백 함수임.

단계 훅 함수 설명
생성 전 setup() 컴포넌트 생성 전 가장 먼저 실행. reactive 변수 및 함수 초기화
마운트 전 onBeforeMount() DOM에 마운트되기 직전에 호출
마운트 완료 onMounted() DOM에 마운트된 후 호출. 초기 DOM 조작, API 호출 등에 사용
업데이트 전 onBeforeUpdate() 반응형 데이터가 변경되어 DOM이 다시 업데이트되기 전
업데이트 후 onUpdated() DOM 업데이트가 완료된 후 호출
언마운트 전 onBeforeUnmount() 컴포넌트가 DOM에서 제거되기 직전에 호출
언마운트 완료 onUnmounted() 컴포넌트가 DOM에서 제거된 후 호출
에러 처리 (선택) onErrorCaptured() 자식 컴포넌트에서 에러가 발생했을 때 캐처
렌더 트리 캐시 복구용 onActivated() <KeepAlive> 안에서 재활성화될 때 호출
렌더 트리 캐시 비활성화 onDeactivated() <KeepAlive> 안에서 비활성화될 때 호출

실무에서 핵심적으로 자주 쓰이는 훅은 setup, onMounted, onUpdated, onUnmounted 네 가지임.

→ 제일 많이 쓰는 것이 onMounted, onUnmounted 임

→ 화면에 Component 가 onMounted, onUnmounted 될 때, 데이터를 가져옴


Lifecycle Hook 예시

<!-- Component Lifecycle - Lifecycle Hook Example -->
<script setup>
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'

const count = ref(0)
let timerId = null // 실시간 타이머 메모리 주소를 담을 변수

// 생성 (Creation) 단계 = <script setup> 본문 그 자체
console.log('1. [setup] 컴포넌트가 메모리에 생성되었습니다. (DOM 접근 불가능)')

// 부착 (Mounting) 단계
onMounted(() => {
  console.log('2. [onMounted] 화면에 완벽히 부착되었습니다! (API 호출/DOM 조작 적기)')
// 실무 활용 시뮬레이션: 3초마다 숫자가 자동으로 올라가는 타이머 가동
  timerId = setInterval(() => {
    count.value++
  }, 3000)
})

// 갱신 (Updating) 단계 - count 변수가 바뀌어서 화면이 리렌더링(새로고침)될 때마다 매번 실행됨
onUpdated(() => {
  console.log(`3. [onUpdated] 데이터가 변경되어 화면을 새로 그렸습니다. (현재 count:${count.value})`)
})

// 소멸 (Unmounting) 단계 - v-if="false" 등으로 이 컴포넌트가 화면에서 완전히 파괴되어 사라질 때 실행됨
onUnmounted(() => {
// ❌ 주의: 여기서 타이머를 안 꺼주면 컴포넌트가 사라져도 백그라운드에서 영원히 타이머가 돔 (메모리 누수)
  clearInterval(timerId)
  console.log('4. [onUnmounted] 컴포넌트가 소멸했습니다. 타이머 청소 완료!')
})
</script>

단계별 실행 순서 요약

  • setup 본문 → onBeforeMount → onMounted → (데이터 변경 시) onBeforeUpdate → onUpdated → (컴포넌트 제거 시) onBeforeUnmount → onUnmounted
  • onMounted는 DOM 접근과 API 호출이 가능한 첫 시점이므로 초기 데이터 패칭의 표준 위치로 사용함
  • onUnmounted에서 clearInterval, 이벤트 리스너 제거 등 정리 작업을 반드시 수행해야 메모리 누수를 막을 수 있음

Props & Emits

Component 연동 구조

Vue 3의 모든 컴포넌트 연동은 "데이터는 위에서 아래로 물려주고, 이벤트는 아래에서 위로 쏘아 올린다"는 단방향 흐름 구조를 따름.

분류 Props (하행선 ▼) Emits (상행선 ▲)
개념 정의 부모가 자식에게 주는 반응형 데이터 값 자식이 부모에게 보고하는 이벤트
흐름 방향 부모 → 자식 (위에서 아래로) 자식 → 부모 (아래에서 위로)
데이터 권한 읽기 전용. 자식은 수정 불가 부모에게 변경 요청 및 값 전달 가능
Compiler Macro defineProps({ ... }) defineEmits([ ... ])
Parent Binding 자식 태그 속성에 콜론(:)으로 주입 자식 태그 이벤트에 골뱅이(@)로 청취

 

Compiler Macro란 Runtime 시점이 아닌 Build 시점에 Vue Compiler가 코드를 변환하는 특수 예약어임. defineProps(), defineEmits(), defineExpose() 같은 함수들은 <script setup>에서만 사용 가능함.


Props & Emits - defineProps()

자식 컴포넌트 내부에서 "부모가 넘겨줄 데이터(속성)의 이름과 규격"을 선언하는 Vue 3 내장 컴파일러 매크로 함수임. <script setup> 안에서 import 없이 즉시 호출할 수 있음.

정의 형식 비교

정의 형식 예시 기본값 지정 타입 유효성
배열 형식 ['name', 'age']
객체 형식 { name: String, age: Number } ✅ (default 속성)
// defineProps() - 배열 형식 (간단)
const props = defineProps(['title', 'count'])

// defineProps() - 객체 형식 (기본값 지정)
defineProps({
// 1. 타입만 간단히 지정하는 경우
  title: String,

// 2. 필수 값과 기본값까지 꼼꼼하게 지정하는 경우
  likes: {
    type: Number,
    required: true // 부모가 이 값을 안 넘기면 에러발생
  },
  status: {
    type: String,
    default: '대기 중' // 부모가 값을 안 주면 이 값이 기본으로 세팅
  }
})

→ 부모로부터 title, count 혹은 title, likes, status 를 받겠다라고 선언하는 것

template / script 에서 사용하기

<!-- defineProps() - template에서 사용 -->
<template>
  <h1>{{ title }}</h1>      <!-- props 변수명 그대로 사용 -->
  <p>좋아요: {{ likes }}</p>
</template>
<!-- defineProps() - script setup에서 사용 -->
<script setup>
// 반환값을 변수에 받아서 .점 문법으로 접근해야 함
const props = defineProps({
  title: String,
  likes: Number
})

// 내부 함수에서 쓸 때는 props.을 앞에 꼭 붙여야 함
const checkPopularity = () => {
  if (props.likes > 100) {
    console.log(`${props.title}은 인기 게시글입니다.`)
  }
}
</script>

Readonly 규칙

defineProps로 전달된 값은 읽기 전용임. Child Component에서 직접 수정하려 하면 에러가 발생함.

// defineProps() - ReadOnly 주의
const props = defineProps(['likes'])

const brokenFunction = () => {
// ❌ 절대 금지! 콘솔에 ReadOnly 에러 발생
  props.likes = 999
}

부모 → 자식 데이터 전달 예시

<!-- ChildComponent.vue -->
<script setup>
const props = defineProps({
  message: String
})
</script>

<template>
  <div>{{ message }}</div>
</template>
<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const parentMessage = ref('안녕하세요, 자식 컴포넌트!')
</script>

<template>
  <!-- 콜론(:)으로 반응형 데이터를 props로 주입 -->
  <ChildComponent :message="parentMessage" />
</template>

camelCase / kebab-case 네이밍 컨벤션

Component 내부 데이터는 camelCase, 컴포넌트 속성(HTML 태그)은 kebab-case로 작성함. HTML 표준 마크업 언어는 대소문자를 구분하지 못하고 전부 소문자로 인식하기 때문이며, Vue 엔진이 JavaScript의 camelCase를 HTML의 kebab-case로 자동 매핑해 줌.

<!-- ParentComponent.vue - camelCase → kebab-case 자동 매핑 -->
<script setup>
import { ref } from 'vue'
import WeatherCard from './components/WeatherCard.vue'

const selectCityName = ref('수원시 영통구')  // 내부 데이터: camelCase
const areaCode = ref(103)
</script>

<template>
  <div>
    <WeatherCard
      :city-name="selectCityName"   <!-- 속성: kebab-case -->
      :area-code="areaCode"
    />
  </div>
</template>
<!-- ChildComponent(WeatherCard.vue) - 수신 측: camelCase -->
<script setup>
defineProps({
  cityName: String,   // camelCase로 선언
  areaCode: Number
})
</script>

<template>
  <div class="card">
    <p>지역명: {{ cityName }}</p>
    <p>지역 코드: {{ areaCode }}</p>
  </div>
</template>

Prop Validation (유효성 검사)

부모가 넘긴 데이터에 대한 유효성 검사를 통해 에러 추적의 용이성을 높이고 견고한 컴포넌트를 만들 수 있음.

// defineProps() - Prop Validation 전체 예시
defineProps({
// 1. 가장 기본: 타입(Type)만 단독 검사
  cityName: String,

// 2. 다중 타입 허용: 문자열 혹은 숫자 둘 다 괜찮을 때
  areaId: [String, Number],

// 3. 필수 여부(Required): 부모가 무조건 넘겨야 하는 필수 데이터일 때
  temperature: {
    type: Number,
    required: true  // 안 넘기면 콘솔에 [Vue warn]: Missing required prop 경고가 뜸
  },

// 4. 기본값(Default): 부모가 깜빡하고 안 넘겼을 때 채워줄 디폴트 값
  status: {
    type: String,
    default: '맑음'  // 부모가 값을 생략하면 '맑음'으로 자동 세팅
  },

// 5. 커스텀 유효성 검사기(Validator): 내 입맛대로 세부 조건 필터링
  score: {
    type: Number,
    validator(value) {
      return value >= 0 && value <= 100  // 값이 0부터 100 사이일 때만 합격(true)
    }
  }
})

Vue Props 지원 자료형(Type) 종류

유형 defineProps 기입 법 실제 넘어오는 데이터 예시 특징
문자열 String "서울", "25°C", "Sunny" 따옴표로 감싸진 모든 텍스트
숫자 Number 25, -5, 3.14 소수점, 음수 포함 모든 숫자
논리형 Boolean true, false  
배열 Array ['서울', '수원', '부산'] default 지정 시 반드시 함수 형태 () => []로 작성
객체 Object { name: '수원', temp: 24 } default 지정 시 반드시 함수 형태 () => ({})로 작성
함수 Function () => console.log('클릭') 자식 컴포넌트가 실행할 콜백 함수 자체를 통째로 넘겨줄 때 씀 (사용빈도 낮음)
기타 Date, Symbol, Promise new Date() 등 특정 날짜 객체나 비동기 객체 검증도 지원
// Vue Props 지원 자료형 - 전체 예시
defineProps({
  cityName: String,        // 1. 문자열
  temperature: Number,     // 2. 숫자

  isActive: {              // 3. 논리형
    type: Boolean,
    default: false
  },

  weeklyForecast: {        // 4. 배열
    type: Array,
// 중요: 배열의 기본값은 무조건 '새 바구니를 구워내는 화살표 함수' 형태로!
    default: () => []
  },

  coordinates: {           // 5. 객체
    type: Object,
// 중요: 객체의 기본값도 무조건 화살표 함수 형태로 반환!
    default: () => ({ lat: 37.5, lng: 126.9 })
  }
})

Props & Emits - defineEmits()

자식 컴포넌트에서 부모에게 사용자 정의 이벤트를 전달하기 위해 사용하는 Vue 3 내장 컴파일러 매크로 함수임. <script setup> 안에서 import 없이 즉시 호출할 수 있음.

핵심 동작 방식

  • 자식 컴포넌트 내부의 브라우저 표준 이벤트(클릭, 키보드 입력 등) 또는 내부 로직 변화를 트리거로 삼아, 부모 컴포넌트가 등록한 Custom Event Listener를 호출하여 콜백 함수를 가동함
  • 데이터 전달(Payload): emit() 함수의 첫 번째 인자로 이벤트 식별 문자열을, 두 번째 인자부터는 부모 컴포넌트의 콜백 함수로 인계할 Payload(데이터)를 Argument로 바인딩하여 전달함
  • 이벤트 타입명은 kebab-case로 작성함

defineEmits() Example 1 — 기본 사용

<!-- ChildComponent.vue - defineEmits 기본 -->
<script setup>
// 발생시킬 이벤트 타입을 배열로 등록
const emit = defineEmits(['childEvent'])

const sendToParent = () => {
// emit('이벤트타입', 보낼데이터)으로 이벤트 발생
  emit('childEvent', '안녕하세요, 부모 컴포넌트!')
}
</script>

<template>
  <button @click="sendToParent">부모에게 메시지 보내기</button>
</template>
<!-- ParentComponent.vue - 자식 이벤트 수신 -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

// @이벤트타입="이벤트핸들러"로 수신
const handleChildEvent = (message) => {
  console.log('자식으로부터 받은 메시지:', message)
}
</script>

<template>
  <ChildComponent @childEvent="handleChildEvent" />
</template>

defineEmits() Example 2 — Props + Emits 통합

<!-- WeatherCard.vue (ChildComponent) -->
<script setup>
// 부모에게 물려받을 Props
defineProps({
  cityName: String,
  status: String
})

// 부모에게 전달할 커스텀 이벤트 타입을 배열로 등록
// (관례상 변수명은 'emit'이라고 똑같이 지어줌)
const emit = defineEmits(['select-city'])

// 카드가 클릭되었을 때 실행될 함수
const handleCardClick = (name) => {
// 부모에게 'select-city' 이벤트 발생하면서, 선택된 도시 이름 전달
  emit('select-city', name)
}
</script>

<template>
  <div class="weather-card" @click="handleCardClick(cityName)">
    <h4>{{ cityName }} ({{ status }})</h4>
    <p>클릭하면 부모에게 신호를 보냅니다.</p>
  </div>
</template>
<!-- ParentComponent.vue -->
<script setup>
import { ref } from 'vue'
import WeatherCard from './components/WeatherCard.vue'

const selectedCityInfo = ref('카드를 클릭해 보세요.')

// 자식이벤트 발생 시 이벤트 핸들러
const receiveCitySignal = (cityName) => {
  selectedCityInfo.value = `${cityName}이(가) 성공적으로 선택되었습니다.`
}
</script>

<template>
  <div class="parent-box">
    <h2>부모 관제탑 (Emits 수신 패널)</h2>
    <!-- @이벤트타입(kebab-case)="핸들러"로 청취 -->
    <WeatherCard cityName="서울" status="맑음" @select-city="receiveCitySignal" />
    <WeatherCard cityName="수원" status="비" @select-city="receiveCitySignal" />
    <div class="status-bar">{{ selectedCityInfo }}</div>
  </div>
</template>

Props & Emits - 통합 예시

Props(하행)와 Emits(상행)이 함께 동작하는 완전한 부모-자식 연동 패턴임.

<!-- PropsEmitsParent.vue -->
<script setup>
import { ref } from 'vue'
import PropsEmitsChild from './PropsEmitsChild.vue'

// 1. 상위 컴포넌트의 로컬 반응형 상태 정의
const message = ref('Parent 초기 메시지')

// 2. 하위 컴포넌트의 커스텀 이벤트를 수신했을 때 실행될 핸들러 함수
// 인자(newValue)로 하위 컴포넌트가 보낸 페이로드가 자동 주입됨
const handleUpdateRequest = (newValue) => {
  message.value = newValue
}
</script>

<template>
  <div class="practice-section">
    <h2>Props & Emits</h2>
    <div class="parent-container">
      <h2>상위 컴포넌트 (Parent)</h2>
      <p>현재 로컬 데이터(State): <strong>{{ message }}</strong></p>
      <br />
      <!-- Props: :parent-data로 내려줌 / Emits: @update-request로 청취 -->
      <PropsEmitsChild
        :parent-data="message"
        @update-request="handleUpdateRequest"
      />
    </div>
  </div>
</template>
<!-- PropsEmitsChild.vue -->
<script setup>
// 1. 상위 컴포넌트로부터 주입받을 데이터의 자료형 및 필수 여부 정의
defineProps({
  parentData: {
    type: String,
    required: true,
  },
})

// 2. 상위 컴포넌트로 송신할 커스텀 이벤트 식별자 등록
const emit = defineEmits(['update-request'])

// 3. 내부 이벤트 발생 시 페이로드를 실어 상위로 이벤트를 디스패치하는 함수
const sendNotification = () => {
  const payload = 'Child에서 가공한 새로운 데이터'
  emit('update-request', payload)
}
</script>

<template>
  <div class="child-container">
    <h2>하위 컴포넌트 (Child)</h2>
    <p>수신된 Props 데이터: <strong>{{ parentData }}</strong></p>
    <br />
    <button @click="sendNotification">상위 컴포넌트로 갱신 요청 (Emit)</button>
  </div>
</template>

Props & Emits 흐름 요약

ParentComponent
  ↓  :parent-data="message"       (Props 하행: 데이터 주입)
ChildComponent
  ↑  emit('update-request', data) (Emits 상행: 이벤트 발사)
ParentComponent
  → handleUpdateRequest(newValue) 실행 → message.value 갱신

Provide & Inject

등장 배경 — Props Drilling 문제

컴포넌트 아키텍처의 계층이 깊어질 때(예: 상위 → 하위 → 최하위), 중간에 위치한 컴포넌트들은 해당 데이터가 필요 없음에도 오직 최하위 컴포넌트로 전달하기 위해 Props를 받아 아래로 토스하는 과정을 반복해야 하는 현상을 Props Drilling이라 함.

[Props Drilling 문제]          [Provide & Inject 해결]

<Root>                         <Root> ──── provide('globalTheme')
  └── <Footer>                   └── <Footer>
        └── <DeepChild>                └── <DeepChild> ← inject('globalTheme')
             ↑ Props로 계속 토스           ↑ 중간 계층 건너뛰고 직접 수신
  • 중간 계층의 컴포넌트들을 완전히 건너뛰고, 조상 컴포넌트가 선언한 반응형 상태를 하위 컴포넌트에서 inject 함수를 통해 다이렉트로 결합하여 사용하는 방식임
  • 중첩된 컴포넌트 계층에서도 중간 컴포넌트가 해당 값을 몰라도 전달 가능함
  • [참고] 전역 상태 관리 라이브러리(Pinia)로 인해 Provide & Inject 사용빈도는 높지 않음

핵심 코드

// GrandParent.vue - provide 측
import { ref, provide } from 'vue'

const themeColor = ref('dark-mode')

// 주입할 키(Key) 이름과 실제 데이터(Value)를 등록
provide('globalTheme', themeColor)
// GrandChild.vue - inject 측
import { inject } from 'vue'

// 상위 조상이 provide한 키 이름을 지정하여 직접 인젝션 유도
const theme = inject('globalTheme')

→ 기본적으로 단방향 — 조상이 provide, 후손이 inject하는 방향만 지원함.

→ 반대 방향(자식 → 부모)은 provide/inject로는 안 되고, 그건 Emits가 담당하는 영역임.

provide / inject 사용 규칙 요약

항목 provide inject
사용 위치 조상 컴포넌트 (<script setup>) 후손 컴포넌트 (<script setup>)
import import { provide } from 'vue' import { inject } from 'vue'
문법 provide('키', 값) const 변수 = inject('키')
반응형 유지 ref/reactive를 그대로 넘기면 반응형 유지됨 수신한 값도 반응형 그대로 사용 가능

Component Slot

자식 컴포넌트의 특정 구역을 비워두고, 부모 컴포넌트가 자식 컴포넌트를 호출할 때 내장할 HTML 마크업 및 템플릿 콘텐츠를 동적으로 주입받아 렌더링할 수 있도록 해주는 기능임.

Props가 부모가 자식에게 데이터를 주입한다면, Slot은 HTML 마크업 태그나 디자인 레이아웃 자체를 주입함.

Slot 유형 3가지

  • Default Slot: 자식 컴포넌트에서 <slot> 태그만 사용하여 부모로부터 전달받은 콘텐츠를 표시
  • Named Slot: 여러 슬롯이 필요한 경우 v-slot:name 또는 #name을 사용해 위치에 지정
  • Scoped Slot: 자식 컴포넌트에서 부모 컴포넌트로 데이터를 전달할 때 사용 (v-slot 바인딩)

Component Slot - Default Slot

별다른 속성 없이 단순하게 <slot> 태그만 사용하는 슬롯임. 부모로부터 교체할 템플릿 조각이 넘어오지 않으면 <slot></slot> 내 기본 콘텐츠가 렌더링됨.

<!-- SlotDefaultChild.vue - slot outlet 정의 -->
<template>
  <div class="base-card">
    <slot>
      <p>기본 콘텐츠 영역입니다.</p>  <!-- 부모가 아무것도 안 넣으면 이게 보임 -->
    </slot>
  </div>
</template>
<!-- SlotDefaultParent.vue - slot content 주입 -->
<script setup>
import SlotDefaultChild from '@/components/practices/component/SlotDefaultChild.vue'
</script>

<template>
  <div class="practice-section">
    <h2>Default Slot 레이아웃 주입 실습</h2>

    <!-- 1) 텍스트 주입 -->
    <SlotDefaultChild>
      <p>단순한 텍스트 문장을 주입합니다.</p>
    </SlotDefaultChild>

    <!-- 2) 복잡한 마크업 주입 -->
    <SlotDefaultChild>
      <h2 style="color: #e74c3c">🔥 경고 상태</h2>
      <button>확인</button>
    </SlotDefaultChild>

    <!-- 3) 아무것도 안 넣으면 자식의 기본값 렌더링 -->
    <SlotDefaultChild></SlotDefaultChild>
  </div>
</template>

최종 렌더링 결과 — 부모가 넣어준 콘텐츠가 <slot> 자리를 그대로 대체함.

<!-- 최종 렌더링 결과 -->
<div class="base-card">
  <p>단순한 텍스트 문장을 주입합니다.</p>   <!-- 1번: 부모 콘텐츠 -->
</div>

<div class="base-card">
  <h2 style="color: #e74c3c">🔥 경고 상태</h2>   <!-- 2번: 부모 콘텐츠 -->
  <button>확인</button>
</div>

<div class="base-card">
  <p>기본 콘텐츠 영역입니다.</p>   <!-- 3번: 부모가 아무것도 안 넣어서 자식 기본값 -->
</div>

Component Slot - Named Slot

자식 컴포넌트 내에서 여러 개의 슬롯을 사용할 때 <slot name="값"> 으로 이름을 지정함. 부모 컴포넌트에서는 <template v-slot:값> 또는 축약형 <template #값>으로 해당 슬롯에 콘텐츠를 주입함.

<!-- SlotNamedChild.vue - named slot outlet 정의 -->
<template>
  <div class="base-card">
    <header>
      <slot name="header"></slot>   <!-- 이름있는 슬롯 -->
    </header>
    <main>
      <slot></slot>                 <!-- 이름없는 default 슬롯 -->
    </main>
  </div>
</template>
<!-- SlotNamedParent.vue - named slot content 주입 -->
<script setup>
import SlotNamedChild from './SlotNamedChild.vue'
</script>

<template>
  <div class="practice-section">
    <h2>Named Slot 주입 실습</h2>
    <SlotNamedChild>
      <!-- v-slot:슬롯이름으로 해당 위치에 주입 -->
      <template v-slot:header>
        <h3>Child 주입 제목</h3>
      </template>

      <!-- template 없이 작성하면 default slot으로 들어감 -->
      <p>"Lorem ipsum dolor sit amet, consectetur adipiscing elit..."</p>
    </SlotNamedChild>
  </div>
</template>

최종 렌더링 결과 — 각 v-slot:이름이 대응하는 <slot name="이름"> 자리를 채움.

<!-- 최종 렌더링 결과 -->
<div class="base-card">
  <header>
    <h3>Child 주입 제목</h3>           <!-- name="header" 슬롯 자리 -->
  </header>
  <main>
    <p>"Lorem ipsum dolor sit amet..."</p>  <!-- default 슬롯 자리 -->
  </main>
</div>

Component Slot - Scoped Slot

자식 컴포넌트에서 부모 컴포넌트로 데이터를 전달할 때 사용함. 일반 Slot과 흐름이 반대로, 자식이 <slot> 태그의 속성 바인딩(:이름="변수명")을 통해 데이터를 위로 올려 보내고, 부모는 v-slot="변수주머니이름"으로 수신하여 HTML 코드 안에 배치함.

<!-- SlotScopedChild.vue - 자식: 데이터를 slot 속성으로 올려 보냄 -->
<script setup>
import { ref } from 'vue'

// 하위 컴포넌트 내부에서 관리하는 2개의 서버 상태 데이터
const message = ref('현재 서버 상태 정상')
const userCount = ref(150)
</script>

<template>
  <div class="base-card">
    <h3>하위 컴포넌트 (Child)</h3>
    <!-- :text, :count 속성으로 부모에게 데이터를 올려 보냄 -->
    <slot :text="message" :count="userCount">
      <p>부모가 마크업을 주입하지 않았을 때의 디폴트 화면</p>
    </slot>
  </div>
</template>
<!-- SlotScopedParent.vue - 부모: v-slot="변수주머니"로 수신 -->
<script setup>
import SlotScopedChild from './SlotScopedChild.vue'
</script>

<template>
  <div class="practice-section">
    <h2>Scoped Slot 주입 실습</h2>
    <h3>상위 컴포넌트 (Parent)</h3>

    <!-- v-slot="slotBag"으로 자식이 올린 데이터를 통째로 수신 -->
    <SlotScopedChild v-slot="slotBag">
      <div class="display-panel">
        <p>알림 메시지: {{ slotBag.text }}</p>
        <p>접속자 수: {{ slotBag.count }}명</p>
      </div>
    </SlotScopedChild>

    <!-- 아무것도 안 넣으면 자식의 기본값 화면 렌더링 -->
    <SlotScopedChild></SlotScopedChild>
  </div>
</template>

최종 렌더링 결과 — 자식이 올려 보낸 message, userCount를 부모가 받아서 마크업 안에 배치함.

<!-- 최종 렌더링 결과 -->

<!-- 1번: 부모가 slotBag으로 수신한 데이터를 배치한 경우 -->
<div class="base-card">
  <h3>하위 컴포넌트 (Child)</h3>
  <div class="display-panel">
    <p>알림 메시지: 현재 서버 상태 정상</p>   <!-- slotBag.text -->
    <p>접속자 수: 150명</p>                   <!-- slotBag.count -->
  </div>
</div>

<!-- 2번: 부모가 아무것도 안 넣은 경우 → 자식 기본값 렌더링 -->
<div class="base-card">
  <h3>하위 컴포넌트 (Child)</h3>
  <p>부모가 마크업을 주입하지 않았을 때의 디폴트 화면</p>
</div>

3가지 Slot 비교 요약

종류 자식 측 부모 측 데이터 흐름
Default Slot <slot></slot> 태그 사이에 콘텐츠 작성 부모 → 자식 (마크업 주입)
Named Slot <slot name="헤더"> <template v-slot:헤더> 부모 → 자식 (위치 지정 주입)
Scoped Slot <slot :data="값"> <태그 v-slot="bag"> 자식 → 부모 (데이터 역방향)