Vue 기본 문법 5. computed, method, watch 정리 (3편)

손근영·2025년 5월 14일

Vue

목록 보기
6/7

앞선 1, 2편에서는 computedmethod의 차이점과 각각의 사용 목적에 대해 알아보았습니다. computed는 의존하는 반응형 데이터가 변경될 때 자동으로 재계산되는 캐싱된 속성이었고, method는 호출 시마다 계산이 수행되는 일반 함수라는 점에서 차이가 있었죠.

이번 3편에서는 그 흐름을 이어서, Vue의 또 다른 중요한 개념인 watch에 대해 집중적으로 알아보려 합니다. watch는 데이터의 변화를 감지하고 그에 따라 특정 작업을 수행할 수 있도록 해주는 도구입니다. 단순히 값을 보여주는 것을 넘어서, 부수 효과(side effect)가 필요한 경우에 자주 활용됩니다.

예를 들어, 어떤 값이 바뀔 때마다 API를 호출하거나, 콘솔에 로그를 남기거나, 다른 데이터를 동기화해야 하는 경우에 watch가 유용합니다. computedmethod로는 처리하기 어려운 부분들을 watch를 통해 어떻게 해결할 수 있는지, 실제 예제를 통해 함께 살펴보겠습니다.


watch 기본 문법

watch 함수는 다음과 같은 세 개의 인자로 구성됩니다.

watch(source, callback, options?)

source: 감시 대상 (ref, getter 함수, 배열 등)

callback: 값이 바뀔 때 실행되는 함수 (newVal, oldVal) => { ... } 와 같은 형태

options: { immediate, deep, flush } 같은 부가 설정 (선택)


예제

간단히 count 를 증가시키는 예제를 봅시다.

<template>
  <div>
    <button @click="count++">count 증가 버튼</button>
  </div>
</template>

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

const count = ref(0)

watch(count, (newVal, oldVal) => {
  console.log(`count가 ${oldVal}에서 ${newVal}로 변경되었습니다.`)
})
</script>

이 코드는 count의 값이 변할 때마다 콘솔에 변경 로그를 남깁니다.


감시하고자 하는 반응형 변수가 조금 더 복잡하다면?

refreactive를 사용해 객체 형태로 상태를 관리하는 경우, 단순한 변수와는 달리 감시 방식에 조금 더 주의가 필요합니다.

예를 들어 아래와 같은 user 객체를 감시한다고 가정해봅시다.

const user = ref({
  name: 'kim',
  age: 20
})

이제 user 객체의 age 속성이 변경될 때마다 감시하고 싶다면 어떻게 해야 할까요?

watch(user.value.age, (newVal, oldVal) => {
  console.log(`나이 변경됨: ${oldVal}${newVal}`)
})

이렇게 코드를 작성하면 될까요?

ref로 선언된 반응형 변수는 JavaScript 코드상에서는 .value로 접근해야 하며, 그에 따라 user.value.age가 변경됨을 감시하고자 한 코드는 논리적으로 맞아 보일 수 있습니다.

그러나 이 코드는 작동하지 않습니다.

그 이유는, user.value.age는 이미 한 번 평가되어 단순한 숫자 값이 되었기 때문입니다.
Vue는 이렇게 즉시 평가된 값을 감시할 수 없습니다. Vue의 watch()반응형 객체 또는 getter 함수를 통해 의존성을 추적해야 반응형으로 감시가 가능합니다.


그렇다면 어떻게?

위 코드를 올바르게 수정하려면 다음과 같이 getter 함수를 써야 합니다.

watch(() => user.value.age, (newVal, oldVal) => {
  console.log(`나이 변경됨: ${oldVal}${newVal}`)
})

이제 Vue는 이 함수 내부에서 어떤 반응형 속성(user.value.age)이 접근되고 있는지 추적하고, 해당 값이 변경될 때 콜백을 실행합니다

혹은, 다음과 같은 방법도 가능합니다.

watch(user, (newVal, oldVal) => {
  console.log('user 객체의 어떤 속성이든 변경됨')
}, { deep: true })

deep: true를 사용하면, user 객체 내부의 모든 중첩된 속성 변경도 감지할 수 있습니다.


reactive 변수는 다르게 작동할까?

ref와는 달리 reactive로 작성된 객체는 .value를 사용할 필요 없이 일반 객체처럼 바로 속성에 접근할 수 있습니다.

그렇다면 watch()로 감시할 때도 동작 방식이 달라질까요?

결론: 동작 방식이 다릅니다.

ref()로 만든 객체는 기본적으로 deep: false 입니다.

즉, 객체 내부의 속성 변경은 감지되지 않습니다.

하지만 reactive()로 만든 객체는 기본적으로 deep: true로 설정되어 있어,
객체 자체를 감시하면 내부 속성의 변화도 자동으로 감지됩니다.

const user = reactive({ name: 'kim', age: 20 })

watch(user, () => {
  console.log('user의 내부 속성이 변경되었습니다.')
})

위 코드에서는 user.age++와 같이 내부 속성만 변경되어도 watch() 콜백이 실행됩니다.
별도로 deep: true 옵션을 명시하지 않아도 Vue가 자동으로 재귀적 감시(deep watch)를 수행하기 때문입니다.


하지만 여전히..

reactive 객체라고 해도 user.age와 같은 특정 속성만 감시하고 싶다면,
여전히 getter 함수를 사용해야 합니다.

watch(() => user.age, (newVal, oldVal) => {
  console.log(`age 변경: ${oldVal}${newVal}`)
})

실제 코드 동작

실제 코드 동작으로 확인해봅시다.

<template>
  <div>
    <h2>Vue watch 실험</h2>
    <button @click="userRef.age++">ref 객체 나이 증가</button>
    <button @click="userReactive.age++">reactive 객체 나이 증가</button>
  </div>
</template>

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

// 1. ref 객체
const userRef = ref({ name: 'Kim', age: 20 })

// 2. reactive 객체
const userReactive = reactive({ name: 'Lee', age: 30 })

// ❌ 얕은 감시 (내부 속성 변경 감지 안 됨)
watch(userRef, () => {
  console.log('userRef 변경 감지됨')
})

// ✅ 깊은 감시 (ref 내부 속성 변경 감지됨)
watch(userRef, () => {
  console.log('userRef (deep: true) 변경 감지됨')
}, { deep: true })

// ✅ reactive는 자동으로 깊게 감시됨
watch(userReactive, () => {
  console.log('userReactive 변경 감지됨')
})

// ✅ reactive는 자동으로 깊게 감시됨
watch(userReactive, () => {
  console.log('userReactive (deep: true) 변경 감지됨')
} , {deep : true})
</script>

실제로 reactive로 선언한 객체는 기본적으로 deep: true로 동작하는 것을 볼 수 있습니다.


watch의 주요 옵션들

immediate : 감시 시작 즉시 한 번 실행

값이 변경되지 않더라도, watch 등록 직후 한 번 콜백을 실행하고 싶은 경우 사용합니다.

const count = ref(0)

watch(count, (newVal, oldVal) => {
  console.log(`count 변경: ${oldVal}${newVal}`)
}, { immediate: true })

컴포넌트가 마운트되면 0 → undefined 로그가 바로 출력됩니다.

이후 값이 바뀔 때도 계속 감시합니다.


once : 최초 변경 시 단 1회 실행

(Vue 3.4 부터 지원되는 옵션입니다.)

감시 대상의 값이 처음으로 변경될 때 단 한 번만 콜백을 실행하고, 자동으로 감시가 종료됩니다.

const msg = ref('hello')

watch(msg, (newVal, oldVal) => {
  console.log(`처음 변경: ${oldVal}${newVal}`)
}, { once: true })

msg.value = 'hi' 로 변경하면 로그가 출력되고,
이후로는 변경해도 더 이상 콜백 실행되지 않습니다.


flush : 콜백 실행 시점 제어

Vue의 watch()는 콜백이 언제 실행될지를 조절할 수 있도록 flush 옵션을 제공합니다.

Vue는 반응형 데이터가 변경되면 컴포넌트의 DOM 업데이트를 비동기적으로 예약합니다.

이때 flush 옵션은 watch 콜백이 DOM 업데이트 전인지 후인지, 혹은 즉시 실행될지를 결정합니다.

설명
'pre'DOM 업데이트 실행 (기본값)
'post'DOM 업데이트 실행 (렌더링 직후)
'sync'즉시 동기적으로 실행 (주의 필요)

실행 흐름 다이어그램

컴포넌트의 템플릿이 이 count를 사용 중이라고 가정해 봅시다.

┌────────────────────────┐
│ 1. 반응형 값 변경      │   (count.value++)
└────────┬───────────────┘
         ↓
┌────────────────────────┐
│ 2. 반응성 트리거 발생   │   (watch(), 컴포넌트 재렌더 예약됨)
└────────┬───────────────┘
         ↓
┌──────────────────────────────┐
│ 3. flush 옵션에 따라 분기됨  │
├──────────────────────────────┤
│ 'pre'  → 렌더링 전에 실행    │ <- 기본값
│ 'post' → 렌더링 완료 후 실행 │
│ 'sync' → 값 바뀌는 즉시 실행 │
└──────────────────────────────┘
         ↓
┌────────────────────────┐
│ 4. DOM 업데이트 진행   │   (컴포넌트 렌더링)
└────────────────────────┘


아래 코드를 실행하면 flush: 'pre'flush: 'post'의 차이를 콘솔과 DOM 상태로 직접 확인할 수 있습니다.


<template>
  <div>
    <h2>flush 테스트</h2>
    <p>count: {{ count }}</p>
    <button @click="count++">count 증가</button>
  </div>
</template>

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

const count = ref(0)

// flush: 'pre' → DOM 업데이트 전에 실행됨 (기본값)
watch(count, () => {
    const domText = document.querySelector('p')?.textContent
  console.log(`[pre] 실제 DOM 내용: ${domText}`) // "count: 1"
  console.log(`[pre] count: ${count.value} (DOM 전)`)
}, { flush: 'pre' })

// flush: 'post' → DOM 업데이트 후 실행됨
watch(count, () => {
    const domText = document.querySelector('p')?.textContent
  console.log(`[post] 실제 DOM 내용: ${domText}`) // "count: 1"
  console.log(`[post] count: ${count.value} (DOM 후)`)
}, { flush: 'post' })
</script>

flush: 'pre'에서는 값은 증가했지만, 화면(DOM)은 아직 이전 상태임을 확인할 수 있습니다.

flush: 'post'에서는 DOM이 실제로 업데이트된 후 콜백이 실행되므로, count 값과 DOM 내용이 일치합니다.

주의
이 예제에서는 flush의 실행 시점을 확인하기 위해 DOM을 직접 조회했으나, 실제 Vue 프로젝트에서는 이렇게 직접 DOM을 조작하거나 읽는 방식은 지양해야 합니다.


마치며

이번 3편에서는 watch의 기본 사용법부터 refreactive의 차이, 그리고 deep, immediate, once, flush와 같은 다양한 옵션들을 예제와 함께 살펴보았습니다.

이제 여러분도 상황에 맞게 computed, method, watch를 구분하여 효율적으로 사용할 수 있을 것입니다.

앞으로도 실전에서 자주 마주치는 Vue의 개념들을 함께 하나씩 풀어가 보겠습니다.
읽어주셔서 감사합니다!

0개의 댓글