본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 10. 30. · 19 Views
Vue3 Composition API 완벽 가이드
Vue 3의 Composition API를 처음 배우는 개발자를 위한 완벽 가이드입니다. setup(), ref(), reactive(), computed() 등 핵심 개념부터 실무에서 바로 활용할 수 있는 고급 패턴까지 단계별로 학습합니다. 실제 프로젝트에서 사용할 수 있는 풍부한 예제와 함께 Vue 3의 강력한 기능을 마스터해보세요.
목차
- setup 함수 - Composition API의 시작점
- ref - 기본 타입의 반응형 상태 만들기
- reactive - 객체의 반응형 상태 만들기
- computed - 계산된 값 만들기
- watch - 반응형 값의 변경 감지하기
- watchEffect - 자동 의존성 추적과 부수 효과
- toRefs와 toRef - 반응성 유지하면서 구조 분해하기
- provide와 inject - 컴포넌트 간 의존성 주입
1. setup 함수 - Composition API의 시작점
시작하며
여러분이 Vue 2에서 Options API로 개발하다가 컴포넌트가 복잡해지면서 data, methods, computed, watch가 여기저기 흩어져서 관련 로직을 찾기 어려웠던 경험 있으신가요? 특히 사용자 인증 로직이 data에도 있고, methods에도 있고, computed에도 있어서 코드를 왔다갔다 하면서 읽어야 했던 적이 있을 겁니다.
이런 문제는 대규모 프로젝트에서 특히 심각합니다. 한 기능을 수정하려면 파일 전체를 스크롤하면서 관련 코드를 모두 찾아야 하고, 팀원이 작성한 코드를 이해하는 데도 시간이 오래 걸립니다.
또한 로직을 재사용하려면 mixin을 사용해야 하는데, mixin은 이름 충돌과 출처 불명확 문제를 일으킵니다. 바로 이럴 때 필요한 것이 setup 함수입니다.
setup 함수는 Composition API의 진입점으로, 관련 로직을 하나로 모으고, 재사용 가능한 함수로 쉽게 추출할 수 있게 해줍니다.
개요
간단히 말해서, setup 함수는 Vue 3 컴포넌트에서 Composition API를 사용하기 위한 시작점입니다. Vue 2의 Options API에서는 data, methods, computed 등을 각각의 옵션으로 분리해서 작성했다면, setup 함수 안에서는 모든 로직을 기능별로 묶어서 관리할 수 있습니다.
예를 들어, 사용자 프로필 편집 기능의 모든 로직(상태, 함수, 계산된 값)을 setup 함수 안의 한 곳에 모아둘 수 있어서 코드를 읽고 수정하기가 훨씬 쉬워집니다. 기존에는 컴포넌트 옵션을 여러 섹션으로 나누어 작성했다면, 이제는 setup 함수 하나 안에서 모든 것을 선언하고, 필요한 것만 return으로 템플릿에 노출시킬 수 있습니다.
setup 함수는 컴포넌트가 생성되기 전에 실행되며, props와 context를 인자로 받습니다. this를 사용할 수 없다는 점이 중요한데, 이는 오히려 명시적인 의존성 관리를 가능하게 합니다.
또한 return한 값들만 템플릿에서 사용할 수 있어서 어떤 값이 노출되는지 명확하게 알 수 있습니다.
코드 예제
<script>
import { ref, computed } from 'vue'
export default {
name: 'UserProfile',
setup() {
// 반응형 상태 선언
const userName = ref('홍길동')
const age = ref(25)
// 계산된 값
const userInfo = computed(() => {
return `${userName.value} (${age.value}세)`
})
// 메서드
function updateName(newName) {
userName.value = newName
}
// 템플릿에 노출할 것들만 return
return {
userName,
age,
userInfo,
updateName
}
}
}
</script>
설명
이것이 하는 일: setup 함수는 컴포넌트 인스턴스가 생성되기 전에 실행되어, 반응형 상태와 로직을 정의하고 템플릿에 필요한 것들을 반환합니다. 첫 번째로, ref()를 사용해서 userName과 age라는 반응형 상태를 만듭니다.
이 값들은 변경되면 자동으로 UI가 업데이트됩니다. ref로 만든 값은 .value 속성을 통해 접근하고 수정할 수 있습니다.
그 다음으로, computed()를 사용해서 userInfo라는 계산된 값을 만듭니다. 이 값은 userName이나 age가 변경될 때마다 자동으로 다시 계산되며, 캐싱되어 성능이 최적화됩니다.
템플릿에서는 .value 없이 바로 사용할 수 있습니다. 세 번째로, updateName이라는 일반 함수를 정의합니다.
이 함수는 새로운 이름을 받아서 userName의 값을 업데이트합니다. Options API의 methods와 달리, setup 안에서는 그냥 일반 함수로 작성하면 됩니다.
마지막으로, return 객체를 통해 템플릿에서 사용할 값들을 선택적으로 노출합니다. return하지 않은 값은 템플릿에서 접근할 수 없어서, 내부 로직을 캡슐화할 수 있습니다.
여러분이 이 코드를 사용하면 관련 로직을 한 곳에 모아서 관리할 수 있고, 필요한 경우 이 로직을 별도 함수로 쉽게 추출해서 재사용할 수 있습니다. 또한 TypeScript와의 통합이 훨씬 자연스러워지며, 코드 자동완성과 타입 체크가 더 잘 작동합니다.
실전 팁
💡 setup 함수 안에서는 this를 사용할 수 없습니다. 모든 값은 명시적으로 선언하거나 import해서 사용하세요. 이렇게 하면 의존성이 명확해져서 코드 추적이 쉬워집니다.
💡 return 객체가 커지면 코드가 복잡해집니다. 이럴 때는 <script setup> 문법을 사용하면 자동으로 모든 선언이 템플릿에 노출되어 return을 생략할 수 있습니다.
💡 관련 로직을 composable 함수로 추출하세요. 예를 들어 useUserProfile() 같은 함수를 만들어서 여러 컴포넌트에서 재사용할 수 있습니다.
💡 setup은 동기 함수여야 합니다. async/await를 사용하려면 setup 내부에서 별도 함수를 만들거나, Suspense 컴포넌트와 함께 사용해야 합니다.
💡 props를 setup 함수의 첫 번째 인자로 받을 수 있지만, 구조 분해하면 반응성을 잃습니다. toRefs()를 사용해서 구조 분해하면서도 반응성을 유지하세요.
2. ref - 기본 타입의 반응형 상태 만들기
시작하며
여러분이 사용자 입력을 받는 폼을 만들 때, 입력값이 변경될 때마다 자동으로 UI가 업데이트되어야 하는 경우가 많습니다. 예를 들어 검색창에 글자를 입력하면 실시간으로 검색 결과가 나타나거나, 카운터 버튼을 클릭하면 숫자가 즉시 증가하는 것처럼 말이죠.
이런 반응형 동작을 구현하려면 Vue가 값의 변경을 감지할 수 있어야 합니다. 하지만 JavaScript의 기본 타입(숫자, 문자열, 불리언)은 그냥 변수에 할당하면 Vue가 변경을 추적할 수 없습니다.
Vue 2에서는 data 함수에 넣어서 해결했지만, Composition API에서는 다른 방식이 필요합니다. 바로 이럴 때 필요한 것이 ref입니다.
ref는 기본 타입 값을 감싸서 반응형으로 만들어주며, Vue가 값의 변경을 감지하고 자동으로 관련된 UI를 업데이트할 수 있게 해줍니다.
개요
간단히 말해서, ref는 기본 타입 값(숫자, 문자열, 불리언 등)을 반응형 객체로 감싸주는 함수입니다. Vue 3에서 반응형 상태를 만드는 가장 기본적인 방법이 ref입니다.
왜냐하면 JavaScript의 Proxy는 객체만 감지할 수 있는데, ref가 기본 타입을 객체로 감싸주기 때문입니다. 예를 들어, 카운터 앱을 만들거나, 로딩 상태를 관리하거나, 사용자 이름을 저장할 때 ref를 사용합니다.
기존 Vue 2의 data 옵션에서는 this.count = 0으로 선언했다면, 이제는 const count = ref(0)으로 선언하고 count.value로 접근합니다. ref의 핵심 특징은 세 가지입니다.
첫째, .value 속성으로 실제 값에 접근합니다(템플릿에서는 자동으로 언래핑되어 .value 불필요). 둘째, 값이 변경되면 자동으로 관련된 computed와 watch가 실행됩니다.
셋째, 객체나 배열도 ref로 감쌀 수 있지만, 이 경우 내부적으로 reactive로 변환됩니다. 이러한 특징들이 개발자가 반응형 시스템을 쉽게 다룰 수 있게 해줍니다.
코드 예제
<script setup>
import { ref, watch } from 'vue'
// 다양한 타입의 ref 생성
const count = ref(0)
const message = ref('안녕하세요')
const isLoading = ref(false)
const userList = ref([])
// ref 값 읽기와 수정
function increment() {
count.value++ // .value로 접근해서 수정
console.log('현재 카운트:', count.value)
}
// ref 변경 감지
watch(count, (newValue, oldValue) => {
console.log(`카운트가 ${oldValue}에서 ${newValue}로 변경됨`)
})
// API 호출 예시
async function fetchUsers() {
isLoading.value = true
try {
const response = await fetch('/api/users')
userList.value = await response.json()
} finally {
isLoading.value = false
}
}
</script>
설명
이것이 하는 일: ref는 전달받은 초기값을 감싸서 반응형 참조 객체를 만들고, 이 객체의 .value 속성을 통해 실제 값에 접근하고 수정할 수 있게 합니다. 첫 번째로, ref() 함수를 호출해서 각각 숫자, 문자열, 불리언, 배열 타입의 반응형 상태를 만듭니다.
이때 ref()는 각 초기값을 감싸서 { value: 초기값 } 형태의 객체로 변환합니다. 이 객체는 getter/setter를 통해 값의 변경을 추적할 수 있습니다.
그 다음으로, increment 함수에서 count.value++로 값을 증가시킵니다. .value를 통해 접근해야 하는 이유는 ref가 값을 객체로 감쌌기 때문입니다.
만약 .value 없이 count++를 하면 ref 객체 자체를 수정하려고 해서 오류가 발생합니다. 세 번째로, watch를 사용해서 count의 변경을 감지합니다.
count.value가 변경될 때마다 watch의 콜백 함수가 자동으로 실행되어 새 값과 이전 값을 받습니다. 이를 통해 값 변경에 따른 부수 효과를 처리할 수 있습니다.
네 번째로, fetchUsers 함수에서 실제 API 호출 패턴을 보여줍니다. isLoading.value로 로딩 상태를 관리하고, userList.value에 응답 데이터를 할당합니다.
try-finally를 사용해서 성공/실패 여부와 관계없이 로딩 상태가 해제되도록 합니다. 여러분이 이 코드를 사용하면 값이 변경될 때마다 자동으로 UI가 업데이트되고, watch를 통해 변경 사항에 반응할 수 있습니다.
또한 TypeScript를 사용하면 ref<number>(0)처럼 타입을 명시해서 타입 안정성을 확보할 수 있으며, 배열이나 객체도 ref로 감싸서 전체를 교체하는 방식으로 관리할 수 있습니다.
실전 팁
💡 템플릿에서는 .value 없이 바로 사용하세요. {{ count }} 형태로 자동 언래핑됩니다. 하지만 script 영역에서는 항상 .value를 붙여야 합니다.
💡 객체나 배열의 속성을 자주 수정한다면 reactive()를 사용하는 게 더 편합니다. ref는 전체를 교체할 때 유용하고, reactive는 속성을 직접 수정할 때 편합니다.
💡 unref()를 사용하면 ref든 일반 값이든 관계없이 실제 값을 얻을 수 있습니다. isRef()로 ref 객체인지 확인할 수도 있어서 유틸리티 함수를 만들 때 유용합니다.
💡 ref를 구조 분해하면 반응성을 잃습니다. const { value } = count 같은 코드는 피하세요. toRef()나 toRefs()를 사용해서 안전하게 구조 분해하세요.
💡 ref는 중첩된 객체도 깊은 반응형으로 만듭니다. shallowRef()를 사용하면 .value 레벨만 반응형으로 만들어서 성능을 최적화할 수 있습니다.
3. reactive - 객체의 반응형 상태 만들기
시작하며
여러분이 사용자 프로필 폼을 만들 때, 이름, 이메일, 나이, 주소 등 여러 필드를 관리해야 하는 경우가 있습니다. 이럴 때 각 필드마다 ref를 만들면 const name = ref(''), const email = ref(''), const age = ref(0)...
이런 식으로 코드가 길어지고, 관련된 데이터임에도 따로따로 관리되어 불편합니다. 이런 문제는 폼뿐만 아니라 설정 객체, 사용자 상태, 필터 조건 등 여러 속성을 가진 데이터를 다룰 때 자주 발생합니다.
각 속성을 개별 ref로 만들면 관리가 복잡해지고, 서버에 보낼 때도 일일이 .value를 붙여서 객체로 만들어야 해서 번거롭습니다. 바로 이럴 때 필요한 것이 reactive입니다.
reactive는 객체 전체를 반응형으로 만들어서, 객체의 모든 속성이 자동으로 반응형이 되고, .value 없이 직접 접근할 수 있게 해줍니다.
개요
간단히 말해서, reactive는 객체나 배열을 받아서 깊은 반응형 프록시 객체로 변환하는 함수입니다. Vue 3의 반응형 시스템은 JavaScript의 Proxy를 기반으로 하는데, reactive가 바로 이 Proxy를 사용해서 객체의 모든 속성 접근과 수정을 감지합니다.
예를 들어, 사용자 정보 객체, 폼 데이터, 설정 객체 등 여러 속성을 가진 데이터를 관리할 때 reactive를 사용하면 자연스러운 객체 문법으로 코드를 작성할 수 있습니다. 기존에는 여러 개의 ref를 따로 관리했다면, 이제는 하나의 reactive 객체로 묶어서 관리할 수 있습니다.
reactive의 핵심 특징은 다음과 같습니다. 첫째, .value 없이 직접 속성에 접근합니다(state.name 형태).
둘째, 중첩된 객체도 모두 자동으로 반응형이 됩니다(깊은 반응성). 셋째, 배열의 메서드(push, pop 등)도 자동으로 감지됩니다.
넷째, 구조 분해하면 반응성을 잃으므로 toRefs()와 함께 사용해야 합니다. 이러한 특징들이 객체 기반 상태 관리를 직관적이고 편리하게 만들어줍니다.
코드 예제
<script setup>
import { reactive, toRefs } from 'vue'
// 복잡한 객체를 반응형으로 만들기
const userForm = reactive({
name: '홍길동',
email: 'hong@example.com',
age: 25,
address: {
city: '서울',
district: '강남구'
},
hobbies: ['독서', '운동']
})
// .value 없이 직접 접근과 수정
function updateUser() {
userForm.name = '김철수'
userForm.address.city = '부산' // 중첩 객체도 반응형
userForm.hobbies.push('영화') // 배열 메서드도 감지됨
}
// 서버에 전송
async function submitForm() {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userForm) // 그대로 사용 가능
})
}
// 반응성을 유지하면서 구조 분해
const { name, email } = toRefs(userForm)
</script>
설명
이것이 하는 일: reactive는 일반 JavaScript 객체를 받아서 Proxy로 감싸고, 모든 속성의 읽기/쓰기를 가로채서 의존성을 추적하고 변경을 알립니다. 첫 번째로, reactive() 함수에 객체를 전달해서 userForm이라는 반응형 상태를 만듭니다.
이 객체는 이름, 이메일, 나이뿐만 아니라 중첩된 address 객체와 hobbies 배열도 포함합니다. reactive는 이 모든 것을 재귀적으로 순회하면서 Proxy로 감싸서 깊은 반응성을 제공합니다.
그 다음으로, updateUser 함수에서 객체의 속성을 직접 수정합니다. ref와 달리 .value를 붙일 필요가 없어서 일반 JavaScript 객체를 다루듯이 자연스럽게 코드를 작성할 수 있습니다.
userForm.address.city처럼 중첩된 속성도 반응형이므로 수정하면 UI가 자동으로 업데이트됩니다. 세 번째로, submitForm 함수에서 reactive 객체를 그대로 JSON.stringify()에 전달합니다.
reactive 객체는 일반 객체처럼 직렬화되므로, API 호출할 때 별도의 변환 없이 바로 사용할 수 있습니다. 이는 ref와 달리 .value를 일일이 추출할 필요가 없어서 매우 편리합니다.
마지막으로, toRefs()를 사용해서 reactive 객체를 구조 분해합니다. 그냥 const { name, email } = userForm으로 구조 분해하면 반응성을 잃지만, toRefs()를 사용하면 각 속성이 ref로 변환되어 반응성을 유지합니다.
이렇게 하면 템플릿에서 userForm.name 대신 name으로 간단하게 사용할 수 있습니다. 여러분이 이 코드를 사용하면 여러 속성을 가진 객체를 자연스럽게 관리할 수 있고, 중첩된 구조도 걱정 없이 다룰 수 있습니다.
또한 TypeScript와 함께 사용하면 interface로 타입을 정의하고 reactive<UserForm>({...})처럼 타입을 명시해서 완벽한 자동완성과 타입 체크를 받을 수 있습니다.
실전 팁
💡 reactive는 객체와 배열에만 사용하세요. 기본 타입(숫자, 문자열, 불리언)은 ref를 사용해야 합니다. reactive(0)은 작동하지 않습니다.
💡 reactive 객체 전체를 교체하면 반응성을 잃습니다. state = newState 대신 Object.assign(state, newState)를 사용하거나, ref(reactive({}))로 감싸세요.
💡 toRefs()로 구조 분해할 때는 필요한 속성만 추출하세요. 모든 속성을 추출하면 코드가 복잡해지고 성능에도 영향을 줄 수 있습니다.
💡 깊은 반응성이 필요 없고 성능이 중요하다면 shallowReactive()를 사용하세요. 첫 번째 레벨 속성만 반응형으로 만들어서 성능을 최적화합니다.
💡 reactive 객체를 props로 전달할 때는 toRaw()로 원본 객체를 얻거나, readonly()로 감싸서 자식 컴포넌트에서 수정하지 못하게 보호하세요.
4. computed - 계산된 값 만들기
시작하며
여러분이 쇼핑몰을 만들 때, 장바구니의 총 금액을 표시해야 하는 경우를 생각해보세요. 각 상품의 가격과 수량을 곱하고, 모든 상품의 금액을 더한 다음, 할인을 적용하고, 배송비를 추가해야 합니다.
이 계산을 상품이 추가되거나 수량이 변경될 때마다 수동으로 다시 해야 한다면 정말 번거롭겠죠? 이런 문제는 단순히 번거로움을 넘어서 버그의 원인이 됩니다.
개발자가 값을 업데이트하는 것을 깜빡하거나, 여러 곳에서 같은 계산을 중복해서 하다가 로직이 달라지는 경우가 생깁니다. 또한 같은 계산을 불필요하게 여러 번 실행해서 성능이 낮아질 수도 있습니다.
바로 이럴 때 필요한 것이 computed입니다. computed는 의존하는 반응형 값이 변경될 때만 자동으로 재계산되고, 결과를 캐싱해서 불필요한 계산을 방지합니다.
개요
간단히 말해서, computed는 다른 반응형 상태를 기반으로 계산된 값을 만들고, 의존성이 변경될 때만 자동으로 재계산하는 함수입니다. Vue의 computed는 게으른 평가(lazy evaluation)와 캐싱을 제공합니다.
왜냐하면 의존하는 값이 변경되지 않으면 이전 결과를 재사용하기 때문입니다. 예를 들어, 검색 필터링, 통계 계산, 데이터 변환, 유효성 검사 등 다른 상태에서 파생되는 모든 값에 computed를 사용할 수 있습니다.
기존에는 메서드로 만들어서 호출할 때마다 계산했다면, 이제는 computed로 만들어서 의존성이 변경될 때만 재계산하고 나머지는 캐시된 값을 사용합니다. computed의 핵심 특징은 다음과 같습니다.
첫째, 의존성 추적이 자동으로 이루어집니다(getter 함수 안에서 사용한 모든 반응형 값을 추적). 둘째, 의존성이 변경되지 않으면 캐시된 값을 반환합니다(성능 최적화).
셋째, 기본적으로 읽기 전용이지만 setter를 추가할 수 있습니다. 넷째, 템플릿에서 .value 없이 사용할 수 있습니다.
이러한 특징들이 파생 상태를 효율적으로 관리할 수 있게 해줍니다.
코드 예제
<script setup>
import { ref, computed } from 'vue'
// 장바구니 데이터
const cart = ref([
{ name: '노트북', price: 1200000, quantity: 1 },
{ name: '마우스', price: 30000, quantity: 2 },
{ name: '키보드', price: 80000, quantity: 1 }
])
const discountRate = ref(0.1) // 10% 할인
// 계산된 값들 - 자동으로 의존성 추적
const totalPrice = computed(() => {
return cart.value.reduce((sum, item) => {
return sum + (item.price * item.quantity)
}, 0)
})
const discountedPrice = computed(() => {
return totalPrice.value * (1 - discountRate.value)
})
const finalPrice = computed(() => {
const shipping = discountedPrice.value >= 50000 ? 0 : 3000
return discountedPrice.value + shipping
})
// getter와 setter가 있는 computed
const firstItemQuantity = computed({
get: () => cart.value[0]?.quantity || 0,
set: (val) => {
if (cart.value[0]) cart.value[0].quantity = val
}
})
</script>
설명
이것이 하는 일: computed는 getter 함수를 받아서 반응형 computed ref를 만들고, getter 내부에서 접근한 모든 반응형 값을 의존성으로 추적하여, 의존성이 변경될 때만 재실행합니다. 첫 번째로, totalPrice computed를 만듭니다.
이 함수 안에서 cart.value를 접근하므로, Vue는 자동으로 cart를 의존성으로 등록합니다. cart의 내용이 변경되면(상품 추가, 수량 변경 등) totalPrice가 자동으로 재계산되지만, cart가 변경되지 않으면 이전에 계산된 값을 재사용합니다.
그 다음으로, discountedPrice computed를 만듭니다. 이 computed는 totalPrice와 discountRate라는 두 개의 의존성을 가집니다.
주목할 점은 totalPrice 자체도 computed라는 것입니다. computed는 다른 computed를 의존성으로 가질 수 있으며, Vue가 알아서 의존성 체인을 관리합니다.
세 번째로, finalPrice computed는 discountedPrice를 기반으로 배송비를 계산합니다. 여기서 조건부 로직을 사용해서 5만원 이상이면 무료 배송을 적용합니다.
이처럼 computed 안에서 복잡한 로직과 조건문을 자유롭게 사용할 수 있습니다. 마지막으로, firstItemQuantity는 getter와 setter를 모두 가진 computed입니다.
get 함수는 값을 읽을 때 호출되고, set 함수는 값을 할당할 때 호출됩니다. 이를 통해 v-model과 같은 양방향 바인딩에서도 computed를 사용할 수 있습니다.
여러분이 이 코드를 사용하면 cart, discountRate 등의 원본 데이터만 수정하면 되고, 나머지 계산된 값들은 자동으로 업데이트됩니다. 또한 totalPrice를 여러 곳에서 참조해도 한 번만 계산되고 캐시되므로 성능이 최적화됩니다.
TypeScript를 사용하면 computed의 반환 타입이 자동으로 추론되어 타입 안정성도 확보할 수 있습니다.
실전 팁
💡 computed는 순수 함수여야 합니다. 즉, 부수 효과(API 호출, DOM 조작 등)를 포함하면 안 됩니다. 부수 효과가 필요하면 watch나 watchEffect를 사용하세요.
💡 computed 안에서 async/await를 직접 사용할 수 없습니다. 비동기 데이터가 필요하면 ref를 만들고 watchEffect에서 비동기 로직을 실행한 뒤, computed에서 그 ref를 사용하세요.
💡 computed vs 메서드: 같은 계산을 여러 번 참조한다면 computed를 사용하세요. 한 번만 호출하거나 인자를 받아야 한다면 메서드가 적합합니다.
💡 computed의 복잡도가 높아지면 성능 문제가 생길 수 있습니다. 큰 배열을 필터링하거나 정렬할 때는 결과를 별도 ref에 저장하고 필요할 때만 업데이트하는 것을 고려하세요.
💡 디버깅할 때는 computed의 onTrack과 onTrigger 옵션을 사용해서 언제 의존성이 추적되고 재계산되는지 확인할 수 있습니다.
5. watch - 반응형 값의 변경 감지하기
시작하며
여러분이 검색창을 만들 때, 사용자가 입력할 때마다 서버에 API를 호출해서 자동완성 결과를 가져와야 하는 경우가 있습니다. 또는 사용자가 특정 설정을 변경하면 로컬 스토리지에 저장하거나, 페이지 번호가 변경되면 새로운 데이터를 로드해야 하는 경우도 있죠.
이런 부수 효과(side effect)는 computed로는 처리할 수 없습니다. computed는 순수 함수여야 하고 값을 반환해야 하지만, API 호출이나 로컬 스토리지 저장 같은 작업은 값을 반환하지 않고 외부 시스템과 상호작용하기 때문입니다.
또한 비동기 작업도 처리해야 하는데, computed는 동기 함수만 지원합니다. 바로 이럴 때 필요한 것이 watch입니다.
watch는 특정 반응형 값의 변경을 감지하고, 변경될 때 부수 효과를 실행할 수 있게 해줍니다.
개요
간단히 말해서, watch는 하나 이상의 반응형 값을 감시하고, 값이 변경될 때 콜백 함수를 실행하는 함수입니다. Vue의 watch는 명시적인 의존성 추적과 부수 효과 실행을 위한 도구입니다.
computed가 "이 값이 변경되면 새로운 값을 계산한다"라면, watch는 "이 값이 변경되면 이런 작업을 실행한다"입니다. 예를 들어, 검색어 변경 시 API 호출, 라우트 변경 시 데이터 리셋, 설정 변경 시 로컬 스토리지 저장, 폼 값 변경 시 유효성 검사 등에 watch를 사용합니다.
기존 Vue 2의 watch 옵션과 유사하지만, Composition API에서는 함수로 제공되어 더 유연하게 사용할 수 있습니다. watch의 핵심 특징은 다음과 같습니다.
첫째, 이전 값과 새 값을 모두 받을 수 있습니다. 둘째, 여러 값을 동시에 감시할 수 있습니다(배열로 전달).
셋째, immediate 옵션으로 즉시 실행할 수 있습니다. 넷째, deep 옵션으로 깊은 감시를 할 수 있습니다.
다섯째, 비동기 작업을 처리할 수 있습니다. 이러한 특징들이 복잡한 부수 효과를 안전하게 관리할 수 있게 해줍니다.
코드 예제
<script setup>
import { ref, watch } from 'vue'
const searchQuery = ref('')
const userId = ref(null)
const settings = ref({ theme: 'light', language: 'ko' })
// 단일 값 감시 - 검색어 변경 시 API 호출
watch(searchQuery, async (newQuery, oldQuery) => {
if (newQuery.length < 2) return
console.log(`검색어가 "${oldQuery}"에서 "${newQuery}"로 변경됨`)
try {
const response = await fetch(`/api/search?q=${newQuery}`)
const results = await response.json()
console.log('검색 결과:', results)
} catch (error) {
console.error('검색 실패:', error)
}
})
// 여러 값 동시 감시
watch([userId, () => settings.value.language], ([newId, newLang], [oldId, oldLang]) => {
console.log('사용자 또는 언어가 변경됨')
// 사용자 데이터 다시 로드
})
// 객체 깊은 감시 + 즉시 실행
watch(settings, (newSettings) => {
localStorage.setItem('settings', JSON.stringify(newSettings))
}, { deep: true, immediate: true })
// watch 중지하기
const stopWatch = watch(searchQuery, () => {})
// 나중에 stopWatch()를 호출하면 감시 중지
</script>
설명
이것이 하는 일: watch는 첫 번째 인자로 받은 반응형 소스를 감시하다가 값이 변경되면, 두 번째 인자로 받은 콜백 함수를 새 값과 이전 값을 전달하면서 실행합니다. 첫 번째로, searchQuery를 감시하는 watch를 만듭니다.
콜백 함수는 async로 선언되어 있어서 비동기 API 호출을 처리할 수 있습니다. newQuery가 2글자 미만이면 조기 반환해서 불필요한 API 호출을 방지합니다.
이처럼 watch 안에서 조건부 로직과 비동기 처리를 자유롭게 할 수 있습니다. 그 다음으로, 여러 값을 동시에 감시하는 예제입니다.
배열의 첫 번째 요소는 ref인 userId이고, 두 번째 요소는 getter 함수입니다. getter 함수를 사용하면 객체의 특정 속성만 감시할 수 있습니다.
콜백은 각각 새 값들의 배열과 이전 값들의 배열을 받습니다. 세 번째로, settings 객체를 깊은 감시(deep: true)합니다.
deep 옵션을 사용하면 객체의 중첩된 속성이 변경되어도 감지합니다. immediate: true 옵션으로 컴포넌트가 마운트될 때 즉시 한 번 실행되어, 초기 설정을 로컬 스토리지에 저장합니다.
마지막으로, watch는 중지 함수를 반환합니다. 이 함수를 호출하면 감시가 중지되어, 더 이상 콜백이 실행되지 않습니다.
이는 메모리 누수를 방지하고, 컴포넌트가 언마운트될 때 정리 작업을 할 때 유용합니다. 여러분이 이 코드를 사용하면 반응형 상태 변경에 따른 부수 효과를 안전하게 관리할 수 있습니다.
API 호출, 로컬 스토리지, 로깅 등 외부 시스템과의 상호작용을 선언적으로 처리할 수 있으며, 비동기 작업도 async/await를 사용해서 깔끔하게 작성할 수 있습니다. 또한 flush: 'post' 옵션을 사용하면 DOM 업데이트 이후에 콜백을 실행할 수도 있습니다.
실전 팁
💡 ref 객체를 직접 전달하면 .value는 자동으로 처리됩니다. 하지만 reactive 객체의 속성을 감시하려면 getter 함수로 감싸야 합니다: watch(() => state.count, ...)
💡 deep 옵션은 성능 비용이 큽니다. 큰 객체에 deep을 사용하면 모든 중첩 속성을 순회하므로 느려질 수 있습니다. 특정 속성만 감시하는 것을 고려하세요.
💡 watch 콜백 안에서 감시 중인 값을 수정하면 무한 루프가 발생할 수 있습니다. 조건문으로 보호하거나, watchEffect 대신 사용을 고려하세요.
💡 컴포넌트가 언마운트되면 watch는 자동으로 중지됩니다. 하지만 비동기 작업 중에 언마운트되면 문제가 생길 수 있으므로, onCleanup 콜백을 사용해서 정리 작업을 하세요.
💡 디바운싱이 필요하면 lodash의 debounce나 setTimeout을 사용하세요. 예: watch(searchQuery, debounce(async (val) => { /* API 호출 */ }, 300))
6. watchEffect - 자동 의존성 추적과 부수 효과
시작하며
여러분이 여러 개의 반응형 값에 의존하는 부수 효과를 만들 때, watch를 사용하면 각 값을 일일이 나열해야 해서 번거로운 경우가 있습니다. 예를 들어, 사용자 ID, 페이지 번호, 필터 조건이 변경될 때마다 데이터를 다시 로드해야 한다면, watch([userId, page, filter], ...)처럼 모든 의존성을 배열로 나열해야 합니다.
이런 방식은 코드가 길어질 뿐만 아니라, 나중에 의존성을 추가하거나 제거할 때 배열을 수정하는 것을 깜빡할 수 있어서 버그의 원인이 됩니다. 특히 의존성이 5개, 10개로 늘어나면 관리가 정말 어려워집니다.
바로 이럴 때 필요한 것이 watchEffect입니다. watchEffect는 함수 안에서 사용된 모든 반응형 값을 자동으로 추적하므로, 의존성을 명시적으로 나열할 필요가 없습니다.
개요
간단히 말해서, watchEffect는 전달받은 함수를 즉시 실행하고, 함수 안에서 접근한 모든 반응형 값을 자동으로 추적하여, 그 값들이 변경될 때마다 함수를 다시 실행하는 함수입니다. watchEffect는 computed와 watch의 중간 성격을 가집니다.
computed처럼 자동 의존성 추적을 하지만, 값을 반환하는 대신 부수 효과를 실행합니다. watch처럼 부수 효과를 실행하지만, 의존성을 명시할 필요가 없습니다.
예를 들어, 로깅, 디버깅, 여러 값에 의존하는 API 호출, 자동 저장 기능 등에 watchEffect를 사용하면 편리합니다. 기존 watch와 달리 의존성을 자동으로 추적하고 즉시 실행된다는 점이 다릅니다.
watchEffect의 핵심 특징은 다음과 같습니다. 첫째, 의존성을 자동으로 추적합니다(함수 내부에서 접근한 모든 반응형 값).
둘째, 컴포넌트 마운트 시 즉시 실행됩니다. 셋째, 이전 값과 새 값을 구분하지 않습니다(현재 상태만 관심).
넷째, onCleanup 함수로 정리 작업을 할 수 있습니다. 다섯째, 비동기 작업을 처리할 수 있습니다.
이러한 특징들이 선언적이고 간결한 부수 효과 관리를 가능하게 합니다.
코드 예제
<script setup>
import { ref, watchEffect } from 'vue'
const userId = ref(1)
const page = ref(1)
const searchQuery = ref('')
const data = ref([])
const isLoading = ref(false)
// 자동 의존성 추적 - userId, page, searchQuery 중 하나라도 변경되면 실행
watchEffect(async (onCleanup) => {
// 로딩 시작
isLoading.value = true
// 이전 요청 취소를 위한 AbortController
const controller = new AbortController()
// 컴포넌트 언마운트 또는 재실행 시 정리 작업
onCleanup(() => {
controller.abort()
console.log('이전 요청 취소됨')
})
try {
// 이 함수 안에서 접근한 모든 ref가 자동으로 의존성이 됨
const response = await fetch(
`/api/users/${userId.value}/posts?page=${page.value}&q=${searchQuery.value}`,
{ signal: controller.signal }
)
data.value = await response.json()
} catch (error) {
if (error.name !== 'AbortError') {
console.error('데이터 로드 실패:', error)
}
} finally {
isLoading.value = false
}
})
// 디버깅용 watchEffect
watchEffect(() => {
console.log('현재 상태:', {
userId: userId.value,
page: page.value,
dataLength: data.value.length
})
})
</script>
설명
이것이 하는 일: watchEffect는 전달받은 함수를 즉시 실행하면서 접근한 모든 반응형 값을 기록하고, 나중에 그 값들 중 하나라도 변경되면 함수를 다시 실행합니다. 첫 번째로, watchEffect가 첫 실행될 때 함수 안의 코드가 실행되면서 userId.value, page.value, searchQuery.value에 접근합니다.
Vue는 이 접근들을 감지하고 자동으로 의존성으로 등록합니다. 이후 이 세 값 중 하나라도 변경되면 함수가 다시 실행됩니다.
그 다음으로, onCleanup 콜백을 등록합니다. 이 콜백은 watchEffect가 재실행되기 직전이나 컴포넌트가 언마운트될 때 호출됩니다.
여기서는 AbortController를 사용해서 이전 API 요청을 취소합니다. 예를 들어, 사용자가 페이지를 빠르게 여러 번 바꾸면 이전 요청들이 모두 취소되어 최신 요청만 처리됩니다.
세 번째로, fetch API를 호출할 때 signal 옵션을 전달합니다. 이렇게 하면 controller.abort()가 호출될 때 요청이 취소됩니다.
try-catch에서 AbortError는 정상적인 취소이므로 무시하고, 다른 에러만 로깅합니다. 네 번째로, 두 번째 watchEffect 예제는 디버깅용입니다.
콘솔에 현재 상태를 출력하므로, 개발 중에 값의 변화를 실시간으로 확인할 수 있습니다. 이처럼 watchEffect는 간단한 로깅과 디버깅에도 매우 유용합니다.
여러분이 이 코드를 사용하면 의존성을 수동으로 관리할 필요 없이, 자연스럽게 코드를 작성하면 Vue가 알아서 추적해줍니다. API 요청 취소 같은 복잡한 정리 작업도 onCleanup으로 깔끔하게 처리할 수 있으며, 여러 조건에 의존하는 로직을 간결하게 작성할 수 있습니다.
실전 팁
💡 watchEffect vs watch: 의존성이 명확하고 이전 값이 필요하면 watch, 여러 값에 의존하고 현재 상태만 필요하면 watchEffect를 사용하세요.
💡 watchEffect는 동기 실행 중에 접근한 값만 추적합니다. await 이후에 접근한 값은 추적되지 않으므로, 필요한 값은 await 전에 미리 읽어두세요.
💡 무한 루프를 조심하세요. watchEffect 안에서 반응형 값을 수정하고, 그 값이 다시 watchEffect를 트리거하면 무한 루프가 됩니다. 조건문으로 보호하세요.
💡 flush: 'post' 옵션을 사용하면 DOM 업데이트 이후에 실행됩니다. DOM을 읽어야 하는 경우에 유용합니다. flush: 'sync'는 동기적으로 실행되지만 성능 문제가 있을 수 있습니다.
💡 개발 중에는 watchEffect로 빠르게 프로토타이핑하고, 나중에 성능이 중요해지면 watch로 리팩토링하는 것도 좋은 전략입니다.
7. toRefs와 toRef - 반응성 유지하면서 구조 분해하기
시작하며
여러분이 reactive로 만든 상태 객체를 템플릿에서 사용할 때, state.name, state.email처럼 매번 state.을 붙이는 게 번거롭다고 느낀 적 있으신가요? 그래서 const { name, email } = state처럼 구조 분해를 시도했다가, name과 email이 반응성을 잃어버려서 UI가 업데이트되지 않는 문제를 겪었을 겁니다.
이런 문제는 JavaScript의 구조 분해가 값을 복사하기 때문에 발생합니다. reactive 객체에서 속성을 구조 분해하면, 원본 객체와의 연결이 끊어져서 더 이상 반응형이 아닌 일반 값이 됩니다.
특히 props를 구조 분해할 때 이 문제가 자주 발생합니다. 바로 이럴 때 필요한 것이 toRefs와 toRef입니다.
이 함수들은 반응형 객체의 속성을 ref로 변환해서, 구조 분해 이후에도 반응성을 유지할 수 있게 해줍니다.
개요
간단히 말해서, toRefs는 reactive 객체의 모든 속성을 개별 ref로 변환하고, toRef는 특정 속성 하나만 ref로 변환하는 함수입니다. 이 함수들은 구조 분해와 반응성을 동시에 가능하게 합니다.
왜냐하면 각 속성을 ref로 감싸서, 원본 객체와의 연결을 유지하면서도 독립적인 변수처럼 사용할 수 있기 때문입니다. 예를 들어, composable 함수에서 여러 값을 반환할 때, props를 구조 분해할 때, 템플릿에서 간결하게 사용하고 싶을 때 toRefs를 활용합니다.
기존에는 반응성을 유지하려면 객체 전체를 사용해야 했다면, 이제는 toRefs로 구조 분해하면서도 반응성을 유지할 수 있습니다. 핵심 특징은 다음과 같습니다.
첫째, 원본 객체와 양방향 동기화됩니다(ref를 수정하면 원본도 변경). 둘째, toRefs는 모든 속성을, toRef는 특정 속성만 변환합니다.
셋째, 존재하지 않는 속성에도 toRef를 사용할 수 있습니다(기본값 설정 가능). 넷째, props를 구조 분해할 때 특히 유용합니다.
이러한 특징들이 반응성을 잃지 않으면서도 편리한 코드를 작성할 수 있게 해줍니다.
코드 예제
<script setup>
import { reactive, toRefs, toRef, computed } from 'vue'
// reactive 객체 생성
const userState = reactive({
name: '홍길동',
email: 'hong@example.com',
age: 25,
isActive: true
})
// toRefs로 모든 속성을 ref로 변환
const { name, email, age, isActive } = toRefs(userState)
// 이제 각 변수가 ref이므로 반응성 유지
function updateName() {
name.value = '김철수' // userState.name도 함께 변경됨
console.log(userState.name) // '김철수' 출력
}
// toRef로 특정 속성만 ref로 변환
const userAge = toRef(userState, 'age')
userAge.value = 26 // userState.age도 26으로 변경
// 존재하지 않는 속성에도 사용 가능 (Vue 3.3+)
const nickname = toRef(userState, 'nickname', '별명없음')
// Composable에서 활용하는 패턴
function useUser() {
const state = reactive({
user: null,
loading: false,
error: null
})
async function fetchUser(id) {
state.loading = true
try {
const response = await fetch(`/api/users/${id}`)
state.user = await response.json()
} catch (e) {
state.error = e
} finally {
state.loading = false
}
}
// toRefs로 반환하면 구조 분해 가능
return {
...toRefs(state),
fetchUser
}
}
// 사용 시 구조 분해하면서 반응성 유지
const { user, loading, error, fetchUser } = useUser()
</script>
설명
이것이 하는 일: toRefs와 toRef는 reactive 객체의 속성에 대한 ref 참조를 만들어서, 원본 객체와 양방향으로 동기화되는 독립적인 ref를 제공합니다. 첫 번째로, toRefs(userState)를 호출하면 userState의 각 속성(name, email, age, isActive)에 대해 개별 ref가 생성됩니다.
이 ref들은 원본 객체의 속성을 가리키는 포인터 같은 역할을 합니다. 따라서 구조 분해 이후에도 name.value를 수정하면 userState.name이 함께 변경됩니다.
그 다음으로, updateName 함수에서 name.value를 수정합니다. 이 변경은 즉시 userState.name에 반영되며, userState를 참조하는 다른 코드나 computed, watch도 모두 이 변경을 감지합니다.
이는 toRefs가 새로운 값을 복사하는 게 아니라, 원본을 참조하기 때문입니다. 세 번째로, toRef를 사용해서 age 속성 하나만 ref로 변환합니다.
전체 객체가 아니라 특정 속성만 필요할 때 toRef를 사용하면 더 명시적이고 효율적입니다. userAge.value = 26으로 수정하면 userState.age도 26이 됩니다.
네 번째로, useUser라는 composable 함수를 만듭니다. 이 함수는 내부에서 reactive로 상태를 관리하고, 반환할 때 toRefs를 사용합니다.
이렇게 하면 사용하는 쪽에서 구조 분해를 하면서도 모든 값의 반응성이 유지됩니다. 이는 composable 패턴의 표준적인 방법입니다.
여러분이 이 코드를 사용하면 reactive 객체의 편리함(여러 속성을 묶어서 관리)과 ref의 편리함(구조 분해, 독립적 사용)을 동시에 얻을 수 있습니다. 특히 composable 함수를 만들 때 toRefs를 사용하면, 사용자가 필요한 값만 선택적으로 구조 분해할 수 있어서 매우 유연합니다.
실전 팁
💡 props는 이미 반응형이므로 toRefs(props)로 변환해서 구조 분해하세요. 단순 구조 분해는 반응성을 잃습니다: const { name } = props (나쁨) vs const { name } = toRefs(props) (좋음)
💡 toRefs는 현재 존재하는 속성만 변환합니다. 나중에 추가된 속성은 ref가 아닙니다. 동적 속성에는 toRef를 개별적으로 사용하세요.
💡 성능이 중요한 경우, 필요한 속성만 toRef로 변환하세요. toRefs는 모든 속성을 순회하므로 큰 객체에서는 비용이 클 수 있습니다.
💡 computed를 toRefs와 함께 반환하려면 { ...toRefs(state), myComputed: computed(() => ...) } 형태로 사용하세요. computed는 이미 ref이므로 toRefs에 포함시킬 필요가 없습니다.
💡 TypeScript를 사용할 때 toRefs의 반환 타입은 자동으로 추론되지만, 명시적으로 타입을 지정하고 싶다면 ToRefs<typeof state> 타입을 사용할 수 있습니다.
8. provide와 inject - 컴포넌트 간 의존성 주입
시작하며
여러분이 부모 컴포넌트에서 손자 컴포넌트, 또는 그보다 더 깊은 자식 컴포넌트에게 데이터를 전달해야 할 때를 생각해보세요. props를 사용하면 중간의 모든 컴포넌트를 거쳐야 해서, 실제로는 사용하지 않는 컴포넌트에도 props를 선언하고 전달해야 하는 번거로움이 있습니다.
이를 "prop drilling"이라고 부릅니다. 이런 문제는 컴포넌트 계층이 깊어질수록 심각해집니다.
5단계 깊이의 컴포넌트에 데이터를 전달하려면 중간의 4개 컴포넌트 모두에 props를 추가해야 하고, 나중에 이름을 바꾸거나 타입을 변경할 때도 모든 컴포넌트를 수정해야 합니다. 바로 이럴 때 필요한 것이 provide와 inject입니다.
이 API는 부모 컴포넌트에서 제공한 값을 자손 컴포넌트가 깊이와 관계없이 직접 주입받을 수 있게 해서, prop drilling을 해결합니다.
개요
간단히 말해서, provide는 상위 컴포넌트에서 값을 제공하고, inject는 하위 컴포넌트에서 그 값을 주입받는 함수입니다. Vue의 provide/inject는 React의 Context API와 유사한 의존성 주입(Dependency Injection) 메커니즘입니다.
왜냐하면 컴포넌트 트리의 어느 레벨에서든 제공된 값을 직접 접근할 수 있기 때문입니다. 예를 들어, 테마 설정, 사용자 인증 정보, 다국어 번역 함수, 전역 설정 등을 provide/inject로 관리하면 편리합니다.
기존에는 props를 계층마다 전달하거나, Vuex 같은 상태 관리 라이브러리를 사용해야 했다면, 이제는 provide/inject로 간단하게 해결할 수 있습니다. 핵심 특징은 다음과 같습니다.
첫째, 중간 컴포넌트를 거치지 않고 직접 전달합니다. 둘째, 반응형 값을 제공하면 자동으로 업데이트됩니다.
셋째, Symbol을 키로 사용해서 충돌을 방지할 수 있습니다. 넷째, 기본값을 설정할 수 있습니다.
다섯째, readonly로 감싸서 읽기 전용으로 만들 수 있습니다. 이러한 특징들이 유연하고 안전한 컴포넌트 간 통신을 가능하게 합니다.
코드 예제
<script setup>
import { ref, provide, inject, readonly } from 'vue'
// ===== 부모 컴포넌트 (App.vue) =====
// Symbol로 고유 키 생성 (충돌 방지)
const themeKey = Symbol('theme')
const userKey = Symbol('user')
const theme = ref('light')
const currentUser = ref({ name: '홍길동', role: 'admin' })
// 값 제공 - readonly로 감싸서 자식이 수정 못하게 보호
provide(themeKey, readonly(theme))
provide(userKey, readonly(currentUser))
// 수정 함수도 함께 제공
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
// ===== 손자 컴포넌트 (깊은 곳의 컴포넌트) =====
// inject로 값 주입 - 타입 안정성을 위해 기본값 제공
const injectedTheme = inject(themeKey, ref('light'))
const injectedUser = inject(userKey, ref({ name: '게스트', role: 'guest' }))
const updateTheme = inject('updateTheme')
// 주입받은 값 사용
function toggleTheme() {
const newTheme = injectedTheme.value === 'light' ? 'dark' : 'light'
updateTheme?.(newTheme) // optional chaining으로 안전하게 호출
}
// ===== Composable에서 활용 =====
// useTheme.js
export const THEME_KEY = Symbol('theme')
export function provideTheme() {
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
provide(THEME_KEY, { theme: readonly(theme), toggleTheme })
return { theme, toggleTheme }
}
export function useTheme() {
const themeContext = inject(THEME_KEY)
if (!themeContext) {
throw new Error('useTheme must be used with provideTheme')
}
return themeContext
}
</script>
설명
이것이 하는 일: provide는 키와 값을 받아서 현재 컴포넌트의 provide 컨텍스트에 등록하고, inject는 키를 받아서 상위 컴포넌트 체인에서 해당 키의 값을 찾아서 반환합니다. 첫 번째로, Symbol을 사용해서 고유한 키를 만듭니다.
문자열 키도 가능하지만, Symbol을 사용하면 다른 라이브러리나 플러그인과 키가 충돌할 위험이 없습니다. theme과 currentUser라는 반응형 값을 만들고, readonly로 감싸서 제공합니다.
이렇게 하면 자식 컴포넌트에서 직접 수정할 수 없어서 단방향 데이터 흐름이 유지됩니다. 그 다음으로, updateTheme이라는 함수도 함께 제공합니다.
이는 자식 컴포넌트가 값을 변경할 수 있는 명시적인 인터페이스를 제공하는 패턴입니다. 값 자체는 readonly지만, 함수를 통해서만 수정할 수 있게 하여 제어권을 부모가 유지합니다.
세 번째로, 손자 컴포넌트에서 inject로 값을 주입받습니다. 두 번째 인자로 기본값을 제공하면, 상위 컴포넌트에서 provide하지 않았을 때 사용됩니다.
이는 컴포넌트를 독립적으로 테스트하거나, 선택적으로 provide를 사용할 때 유용합니다. 네 번째로, composable 패턴을 보여줍니다.
provideTheme과 useTheme을 별도 파일로 분리하면, 여러 컴포넌트에서 일관된 방식으로 테마를 사용할 수 있습니다. useTheme 안에서 에러를 던지는 것은, provide 없이 inject를 사용하는 실수를 방지합니다.
여러분이 이 코드를 사용하면 깊은 컴포넌트 계층에서도 깔끔하게 데이터를 공유할 수 있고, 전역 상태 관리 없이도 필요한 부분만 의존성을 주입할 수 있습니다. TypeScript를 사용하면 InjectionKey<T> 타입으로 키의 타입을 명시해서 완벽한 타입 안정성을 확보할 수 있습니다.
실전 팁
💡 provide/inject는 앱 전역 상태가 아닙니다. 제공한 컴포넌트의 하위 트리에서만 사용 가능하므로, 여러 독립적인 부분에서 같은 키로 다른 값을 제공할 수 있습니다.
💡 반응형 값을 provide할 때는 ref나 reactive를 그대로 전달하세요. 일반 값(.value)을 전달하면 반응성을 잃습니다. provide('count', count.value) (나쁨) vs provide('count', count) (좋음)
💡 대규모 앱에서는 모든 injection key를 별도 파일에 모아서 관리하세요. 예: keys.ts에서 export const THEME_KEY = Symbol()처럼 정의하면 재사용과 관리가 쉽습니다.
💡 provide/inject vs Pinia: 앱 전역 상태는 Pinia를, 컴포넌트 트리 일부의 로컬 상태는 provide/inject를 사용하세요. 모든 곳에서 접근해야 하면 Pinia가 적합합니다.
💡 app.provide()를 사용하면 앱 레벨에서 전역으로 제공할 수 있습니다. 플러그인이나 라이브러리를 만들 때 유용합니다: app.provide('api', apiClient)
댓글 (0)
함께 보면 좋은 카드 뉴스
서비스 메시 완벽 가이드
마이크로서비스 간 통신을 안전하고 효율적으로 관리하는 서비스 메시의 핵심 개념부터 실전 도입까지, 초급 개발자를 위한 완벽한 입문서입니다. Istio와 Linkerd 비교, 사이드카 패턴, 실무 적용 노하우를 담았습니다.
EFK 스택 로깅 완벽 가이드
마이크로서비스 환경에서 로그를 효과적으로 수집하고 분석하는 EFK 스택(Elasticsearch, Fluentd, Kibana)의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 정리한 가이드입니다.
Grafana 대시보드 완벽 가이드
실시간 모니터링의 핵심, Grafana 대시보드를 처음부터 끝까지 배워봅니다. Prometheus 연동부터 알람 설정까지, 초급 개발자도 쉽게 따라할 수 있는 실전 가이드입니다.
분산 추적 완벽 가이드
마이크로서비스 환경에서 요청의 전체 흐름을 추적하는 분산 추적 시스템의 핵심 개념을 배웁니다. Trace, Span, Trace ID 전파, 샘플링 전략까지 실무에 필요한 모든 것을 다룹니다.
CloudFront CDN 완벽 가이드
AWS CloudFront를 활용한 콘텐츠 배포 최적화 방법을 실무 관점에서 다룹니다. 배포 생성부터 캐시 설정, HTTPS 적용까지 단계별로 알아봅니다.