watch와 watchEffect
watch는 "특정 데이터가 변할 때 사이드 이펙트를 실행"하는 도구입니다. computed가 파생 데이터를 만든다면, watch는 데이터 변경에 반응하는 로직을 처리합니다.
면접에서 "watch와 watchEffect의 차이"를 물어보면, 의존성 추적 방식 의 차이를 설명할 수 있어야 합니다.
watch — 명시적 감시
<script setup lang="ts">
import { ref, watch } from 'vue'
const count = ref(0)
const name = ref('심정훈')
// 기본 사용 — ref 감시
watch(count, (newValue, oldValue) => {
console.log(`count: ${oldValue} → ${newValue}`)
})
// getter 함수로 감시
watch(
() => name.value.length,
(newLen, oldLen) => {
console.log(`이름 길이: ${oldLen} → ${newLen}`)
}
)
// 여러 소스 동시 감시
watch(
[count, name],
([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} → ${newCount}`)
console.log(`name: ${oldName} → ${newName}`)
}
)
</script>
watchEffect — 자동 의존성 추적
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
const searchQuery = ref('')
const page = ref(1)
const results = ref([])
// 콜백 내부에서 사용된 반응형 데이터를 자동으로 추적
watchEffect(async () => {
// searchQuery 또는 page가 변경되면 자동 재실행
const response = await fetch(
`/api/search?q=${searchQuery.value}&page=${page.value}`
)
results.value = await response.json()
})
</script>
watch vs watchEffect 비교
| 항목 | watch | watchEffect |
|---|---|---|
| 의존성 | 명시적 지정 | 자동 추적 |
| 이전 값 | (newVal, oldVal) 접근 가능 | 접근 불가 |
| 즉시 실행 | 기본 lazy (옵션으로 immediate) | 즉시 실행 |
| 정밀 제어 | 어떤 값을 감시할지 선택 | 사용된 모든 값 추적 |
| 사용 시나리오 | 특정 값 변경 시 로직 | 여러 값에 의존하는 사이드 이펙트 |
watch 옵션들
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
const user = ref({ name: '심정훈', address: { city: '서울' } })
const items = reactive([1, 2, 3])
// immediate — 생성 시 즉시 한 번 실행
watch(
() => user.value.name,
(newName) => {
console.log('이름:', newName)
},
{ immediate: true }
)
// deep — 깊은 감시 (중첩 객체 변경도 감지)
watch(
user,
(newUser) => {
console.log('user 변경:', newUser)
},
{ deep: true }
)
// once — 한 번만 실행 후 자동 정지 (Vue 3.4+)
watch(
() => user.value.name,
(newName) => {
console.log('첫 변경:', newName)
},
{ once: true }
)
// flush — 콜백 실행 타이밍
watch(count, handler, {
flush: 'post' // DOM 업데이트 후 (기본값)
// flush: 'pre' // DOM 업데이트 전
// flush: 'sync' // 동기적 즉시 실행 (성능 주의)
})
</script>
감시 중지
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue'
const count = ref(0)
// watch/watchEffect는 stop 함수를 반환
const stopWatch = watch(count, (newVal) => {
console.log('count:', newVal)
if (newVal >= 10) {
stopWatch() // 조건부 중지
}
})
const stopEffect = watchEffect(() => {
console.log('count:', count.value)
})
// 필요 시 수동 중지
// stopEffect()
</script>
클린업 함수 (onCleanup)
이전 사이드 이펙트를 정리하는 패턴입니다. 디바운싱, API 요청 취소 등에 유용합니다.
<script setup lang="ts">
import { ref, watch } from 'vue'
const searchQuery = ref('')
// API 요청 취소 패턴
watch(searchQuery, async (newQuery, oldQuery, onCleanup) => {
const controller = new AbortController()
// 다음 실행 시 이전 요청을 취소
onCleanup(() => {
controller.abort()
})
try {
const response = await fetch(`/api/search?q=${newQuery}`, {
signal: controller.signal
})
const data = await response.json()
console.log('결과:', data)
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
console.log('이전 요청 취소됨')
}
}
})
// 디바운스 패턴
watch(searchQuery, (newQuery, oldQuery, onCleanup) => {
const timerId = setTimeout(() => {
// 디바운스된 검색 실행
console.log('검색:', newQuery)
}, 300)
onCleanup(() => {
clearTimeout(timerId)
})
})
</script>
watchEffect와 onCleanup
<script setup lang="ts">
import { ref, watchEffect } from 'vue'
const userId = ref(1)
watchEffect((onCleanup) => {
const controller = new AbortController()
fetch(`/api/users/${userId.value}`, {
signal: controller.signal
}).then(res => res.json())
.then(data => console.log(data))
onCleanup(() => {
controller.abort()
})
})
</script>
computed vs watch — 언제 어떤 것을 사용?
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
const price = ref(1000)
const quantity = ref(5)
// computed — 파생 데이터를 계산할 때
// "이 값들로부터 새로운 값을 만들어야 할 때"
const total = computed(() => price.value * quantity.value)
// watch — 사이드 이펙트를 실행할 때
// "이 값이 변하면 뭔가를 해야 할 때"
watch(total, (newTotal) => {
// API 호출, 로그 기록, 알림 등
if (newTotal > 10000) {
console.log('주문 금액이 10,000원을 초과했습니다')
}
})
</script>
핵심 원칙: 값을 만들어야 하면 computed, 작업을 해야 하면 watch
실전 패턴 — 라우트 변경 감지
<script setup lang="ts">
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
// 라우트 파라미터 변경 시 데이터 다시 로드
watch(
() => route.params.id,
async (newId) => {
if (newId) {
// await fetchPost(newId)
}
},
{ immediate: true }
)
</script>
면접 팁
- watch와 watchEffect의 차이를 ** 의존성 추적 방식 **(명시적 vs 자동)으로 설명하세요
- "watch를 쓸 때 deep: true를 남발하면 안 되는 이유"를 물어보면, ** 성능 비용 **을 언급하세요 — 모든 중첩 속성을 순회하므로 큰 객체에서는 비용이 큽니다
- 클린업 패턴(API 요청 취소, 디바운스)은 실무에서 자주 쓰는 패턴이므로 설명할 수 있으면 좋습니다
요약
watch는 명시적 소스를 감시하고 이전 값에 접근할 수 있으며, watchEffect는 자동으로 의존성을 추적하여 즉시 실행됩니다. 파생 데이터에는 computed, 사이드 이펙트에는 watch를 사용하고, 클린업 함수로 이전 사이드 이펙트를 정리하는 것이 중요합니다.
댓글 로딩 중...