IT기술/뷰.js (vue.js)

Vue.js 감시자(Watchers) 완벽 가이드: 데이터 변화 감지와 반응형 프로그래밍

후스파 2025. 7. 5. 08:27
반응형

Vue.js에서 감시자(Watchers)는 데이터 속성의 변화를 감지하고, 변화가 발생했을 때 특정 작업을 수행할 수 있도록 도와주는 기능입니다.
감시자는 주로 비동기 작업이나 복잡한 데이터 변화를 처리할 때 유용하게 사용됩니다.


기본 개념과 사용법

감시자란?

감시자는 특정 데이터 속성을 감시하여 데이터가 변경될 때마다 특정 동작을 수행하는 기능입니다. Vue.js에서 반응형 데이터(data, props, computed 속성)를 감시하여 데이터 변화를 감지해 API 호출, 컴포넌트 업데이트, 복잡한 계산 등의 로직을 쉽게 처리할 수 있습니다.

기본 예제

감시자를 사용하여 데이터 속성을 감시하고, 해당 속성이 변경될 때마다 특정 작업을 수행하는 기본 예제입니다.

<template>
  <div>
    <input v-model="message" placeholder="메시지를 입력하세요" />
    <p>메시지: {{ message }}</p>
    <p>문자 수: {{ messageLength }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: '',
      messageLength: 0
    };
  },
  watch: {
    message(newValue, oldValue) {
      console.log(`메시지가 변경되었습니다: ${oldValue} -> ${newValue}`);
      this.messageLength = newValue.length;
      
      // 비동기 작업 예시
      if (newValue.length > 10) {
        this.validateMessage(newValue);
      }
    }
  },
  methods: {
    async validateMessage(message) {
      // API 호출 시뮬레이션
      console.log('메시지 검증 중...');
    }
  }
};
</script>

Composition API에서의 watch

watch 함수 사용법

<template>
  <div>
    <input v-model="searchQuery" placeholder="검색어를 입력하세요" />
    <p>검색 결과: {{ searchResults.length }}개</p>
  </div>
</template>

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

const searchQuery = ref('')
const searchResults = ref([])

// 기본 watch 사용법
watch(searchQuery, (newQuery, oldQuery) => {
  console.log(`검색어 변경: ${oldQuery} -> ${newQuery}`)
  performSearch(newQuery)
})

// 여러 값을 동시에 감시
const firstName = ref('')
const lastName = ref('')
const fullName = ref('')

watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
  fullName.value = `${newFirst} ${newLast}`
  console.log('이름이 변경되었습니다')
})

const performSearch = async (query) => {
  if (query.length > 2) {
    // API 호출 시뮬레이션
    searchResults.value = await fetchSearchResults(query)
  } else {
    searchResults.value = []
  }
}

const fetchSearchResults = async (query) => {
  // 실제 API 호출 로직
  return new Promise(resolve => {
    setTimeout(() => {
      resolve([`결과1 for ${query}`, `결과2 for ${query}`])
    }, 500)
  })
}
</script>

watchEffect 사용법

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

const count = ref(0)
const multiplier = ref(2)

// watchEffect는 콜백 함수 내에서 접근하는 모든 반응형 속성을 자동으로 추적
watchEffect(() => {
  console.log(`계산 결과: ${count.value * multiplier.value}`)
})

// count나 multiplier 중 어느 것이든 변경될 때마다 자동으로 실행됨
</script>

깊은 감시자(Deep Watchers)

객체나 배열의 속성을 감시할 때, 깊은 감시가 필요할 수 있습니다. 깊은 감시를 사용하면 객체의 내부 속성이나 배열의 요소가 변경될 때도 감지할 수 있습니다.

Options API에서 깊은 감시

<template>
  <div>
    <button @click="addItem">아이템 추가</button>
    <button @click="updateUser">사용자 정보 변경</button>
    <ul>
      <li v-for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
    <p>사용자: {{ user.name }} ({{ user.age }}세)</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      items: [
        { id: 1, name: '아이템 1' },
        { id: 2, name: '아이템 2' }
      ],
      user: {
        name: 'Alice',
        age: 25,
        preferences: {
          theme: 'dark',
          language: 'ko'
        }
      }
    };
  },
  watch: {
    items: {
      handler(newItems) {
        console.log('아이템 목록이 변경되었습니다:', newItems);
        this.saveToLocalStorage('items', newItems);
      },
      deep: true // 깊은 감시 활성화
    },
    user: {
      handler(newUser, oldUser) {
        console.log('사용자 정보가 변경되었습니다');
        // 중첩된 객체 변경도 감지됨
      },
      deep: true
    }
  },
  methods: {
    addItem() {
      this.items.push({ 
        id: Date.now(), 
        name: `아이템 ${this.items.length + 1}` 
      });
    },
    updateUser() {
      this.user.preferences.theme = this.user.preferences.theme === 'dark' ? 'light' : 'dark';
    },
    saveToLocalStorage(key, data) {
      localStorage.setItem(key, JSON.stringify(data));
    }
  }
};
</script>

Composition API에서 깊은 감시

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

const user = reactive({
  profile: {
    name: 'John',
    email: 'john@example.com'
  },
  settings: {
    notifications: true,
    theme: 'light'
  }
})

// 반응형 객체를 직접 감시하면 자동으로 깊은 감시가 됨
watch(user, (newUser, oldUser) => {
  console.log('사용자 객체가 변경되었습니다')
  // 중첩된 속성 변경도 감지됨
})

// 특정 깊이까지만 감시 (Vue 3.5+)
watch(user, (newUser, oldUser) => {
  console.log('2단계 깊이까지만 감시')
}, { deep: 2 })

// getter 함수와 deep 옵션 조합
watch(
  () => user.profile,
  (newProfile, oldProfile) => {
    console.log('프로필이 변경되었습니다')
  },
  { deep: true }
)
</script>

즉시 실행 감시자(Immediate Watchers)

즉시 실행 감시자는 컴포넌트가 생성될 때 즉시 실행되도록 설정할 수 있습니다.

Options API에서 즉시 실행

<template>
  <div>
    <input v-model="searchTerm" placeholder="검색어 입력" />
    <div v-if="loading">검색 중...</div>
    <div v-else>
      <p>검색 결과: {{ results.length }}개</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      searchTerm: 'vue',
      results: [],
      loading: false
    };
  },
  watch: {
    searchTerm: {
      handler(newTerm) {
        this.performSearch(newTerm);
      },
      immediate: true // 컴포넌트 생성 시 즉시 실행
    }
  },
  methods: {
    async performSearch(term) {
      if (!term) return;
      
      this.loading = true;
      try {
        // API 호출 시뮬레이션
        await new Promise(resolve => setTimeout(resolve, 1000));
        this.results = [`${term} 결과 1`, `${term} 결과 2`];
      } finally {
        this.loading = false;
      }
    }
  }
};
</script>

Composition API에서 즉시 실행

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

const userId = ref(1)
const userProfile = ref(null)
const loading = ref(false)

// immediate 옵션으로 즉시 실행
watch(userId, async (newUserId) => {
  loading.value = true
  try {
    userProfile.value = await fetchUserProfile(newUserId)
  } finally {
    loading.value = false
  }
}, { immediate: true })

const fetchUserProfile = async (id) => {
  // API 호출 시뮬레이션
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ id, name: `User ${id}`, email: `user${id}@example.com` })
    }, 500)
  })
}
</script>

고급 감시자 기능

once 옵션 - 한 번만 실행

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

const isAuthenticated = ref(false)

// 한 번만 실행되는 감시자
watch(isAuthenticated, (newValue) => {
  if (newValue) {
    console.log('사용자가 처음 로그인했습니다')
    // 초기 설정 작업 수행
    initializeUserData()
  }
}, { once: true })

const initializeUserData = () => {
  // 사용자 데이터 초기화 로직
  console.log('사용자 데이터 초기화 완료')
}
</script>

flush 옵션 - 실행 타이밍 제어

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

const count = ref(0)

// 기본값: 'pre' - Vue 업데이트 전에 실행
watch(count, (newCount) => {
  console.log('pre:', newCount)
})

// 'post' - Vue 업데이트 후에 실행 (DOM 업데이트 후)
watch(count, (newCount) => {
  console.log('post:', newCount)
  // DOM이 업데이트된 후 실행됨
}, { flush: 'post' })

// 'sync' - 동기적으로 즉시 실행
watch(count, (newCount) => {
  console.log('sync:', newCount)
}, { flush: 'sync' })
</script>

this.$watch()를 활용한 동적 감시자

this.$watch() 메서드를 사용하여 동적으로 감시자를 추가할 수도 있습니다.

<template>
  <div>
    <input v-model="name" placeholder="이름을 입력하세요" />
    <button @click="startWatching" :disabled="isWatching">감시 시작</button>
    <button @click="stopWatching" :disabled="!isWatching">감시 중지</button>
    <p>감시 상태: {{ isWatching ? '활성' : '비활성' }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      name: '',
      isWatching: false,
      unwatchName: null
    };
  },
  methods: {
    startWatching() {
      this.unwatchName = this.$watch('name', (newValue, oldValue) => {
        console.log(`이름이 변경되었습니다: ${oldValue} -> ${newValue}`);
        
        // 특정 조건에서 자동으로 감시 중지
        if (newValue.length > 20) {
          console.log('이름이 너무 깁니다. 감시를 중지합니다.');
          this.stopWatching();
        }
      });
      this.isWatching = true;
    },
    stopWatching() {
      if (this.unwatchName) {
        this.unwatchName(); // 감시 중지
        this.unwatchName = null;
        this.isWatching = false;
        console.log('감시가 중지되었습니다.');
      }
    }
  },
  beforeUnmount() {
    // 컴포넌트 제거 시 감시자 정리
    this.stopWatching();
  }
};
</script>

감시자 중단하기

자동 중단

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

const count = ref(0)

// setup() 내부에서 동기적으로 생성된 감시자는 자동으로 중단됨
watchEffect(() => {
  console.log('Count:', count.value)
})

// 비동기 콜백에서 생성된 감시자는 수동으로 중단해야 함
setTimeout(() => {
  const unwatch = watchEffect(() => {
    console.log('Async watcher:', count.value)
  })
  
  // 나중에 수동으로 중단
  setTimeout(() => {
    unwatch()
  }, 5000)
}, 1000)
</script>

조건부 감시자

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

const isEnabled = ref(true)
const data = ref(null)
const processedData = ref(null)

// 조건부 감시 로직
watch(data, (newData) => {
  if (isEnabled.value && newData) {
    processedData.value = processData(newData)
  }
})

// 또는 computed를 활용한 조건부 감시
const shouldProcess = computed(() => isEnabled.value && data.value)

watch(shouldProcess, (shouldProcess) => {
  if (shouldProcess) {
    processedData.value = processData(data.value)
  }
})

const processData = (rawData) => {
  // 데이터 처리 로직
  return `Processed: ${rawData}`
}
</script>

실제 사용 사례

API 호출과 디바운싱

<template>
  <div>
    <input 
      v-model="searchQuery" 
      placeholder="사용자 검색..." 
      @input="debouncedSearch"
    />
    <div v-if="loading">검색 중...</div>
    <ul v-else>
      <li v-for="user in searchResults" :key="user.id">
        {{ user.name }} - {{ user.email }}
      </li>
    </ul>
  </div>
</template>

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

const searchQuery = ref('')
const searchResults = ref([])
const loading = ref(false)
let debounceTimer = null

// 디바운싱을 적용한 검색
watch(searchQuery, (newQuery) => {
  clearTimeout(debounceTimer)
  
  if (!newQuery.trim()) {
    searchResults.value = []
    return
  }
  
  debounceTimer = setTimeout(async () => {
    loading.value = true
    try {
      searchResults.value = await searchUsers(newQuery)
    } catch (error) {
      console.error('검색 실패:', error)
      searchResults.value = []
    } finally {
      loading.value = false
    }
  }, 300) // 300ms 디바운스
})

const searchUsers = async (query) => {
  // 실제 API 호출
  const response = await fetch(`/api/users?search=${encodeURIComponent(query)}`)
  return response.json()
}

const debouncedSearch = () => {
  // 입력 이벤트 핸들러 (필요시 추가 로직)
}
</script>

폼 유효성 검사

<template>
  <form @submit.prevent="submitForm">
    <div>
      <input 
        v-model="form.email" 
        type="email" 
        placeholder="이메일"
        :class="{ error: errors.email }"
      />
      <span v-if="errors.email" class="error-message">{{ errors.email }}</span>
    </div>
    
    <div>
      <input 
        v-model="form.password" 
        type="password" 
        placeholder="비밀번호"
        :class="{ error: errors.password }"
      />
      <span v-if="errors.password" class="error-message">{{ errors.password }}</span>
    </div>
    
    <button type="submit" :disabled="!isFormValid">가입하기</button>
  </form>
</template>

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

const form = reactive({
  email: '',
  password: ''
})

const errors = reactive({
  email: '',
  password: ''
})

// 이메일 유효성 검사
watch(() => form.email, (newEmail) => {
  if (!newEmail) {
    errors.email = '이메일을 입력해주세요'
  } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newEmail)) {
    errors.email = '올바른 이메일 형식이 아닙니다'
  } else {
    errors.email = ''
  }
})

// 비밀번호 유효성 검사
watch(() => form.password, (newPassword) => {
  if (!newPassword) {
    errors.password = '비밀번호를 입력해주세요'
  } else if (newPassword.length < 8) {
    errors.password = '비밀번호는 8자 이상이어야 합니다'
  } else {
    errors.password = ''
  }
})

const isFormValid = computed(() => {
  return form.email && form.password && !errors.email && !errors.password
})

const submitForm = () => {
  if (isFormValid.value) {
    console.log('폼 제출:', form)
  }
}
</script>

<style scoped>
.error {
  border-color: red;
}

.error-message {
  color: red;
  font-size: 0.8em;
}
</style>

성능 최적화 팁

감시자 사용 시 주의사항

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

const items = ref([])
const filter = ref('')

// 비효율적: 매번 필터링 로직 실행
watch([items, filter], ([newItems, newFilter]) => {
  // 복잡한 필터링 로직
  const filtered = newItems.filter(item => 
    item.name.toLowerCase().includes(newFilter.toLowerCase())
  )
  console.log('필터링된 아이템:', filtered)
})

// 효율적: computed 사용
const filteredItems = computed(() => {
  return items.value.filter(item => 
    item.name.toLowerCase().includes(filter.value.toLowerCase())
  )
})

// 필터링된 결과가 변경될 때만 감시
watch(filteredItems, (newFilteredItems) => {
  console.log('필터링된 아이템:', newFilteredItems)
})

// 깊은 감시 남용
const largeObject = ref({})
watch(largeObject, () => {
  console.log('객체 변경됨')
}, { deep: true }) // 대용량 객체에서는 성능 문제 발생 가능

// 특정 속성만 감시
watch(() => largeObject.value.specificProperty, (newValue) => {
  console.log('특정 속성 변경됨:', newValue)
})
</script>

결론

Vue.js의 감시자는 데이터 속성의 변화를 감지하고, 그에 따라 특정 작업을 수행할 수 있는 유용한 기능입니다. 기본 감시 외에도 깊은 감시, 즉시 실행 감시, 동적 감시 추가 및 중지 기능을 통해 다양한 상황에 맞게 활용할 수 있습니다.
핵심 포인트:

  • watch vs watchEffect: 명시적 감시 vs 자동 의존성 추적
  • 깊은 감시(deep): 중첩된 객체/배열 변경 감지
  • 즉시 실행(immediate): 컴포넌트 생성 시 즉시 실행
  • 실행 타이밍(flush): pre/post/sync 옵션으로 실행 시점 제어
  • 성능 최적화: computed와 적절한 조합, 불필요한 깊은 감시 피하기

감시자를 올바르게 활용하면 Vue.js 애플리케이션에서 복잡한 데이터 흐름과 비동기 작업을 효과적으로 관리할 수 있습니다.

반응형