[vue3] 감시자(watchers)

송인호·2023년 8월 21일
0

vue3

목록 보기
13/15
post-thumbnail

계산된 속성은 계산되어 파생된 값을 선언적으로 사용할 수 있게 함.
그러나 상태 변경에 대한 반응으로 '사이드 이펙트'(DOM을 변경하거나 비동기 작업의 결과를 기반으로 다른 상태를 변경하는 것)을 수행해야 하는 경우가 있음.

Composition API를 사용하는 경우, watch 함수를 사용하여 반응형 속서잉 변경될 때마다 함수를 실행 할 수 있음.

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

const question = ref('')
const answer = ref('질문에는 일반적으로 물음표가 포함됩니다.')

// watch는 ref에서 직접 작동합니다
watch(question, async (newQuestion, oldQuestion) => {
  if (newQuestion.indexOf('?') > -1) {
    answer.value = '생각 중...'
    try {
      const res = await fetch('https://yesno.wtf/api')
      answer.value = (await res.json()).answer === 'yes' ? '네' : '아니오'
    } catch (error) {
      answer.value = '에러! API에 연결할 수 없습니다. ' + error
    }
  }
})
</script>

<template>
  <p>/아니오 질문:
    <input v-model="question" />
  </p>
  <p>{{ answer }}</p>
</template>

감시 대상 타입

watch 의 첫 번째 인자는 다양한 유형의 반응형 "소스"일 수 있음. 참조(개선된 참조 포함), 반응형 객체, 게터 함수 또는 여러 소스의 배열이 될 수 있음.

const x = ref(0)
const y = ref(0)

// 단일 ref
watch(x, (newX) => {
  console.log(`x값: ${newX}`)
})

// getter
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`x + y: ${sum}`)
  }
)

// 여러 소스의 배열
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x는 ${newX}이고, y는 ${newY} 입니다.`)
})

반응형 객체의 속성을 감시할 수는 없음

const obj = reactive({ count: 0 })

// 이것은 watch()에 숫자를 전달하기 때문에 작동하지 않습니다.
watch(obj.count, (count) => {
  console.log(`count 값: ${count}`)
})

대신 getter를 사용하면 가능

watch(
  () => obj.count,
  (count) => {
    console.log('count 값: ${count}`)
  }
)

깊은 감시자

반응형 객체에서 watch()를 직접 호출하면 암시적으로 심층 감시자가 생성되며, 콜백은 중첩된 모든 변경에서 트리거됨.

const someObject = reactive({ count : 0 })

watch(someObject, (newValue, oldValue) => {
  // 중첩된 속성 변경사항이 있을 경우 실행됨.
  // newValue와 oldValue는 같음
  // 둘 다 동일한 객체를 참고하고 있기 때문.
})

someObject.count++

newValue = 1, oldValue = 1 이 됨.

반응형 객체를 반환하는 게터와 구별해야 함. 후자의 경우 콜백은 게터가 다른 객체를 반환하는 경우에만 실행됨.

const state = reactive({
  someObject: { count: 0 }
})

watch(
  () => state.someObject,
  () => {
    // state.someObject가 교체될 때만 실행됨
  }
)

deep 옵션을 명시적으로 사용하여 깊은 감시자로 강제할 수 있음.

watch(
  () => state.someObject,
  (newValue, oldValue) => {
    // state.someObject가 교체되지 않는 한 여기에서
    // `newValue`와 `oldValue`는 같음.
  },
  { deep: true }
)

이렇게 하면 깊은 감시자와 같이 사용할 수 있음.

단 깊은 감시는 감시된 객체의 모든 중첩 속성을 탐색하여, 큰 데이터 구조에서 사용할 때 비용이 많이 들 수 있음. 성능에 영향을 주는지 고려해서 필요한 경우에만 사용하기.

열성적인(Eager) 감시자

watch는 기본적으로 게으름(lazy). 콜백은 감시된 소스가 변경되기 전까지 호출되지 않음.
그러나 어떤 경우에는 동일한 콜백 로직이 열성적으로 실행되기를 원할 수 있음. 예를 들어 최초 데이터가 구성된 후 콜백이 실행되기를 원할 수 있음.
immediate: true 옵션을 전달하여 watch 콜백이 즉시 실행되도록 강제할 수 있음.

watch(source, (newValue, oldValue) => {
  // 즉시 실행된 다음 `source`가 변경되면 다시 실행됩니다.
}, { immediate: true })

watchEffect()

watch()는 게으르므로 감시 소스가 변경될 때까지 콜백이 호출되지 않음.
그러나 어떤 경우에는 동일한 콜백 로직이 열성적으로 실행되기를 원할 수 있음. 예를 들어 초기 데이터를 가져온 다음 관련 상태가 변경될 때마다 데이터를 다시 가져오기를 원할 수 있음.

const url = ref('http://...')
const data = ref(null)

async function fetchData() {
  const response = await fetch(url.value)
  data.value = await response.json()
}

// 즉시 데이터 가져오기
fetchData()
// ...그런다음 url 변경을 감시하도록 watch를 실행
watch(url, fetchData)

위 구현 로직은 watchEffect() 로 단순화 할 수 있음. watchEffect()를 사용하면 반응형 의존성을 자동으로 감시하면서, 최초에 즉시 사이드 이펙트를 한 번 실행함. 위의 예는 다음과 같이 다시 작성할 수 있음.

const url = ref('https://...')
const data = ref(null)

watchEffect(astnc () => {
  const response = await fetch(url.value)
  data.value = await response.json()
}) 

이러면 콜백이 최초에 즉시 한 번 실행됨. 실행되는 동안에도 자동으로 의존성인 url.value를 추적함.
따라서 url.value가 변경될 때마다 콜백이 다시 실행됨.

여기서 알 수 있는 것 watchEffect는 created와 watch를 함께 사용하는 것

watch vs watchEffect

watchwatchEffect 둘 다 사이드 이펙트를 반응적으로 실행할 수 있게 해줌.
주요 차이점은 반응형 의존성을 추적하는 방식.

  • watch는 명시적으로 감시된 소스만 추적함. 콜백 내에서 조회하는 항목은 추적하지 않음. 또한 콜백은 소스가 실제 변경된 경우에만 트리거가 됨. watch는 의존성 추적을 사이드 이펙트와 분리하여, 콜백이 실행되어야 하는 시기를 보다 정확히 제어할 수 있음.

  • watchEffect는 의존성 추적과 사이드 이펙트를 하나의 단계로 결합함. 동기적(sync) 실행중에 조회되는 모든 반응형 속성을 자동으로 추적함.
    이것은 더 편리하고 일반적으로 더 간결한 코드를 생성하지만, 콜백이 실행되어야 하는 시기가 덜 명시적임.

콜백 실행 타이밍

반응형 상태를 번경하면 Vue 컴포넌트 업데이트와 사용자가 만든 감시자 콜백이 모두 실행됨.

기본적으로 개발자가 생성한 감시자 콜백은 Vue 컴포넌트가 업데이트되기 전에 실행됨. 따라서 감시자 콜백 내에서 DOM에 접근하면 DOM이 Vue에 의해 업데이트되기 전의 상태입니다.

Vue에 의해 업데이트된 후 의 DOM을 감시자 콜백에서 접근하려면, fulsh: 'post' 옵션을 지정해야 함.

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

watchEffect(callback, {
  flush: 'post'
})

flush: 'post' 옵션이 적용된 watchEffect()를 보다 간편하게 사용하기 위해서 watchPostEffect()를 사용할 수 있음.

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  /* Vue가 업데이트 된 후 실행함. */
})

기본적으로 flush: 'pre'|'post' 옵션은 콜백을 버퍼링하여, 동일한 "틱(tick)"에서 여러 번 상태 변경이 되더라도, 마지막에 한 번만 호출됨.

동일한 틱 내에 여러 번 상태 변경 시 마다 동기적으로 콜백을 호출해야 하는 경우, flush: 'sync'옵션을 사용해야함.
단, 일반적으로 이러한 동작은 비효율적이므로 사용하려는 경우, 정말 필요한지 다시 고민해보아야 함.

const count = ref(0)
const callback = (val, preVal) => console.log('변경이 감지됨!', val, preVal)
const options = { flush: 'sync' }

watch(count, callback, options)

count.value++
// 이어서 callback이 실행됨
count.value++
// 역시 callback이 실행됨
count.value++
// 또 callback이 실행됨

감시자 중지하기

setup() 또는 <script setup> 내부에서 동기적으로 선언된 감시자는 해당 컴포넌트 인스턴스에 바인딩되며, 해당 컴포넌트가 마운트 해제되면 자동으로 중지됨.
대부분의 경우 감시자를 직접 중지하는 것에 대해 고민할 필요가 없음.

핵심은 감시자가 동기적으로 생성되어야 함. 감시자가 비동기 콜백에서 생성된 경우, 감시자는 해당 컴포넌트에 바인딩 되지 않으며 메모리 누수를 방지하기 위해 수동으로 중지해야 함.

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

// 이 감시자는 컴포넌트가 마운트 해제되면 자동으로 중지됩니다.
watchEffect(() => {})

// ...하지만 이것은 자동으로 중지되지 않습니다.
setTimeout(() => {
  watchEffect(() => {})
}, 100)
</script>

감시자를 수동으로 중지하려면 반환된 함수를 사용. 이것은 watchwatchEffect 모두에서 작동함.

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

// ...나중에 감시자가 더 이상 필요하지 않을 때
unwatch()

감시자를 비동기식으로 생성해야 하는 경우는 거의 없으며, 가능하면 동기식 생성을 해야함. 일부 비동기 데이터를 기다려야 하는 경우, 감시자 로직을 조건부로 만들 수 있음.

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

watchEffect(() => {
  if (data.value) {
    // 데이터가 로드될 때 실행될 로직
  }
})
profile
프론트엔드 개발자

0개의 댓글