본문 바로가기
Font-End

Vue 3의 watch와 watchEffect - 반응형 데이터 추적의 모든 것

by 향테크 2025. 3. 30.

오늘은 Vue 3의 Composition API에서 제공하는 두 가지 중요한 함수인 watch와 watchEffect에 대해 깊이 파헤쳐 보려고 합니다.

두 함수 모두 반응형 데이터의 변화를 추적하는 데 사용되지만, 사용 방식과 동작 원리에 미묘한 차이가 있습니다.

제대로 이해하고 적재적소에 활용해 봅시다!

 

Vue3의 watch와 watchEffect에 대해서 알아보자

 

목차

  1. 반응형 데이터 추적이 필요한 이유
  2. watch 함수 기본 사용법
  3. watchEffect와의 차이점
  4. 실전 사용 사례
  5. 성능 최적화 팁
  6. 자주 겪는 실수와 해결책
  7. 마무리

 

반응형 데이터 추적이 필요한 이유

Vue 애플리케이션을 개발하다 보면 어떤 데이터가 변경될 때 특정 작업을 수행해야 하는 경우가 많습니다. 예를 들어, 사용자가 검색어를 입력할 때마다 API 요청을 보내거나, 장바구니에 상품이 추가될 때마다 총액을 다시 계산하는 등의 작업이죠.

 

Options API에서는 주로 watch 속성을 사용했지만, Composition API에서는 watch와 watchEffect 두 가지 함수로 더 유연하게 반응형 데이터를 추적할 수 있게 되었습니다.

 

watch 함수 기본 사용법

watch 함수는 특정 반응형 소스(ref, reactive 객체, computed 값, getter 함수)를 감시하고, 그 값이 변경될 때 콜백 함수를 실행합니다.

 

단일 ref 감시하기

import { ref, watch } from 'vue'

export default {
  setup() {
    const count = ref(0)
    
    watch(count, (newValue, oldValue) => {
      console.log(`카운트가 ${oldValue}에서 ${newValue}로 변경되었습니다.`)
    })
    
    return { count }
  }
}

 

 

여러 소스 동시에 감시하기

import { ref, watch } from 'vue'

export default {
  setup() {
    const firstName = ref('향')
    const lastName = ref('테크')
    
    watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
      console.log(`이름이 ${oldFirst}${oldLast}에서 ${newFirst}${newLast}로 변경되었습니다.`)
    })
    
    return { firstName, lastName }
  }
}

 

getter 함수로 복잡한 표현식 감시하기

import { reactive, watch } from 'vue'

export default {
  setup() {
    const user = reactive({
      name: '향테크',
      age: 32,
      address: {
        city: '서울',
        district: '강남구'
      }
    })
    
    // user.address.city가 변경될 때만 실행됨
    watch(
      () => user.address.city,
      (newCity, oldCity) => {
        console.log(`도시가 ${oldCity}에서 ${newCity}로 변경되었습니다.`)
      }
    )
    
    return { user }
  }
}

 

옵션 활용하기

watch는 세 번째 인자로 옵션 객체를 받을 수 있습니다.

watch(source, callback, {
  immediate: true, // 컴포넌트가 마운트될 때 즉시 콜백 실행
  deep: true,      // 객체 내부의 중첩된 변경 사항까지 감지
  flush: 'post'    // DOM 업데이트 후에 콜백 실행 ('pre', 'post', 'sync')
})

 

실제 프로젝트에서는 immediate: true를 자주 사용하게 되는데, 이는 초기값에 대해서도 콜백을 실행하고 싶을 때 유용합니다.

 

watchEffect와의 차이점

watchEffect는 watch와 비슷하지만 조금 다른 접근 방식을 가집니다.

  1. 자동 의존성 추적 : 콜백 함수 내에서 접근하는 모든 반응형 속성을 자동으로 추적합니다.
  2. 즉시 실행 : 별도의 옵션 없이도 컴포넌트 마운트 시 즉시 실행됩니다.
  3. 이전 값에 접근 불가 : 콜백에서 이전 값(oldValue)에 접근할 수 없습니다.
import { ref, watchEffect } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const name = ref('향테크')
    
    watchEffect(() => {
      // count와 name 모두 추적됨
      console.log(`현재 카운트: ${count.value}, 이름: ${name.value}`)
      
      // API 호출 등의 사이드 이펙트를 수행할 수 있음
      document.title = `${name.value}님의 카운트: ${count.value}`
    })
    
    return { count, name }
  }
}

 

 

watchEffect는 간결한 코드로 여러 값을 동시에 추적하기 좋지만, 특정 변수의 변경에만 반응하고 싶거나 이전 값과 비교가 필요한 경우에는 watch가 더 적합합니다.

 

실전 사용 사례

사례 1 - 검색 자동완성

import { ref, watch } from 'vue'
import { debounce } from 'lodash-es'

export default {
  setup() {
    const searchQuery = ref('')
    const searchResults = ref([])
    const isLoading = ref(false)
    
    // 디바운스 적용한 API 호출
    const debouncedGetResults = debounce(async (query) => {
      if (query.length < 2) return
      
      try {
        isLoading.value = true
        const response = await fetch(`/api/search?q=${query}`)
        searchResults.value = await response.json()
      } catch (error) {
        console.error('검색 중 오류 발생:', error)
      } finally {
        isLoading.value = false
      }
    }, 300)
    
    watch(searchQuery, (newQuery) => {
      debouncedGetResults(newQuery)
    })
    
    return { searchQuery, searchResults, isLoading }
  }
}

 

사례 2 - 양식 유효성 검사

import { reactive, computed, watchEffect } from 'vue'

export default {
  setup() {
    const form = reactive({
      email: '',
      password: '',
      confirmPassword: ''
    })
    
    const errors = reactive({
      email: '',
      password: '',
      confirmPassword: ''
    })
    
    const isValid = computed(() => {
      return !errors.email && !errors.password && !errors.confirmPassword
    })
    
    watchEffect(() => {
      // 이메일 유효성 검사
      if (!form.email) {
        errors.email = '이메일을 입력해주세요.'
      } else if (!/^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/.test(form.email)) {
        errors.email = '유효한 이메일 형식이 아닙니다.'
      } else {
        errors.email = ''
      }
      
      // 비밀번호 유효성 검사
      if (!form.password) {
        errors.password = '비밀번호를 입력해주세요.'
      } else if (form.password.length < 8) {
        errors.password = '비밀번호는 8자 이상이어야 합니다.'
      } else {
        errors.password = ''
      }
      
      // 비밀번호 확인 검사
      if (form.password !== form.confirmPassword) {
        errors.confirmPassword = '비밀번호가 일치하지 않습니다.'
      } else {
        errors.confirmPassword = ''
      }
    })
    
    return { form, errors, isValid }
  }
}

 

성능 최적화 팁

1. 필요한 경우에만 deep 옵션 사용하기

deep: true 옵션은 객체의 모든 중첩 속성을 재귀적으로 감시하므로 성능 비용이 큽니다. 특정 속성만 감시하는 것이 가능하다면 getter 함수를 사용하세요.

// 비효율적인 방법 (전체 객체를 깊게 감시)
watch(user, (newUser) => {
  console.log('도시 변경됨:', newUser.address.city)
}, { deep: true })

// 효율적인 방법 (특정 속성만 감시)
watch(() => user.address.city, (newCity) => {
  console.log('도시 변경됨:', newCity)
})

 

2. 감시 중지하기

watch와 watchEffect는 모두 감시를 중지하는 함수를 반환합니다. 불필요한 감시는 중지하여 메모리 누수를 방지하세요.

const stopWatching = watch(count, (newValue) => {
  if (newValue > 10) {
    console.log('카운트가 10을 초과했습니다!')
    stopWatching() // 더 이상 감시하지 않음
  }
})

 

3. flush 타이밍 조정하기

flush 옵션으로 콜백 실행 타이밍을 조정할 수 있습니다.

watch(source, callback, {
  flush: 'post' // DOM 업데이트 후 실행 (기본값)
})

watchEffect(callback, {
  flush: 'pre' // DOM 업데이트 전 실행
})

 

DOM에 접근해야 하는 경우 'post' 옵션이, 렌더링 최적화가 필요한 경우 'pre' 옵션이 유용합니다.

 

자주 겪는 실수와 해결책

1. reactive 객체를 직접 감시할 때 변경 감시 실패

const user = reactive({ name: '김코딩' })

// ❌ 이렇게 하면 user가 교체될 때만 감지하고, 속성 변경은 감지하지 못함
watch(user, () => {
  console.log('유저 정보 변경됨')
})

// ✅ 이렇게 해결
watch(() => ({ ...user }), () => {
  console.log('유저 정보 변경됨')
}, { deep: true })

// ✅ 또는 toRefs 사용
import { toRefs } from 'vue'
const { name } = toRefs(user)
watch(name, () => {
  console.log('이름 변경됨')
})

 

2. 비동기 콜백에서 이전 값 참조

비동기 콜백 내에서 oldValue를 참조할 때, 이미 값이 변경되었을 수 있습니다.

// ❌ 문제가 될 수 있는 코드
watch(count, async (newValue, oldValue) => {
  await someAsyncOperation()
  console.log(oldValue) // 예상과 다른 값이 출력될 수 있음
})

// ✅ 해결책: 필요한 값을 미리 캡처
watch(count, async (newValue, oldValue) => {
  const capturedOldValue = oldValue
  await someAsyncOperation()
  console.log(capturedOldValue) // 정확한 이전 값
})

 

3. watchEffect 내부에서 비동기 작업 처리

watchEffect 내부에서 비동기 작업을 수행할 때 주의가 필요합니다.

// ❌ 문제가 될 수 있는 코드
watchEffect(async () => {
  const result = await fetchData(id.value)
  data.value = result
})

// ✅ 해결책: watch 사용 또는 비동기 로직 분리
const fetchDataAction = async () => {
  const result = await fetchData(id.value)
  data.value = result
}

watchEffect(() => {
  // id.value에 접근하여 의존성 추적
  fetchDataAction()
})

 

마무리

Vue 3의 watch와 watchEffect는 반응형 데이터의 변화를 감지하고 대응하는 강력한 도구입니다. 둘 다 특정 상황에 맞게 선택하여 사용하면 됩니다.

  • 특정 데이터 변화만 감시하고 싶을 때 : watch
  • 여러 데이터의 변화를 한 번에 처리하고 싶을 때 : watchEffect
  • 이전 값과 비교가 필요할 때 : watch
  • 코드를 간결하게 유지하고 싶을 때 : watchEffect

실무에서는 두 함수를 상황에 맞게 혼합해서 사용하는 것이 일반적입니다. 직접 다양한 상황에 적용해 보면서 감각을 키워보세요!

개인적으로는 명시적인 의존성 추적이 가능한 watch를 선호하지만, 간단한 사이드 이펙트나 여러 반응형 소스를 다룰 때는 watchEffect의 간결함이 매력적입니다.

728x90
반응형