Watchers


Composition API를 사용하면 반응형 상태가 변경될 때마다 콜백을 트리거하는 watch 함수를 사용할 수 있다.


<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('질문에는 보통 물음표가 포함됩니다. ;-)')
const loading = ref(false)

// watch는 ref에 직접 작동합니다
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.includes('?')) {
    loading.value = true
    answer.value = '생각 중...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer
    } catch (error) {
      answer.value = '오류! API에 도달할 수 없습니다. ' + error
    } finally {
      loading.value = false
    }
  }
})
</script>

<template>
  <p>
    예/아니오 질문을 하세요:
    <input v-model="question" :disabled="loading" />
  </p>
  <p>{{ answer }}</p>
</template>
  • watch()는 기본적으로 lazy.
    • 콜백은 감시된 소스가 변경되었을 때만 호출
  • 첫 번째 인자: 감시될 소스
    • 값을 반환하는 getter 함수
    • ref
    • 반응형 객체
    • 또는 위에 나열한 것들의 배열
  • 두 번째 인자: 소스가 변경될 때 호출될 콜백
    • "변경된 값", "이전 값", "사이드 이펙트 클린업 콜백을 등록하기 위한 함수"
  • 세 번째 인자(optional)
    • immediate 감시자가 생성되는 즉시 콜백이 호출.
      • source가 변경될 때 다시 실행
      • (최초 호출 시, 이전 값은 undefined)
    • deep 소스가 객체인 경우, 깊은 변경사항에서도 콜백이 실행되도록 한다.
    • flush 콜백의 발생(flush) 타이밍을 조정
    • onTrack / onTrigger 감시자의 의존성을 디버그
    • once 콜백을 한 번만 실행. 감시자는 첫 번째 콜백 실행 후 자동으로 중지!



watchEffect()


콜백의 반응형 종속성을 자동으로 추적할 수 있게 해줌

  • React의 useEffect와 유사함
watchEffect(async () => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/todos/${todoId.value}`
  )
  data.value = await response.json()
})


3.5+ 버전

const { stop, pause, resume } = watchEffect(() => {})

// 임시로 감시자를 중지함
pause()

// 나중에 재개
resume()

// 중단
stop()



watch와 watchEffect 비교


watch와 watchEffect는 모두 반응형으로 부수 효과를 수행할 수 있게 해줍니다. 주요 차이점은 반응형 종속성을 추적하는 방식입니다:


  • watch명시적으로 감시된 소스만 추적

    • 콜백 내부에서 접근한 것은 추적하지 않는다.
    • 또한, 소스가 실제로 변경되었을 때만 콜백이 실행된다.
    • watch는 종속성 추적을 부수 효과와 분리하여 콜백이 실행될 시기를 더 정확하게 제어할 수 있다.
  • watchEffect는 종속성 추적과 부수 효과를 하나의 단계로 결합한다.

    • 동기적 실행 동안 접근한 모든 반응형 속성을 자동으로 추적
    • 이는 더 편리하며 일반적으로 더 간결한 코드를 작성할 수 있게 해주지만...
      • 반응형 종속성이 덜 명확


부작용 정리


watcher에서 비동기 요청과 같은 부작용(side effects)을 수행한다.

watch(id, (newId) => {
  fetch(`/api/${newId}`).then(() => {
    // callback logic
  })
})
  • 요청이 완료되기 전에 id가 변경된다면..?
    • 이전 요청이 완료되었을 때, 이미 오래된(stale) id 값을 가진 상태에서 콜백이 실행
    • 이상적으로는 id가 새로운 값으로 변경될 때, 이전의 유효하지 않은 요청을 취소할 수 있어야 함
  • 이를 위해 onWatcherCleanup() API를 사용
    • watcher가 무효화(invalidate)되고 다시 실행되기 전에 호출될 정리(cleanup) 함수를 등록할 수 있다.


import { watch, onWatcherCleanup } from 'vue'

watch(id, (newId) => {
  const controller = new AbortController()

  fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
    // callback logic
  })

  onWatcherCleanup(() => {
    // abort stale request
    controller.abort()
  })
})



콜백 플러시 타이밍


후처리 감시자

watch(source, callback, {
  flush: 'post'
})

watchEffect(callback, {
  flush: 'post'
})
import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* Vue 업데이트 후 실행됨 */
})


동기 감시자


Vue 관리 업데이트 이전에 동기적으로 실행되는 감시자

watch(source, callback, {
  flush: 'sync'
})

watchEffect(callback, {
  flush: 'sync'
})
import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  /* 반응형 데이터 변경 시 동기적으로 실행됨 */
})



감시자 중지

<script setup>
import { watchEffect } from 'vue'

// 이 감시자는 자동으로 중지됩니다.
watchEffect(() => {})

// ...이 감시자는 중지되지 않습니다!
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>
  • 대부분의 경우 감시자를 직접 중지할 필요가 없지만...
  • 감시자가 비동기 콜백 내에서 생성되면 소유자 컴포넌트에 바인딩되지 않으며
  • 메모리 누수를 피하기 위해 수동으로 중지해야 한다.


const unwatch = watchEffect(() => {})

// ...나중에, 더 이상 필요하지 않을 때
unwatch()
  • 감시자를 수동으로 중지하려면 반환된 핸들 함수를 사용


// 비동기적으로 로드될 데이터
const data = ref(null)

watchEffect(() => {
  if (data.value) {
    // 데이터가 로드될 때 무언가를 수행합니다.
  }
})
  • 비동기 데이터를 기다려야 하는 경우, 감시 로직을 조건부로 만들 수 있다.