watch는 "특정 데이터가 변할 때 사이드 이펙트를 실행"하는 도구입니다. computed가 파생 데이터를 만든다면, watch는 데이터 변경에 반응하는 로직을 처리합니다.

면접에서 "watch와 watchEffect의 차이"를 물어보면, 의존성 추적 방식 의 차이를 설명할 수 있어야 합니다.


watch — 명시적 감시

VUE
<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 — 자동 의존성 추적

VUE
<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 비교

항목watchwatchEffect
의존성명시적 지정자동 추적
이전 값(newVal, oldVal) 접근 가능접근 불가
즉시 실행기본 lazy (옵션으로 immediate)즉시 실행
정밀 제어어떤 값을 감시할지 선택사용된 모든 값 추적
사용 시나리오특정 값 변경 시 로직여러 값에 의존하는 사이드 이펙트

watch 옵션들

VUE
<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>

감시 중지

VUE
<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 요청 취소 등에 유용합니다.

VUE
<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

VUE
<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 — 언제 어떤 것을 사용?

VUE
<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


실전 패턴 — 라우트 변경 감지

VUE
<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를 사용하고, 클린업 함수로 이전 사이드 이펙트를 정리하는 것이 중요합니다.

댓글 로딩 중...