Vue 3 Computed 완벽 정리
computed란
computed는 반응형 데이터를 기반으로 자동 계산되는 값이다. 의존하는 데이터가 변경될 때만 재계산되고, 변경되지 않으면 캐싱된 값을 반환한다.
import { ref, computed } from 'vue'
const price = ref(10000)
const quantity = ref(3)
const total = computed(() => price.value * quantity.value)
console.log(total.value) // 30000
computed vs method
// computed — 캐싱됨
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// method — 호출할 때마다 실행
function getFullName() {
return `${firstName.value} ${lastName.value}`
}
computedmethod캐싱의존 값 변경 시에만 재계산호출할 때마다 실행사용fullName.valuegetFullName()템플릿{{ fullName }}{{ getFullName() }}용도파생 데이터이벤트 핸들러, 액션
const list = ref([1, 2, 3, 4, 5])
// computed: list가 바뀔 때만 재계산
const evenNumbers = computed(() => list.value.filter(n => n % 2 === 0))
// method: 템플릿이 렌더링될 때마다 실행
function getEvenNumbers() {
return list.value.filter(n => n % 2 === 0)
}
읽기 전용 computed
기본 형태. getter만 있다.
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
doubleCount.value = 10 // 경고: computed는 읽기 전용
읽기/쓰기 computed
getter와 setter를 모두 정의할 수 있다.
const firstName = ref('길동')
const lastName = ref('홍')
const fullName = computed({
get() {
return `${lastName.value}${firstName.value}`
},
set(newValue: string) {
lastName.value = newValue.charAt(0)
firstName.value = newValue.slice(1)
}
})
console.log(fullName.value) // "홍길동"
fullName.value = '김철수'
console.log(lastName.value) // "김"
console.log(firstName.value) // "철수"
실무 패턴
1. 목록 필터링/정렬
const products = ref([
{ name: '노트북', price: 1500000, category: '전자기기' },
{ name: '마우스', price: 35000, category: '전자기기' },
{ name: '책상', price: 250000, category: '가구' },
])
const searchQuery = ref('')
const selectedCategory = ref('')
const sortBy = ref('name')
const filteredProducts = computed(() => {
let result = products.value
if (selectedCategory.value) {
result = result.filter(p => p.category === selectedCategory.value)
}
if (searchQuery.value) {
const q = searchQuery.value.toLowerCase()
result = result.filter(p => p.name.toLowerCase().includes(q))
}
if (sortBy.value === 'price') {
result = [...result].sort((a, b) => a.price - b.price)
} else {
result = [...result].sort((a, b) => a.name.localeCompare(b.name))
}
return result
})
2. 폼 유효성 검사
const email = ref('')
const password = ref('')
const isEmailValid = computed(() =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)
)
const isPasswordValid = computed(() =>
password.value.length >= 8
)
const canSubmit = computed(() =>
isEmailValid.value && isPasswordValid.value
)
<button :disabled="!canSubmit">가입</button>
3. 포맷팅
const price = ref(1500000)
const formattedPrice = computed(() =>
price.value.toLocaleString('ko-KR') + '원'
)
// "1,500,000원"
const fileSize = ref(2548576)
const formattedSize = computed(() => {
const kb = fileSize.value / 1024
if (kb < 1024) return `${kb.toFixed(1)} KB`
return `${(kb / 1024).toFixed(1)} MB`
})
// "2.4 MB"
4. 상태 파생
const cart = ref([
{ name: '상품A', price: 10000, qty: 2 },
{ name: '상품B', price: 5000, qty: 1 },
])
const itemCount = computed(() =>
cart.value.reduce((sum, item) => sum + item.qty, 0)
)
const totalPrice = computed(() =>
cart.value.reduce((sum, item) => sum + item.price * item.qty, 0)
)
const isEmpty = computed(() => cart.value.length === 0)
5. computed 체이닝
computed가 다른 computed를 참조할 수 있다.
const items = ref([1, 2, 3, 4, 5, 6])
const evenItems = computed(() =>
items.value.filter(n => n % 2 === 0)
)
const evenSum = computed(() =>
evenItems.value.reduce((a, b) => a + b, 0)
)
// items 변경 → evenItems 재계산 → evenSum 재계산
6. v-model과 함께 (Getter/Setter)
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
const inputValue = computed({
get() { return props.modelValue },
set(val: string) { emit('update:modelValue', val) }
})
<input v-model="inputValue" />
주의사항
1. 사이드 이펙트를 넣지 않는다
// 잘못된 사용
const result = computed(() => {
fetchData() // API 호출 금지
console.log('계산') // 로그 금지
otherRef.value = 10 // 다른 반응형 값 변경 금지
return someValue.value * 2
})
// 사이드 이펙트는 watch를 사용
watch(someValue, (val) => {
fetchData()
console.log('변경됨:', val)
})
2. 비동기 로직을 넣지 않는다
// 잘못된 사용
const data = computed(async () => {
const res = await fetch('/api/data')
return res.json()
})
// 비동기는 watch나 함수로 처리
const data = ref(null)
watch(id, async (newId) => {
data.value = await fetch(`/api/data/${newId}`).then(r => r.json())
}, { immediate: true })
3. computed 안에서 원본 배열을 변경하지 않는다
// 잘못된 사용 — 원본 배열을 직접 정렬
const sorted = computed(() => items.value.sort())
// 올바른 사용 — 복사 후 정렬
const sorted = computed(() => [...items.value].sort())
4. 불필요한 computed를 만들지 않는다
// 불필요 — ref를 그대로 감싸기만 함
const userName = computed(() => user.value.name)
// 그냥 직접 접근
// template: {{ user.name }}
computed vs watch
computedwatch목적값을 계산변화에 반응반환새 값없음 (사이드 이펙트 실행)캐싱있음없음비동기불가가능사용 예필터링, 포맷팅, 유효성API 호출, 로그, DOM 조작
// computed: "이 값은 무엇인가?"
const fullName = computed(() => `${first.value} ${last.value}`)
// watch: "이 값이 바뀌면 무엇을 할 것인가?"
watch(searchQuery, (query) => {
fetchResults(query)
})
정리
computed는 반응형 데이터에서 파생된 값을 만들 때 사용- 의존 데이터가 변경되지 않으면 캐싱된 값을 반환하여 성능에 유리
- getter만 있으면 읽기 전용, getter/setter 모두 정의하면 양방향 바인딩 가능
- 사이드 이펙트, 비동기, 원본 변경은 피한다
- "값을 계산"하려면
computed, "변화에 반응"하려면watch