Vue 3.0/02.Essentials

[vue 3] 11.watch

  • -

이번 포스트에서는 "지켜보는 녀석" watch에 대해서 살펴보자!

watch

 

watch?

watch는 computed와 유사하게 data 변경 시 동작하는 속성인데 값을 사용할 수도 있지만 부가적인 동작(side effect) 처리에 더 많이 사용되는 속성이다. computed는 data 처럼 template에서 사용되어야 동작하지만 watch는 template에서는 사용되지 않는다. 그저 지켜보다가 값이 변경되면 어떤 일을 처리한다. 또한 computed가 주로 동기로 값을 리턴하는 반면 watch는 비동기로 처리되며 값을 리턴하지 않는다.

사용법

// 하나의 소스 감시
function watch<T>(
  source: WatchSource<T>,
  callback: WatchCallback<T>,
  options?: WatchOptions
): StopHandle

// 여러 개의 소스 감시
function watch<T>(
  sources: WatchSource<T>[],
  callback: WatchCallback<T[]>,
  options?: WatchOptions
): StopHandle
  • source: 감시할 대상으로 ref 및 reactive 객체, getter 함수 또는 위에서 나열한 것들의 배열이다.
  • callback: 값이 변경되었을 때 동작을 수행할 함수를 등록한다. 이 콜백 함수의 파라미터로 새로운 값, 기존 값, 클린업콜백이 전달된다. 클린업 콜백의 경우는 주로 비동기에서 새로운 변경을 처리하는데 기존의 처리가 미반영 된 경우 이를 정리하는 용도로 사용될 수 있다.
  • option: watch를 위한 부가적인 option들로 이뤄진 객체이다.
    - immedidate: watcher가 처음 생성될 때도 동작할 것인가를 설정하며 이때 초기의 old value는 undefined이다.(default는 false)
    - deep: 감시 대상이 객체인 경우 객체의 property가 변경되는 것까지 감시할 것인지 설정한다. 이 값은 반응형 객체에 대한 감시인 경우 default가 true, getter function을 통한 감시의 경우 default가 false 이다.
watch(
  () => route.query.age, // getter function를 이용하는 형태
  (newParam, oldParam) => {
    console.log('watch ', oldParam, '=>', newParam)
  }
)

 

watch 활용 예

 

기본 동작

값 변경 시 추가적인 연산을 한다는 측면에서 computed와 watch가 비슷할 수 있는데 둘의 차이를 비교하면서 watch를 사용해보자. 

<div id="app">
    <label>기본 숫자:<input type="number" v-model.number.lazy="num" /></label>
    <p>제곱: {{squareC}}, {{squareW}}</p>
    <p>제곱근: {{sqrtC}}, {{sqrtW}}</p>
</div>

먼저 template에는 기본 숫자 num과 computed에 의해 처리되는 squareC, sqrtC, 그리고 watch의 영향을 받을 squareW, sqrtW가 사용되고 있다.

다음으로 script 영역을 살펴보자.

<script>
  const { createApp, ref, watch, computed } = Vue;

  createApp({
    setup() {
      const num = ref(4);

      // computed: 값이 변경되면 계산 후 반환 --> template에서 사용
      const squareC = computed(() => num.value * num.value)
      const sqrtC = computed(() => Math.sqrt(num.value));

      const squareW = ref(0);
      const sqrtW = ref(0);

      watch(
        num, // 감시 대상
        (nv, ov) => {
          console.log(`num 변경:${ov} -> ${nv}`);
          squareW.value = nv * nv; // side effect로 처리(반환X)
          sqrtW.value = Math.sqrt(nv);
        },
        { immediate: true } // 옵션 객체
      );

      return { num, squareC, squareW, sqrtC, sqrtW };
    },
  }).mount("#app");
</script>

squareC와 sqrtC는 computed에 의해 생성되며 파라미터 함수에서 return된 값이 사용된다. 따라서 template에도 squareC, sqrtC가 사용된다. 이 computed 들은 종속 값이 num이 변경되면 자동으로 값을 계산하여 반환한다.

반면 squareW와 sqrtW는 단순한 ref 이다. 그리고 watch는 3개의 파라미터를 받도록 구성되어있는데 첫 번째 파라미터는 감시 대상인 num 이다. 두 번째 파라미터에는 감시 대상 변경시 동작할 callback으로 새로운 값(nv)와 기존 값(ov)가 파라미터로 전달된다. callback 내부에서는 nv를 이용해서 squareW, sqrtW를 변경하고 있다. 즉 반환으로 처리되지 않고 있다. 세 번째 파라미터인 option 객체에서는 immediate 속성을 이용해서 최초 생성 시 부터 감시하도록 구성하였다.

결론적으로 이 둘의 동작 결과는 동일하지만 사용법에서는 많은 차이를 보여준다.

 

하지만 watch의 대상이 반응형이 아닌 경우는 감시하지 못한다. 이 경우는 해당 값을 반환하는 getter 형태로 구성해주어야 한다.

const obj = reactive({ user: { name: "hong gil dong" } });

const message = ref("");
watch(
  // obj.user.name, // 감시 불가
  () => obj.user.name, // getter 형태로 변경 필요
  (nv, ov) => {
    message.value = `정보 변경 기존: 신규: ${nv.name}, ${nv.age}, ${nv == ov}`;
  }
);

만약 감시하려는 객체 그래프의 깊이가 아주 깊다면 그 내용을 모두 감시하는데 아주 힘들 수 있다. 이런 경우 객체의 일부 값만을 감시하는게 유리할 수 있는데 이때 이런 getter를 활용할 수 있다.

 

watch를 활용한 비동기 처리

watch는 값의 변경에 따라 비동기적으로 일을 처리해야 하는 경우에 유용하다. 다음 예는 question이 변경될 때 그에 대한 답을 비동기적으로 처리하기 위한 예이다.

<script>
    const { createApp, ref, reactive, watch } = Vue;
    createApp({
      setup(props) {
        const question = ref("");
        const answer = Vue.ref("한방에 결정해주지!!");
        const img = ref("");
        const cleanup = () => {
          console.log("사전 정리 작업");
          img.value = "";
        };

        watch(question, async (newQuestion, oldQuestion, onCleanup) => {
          answer.value = "그건말이지....";
          try {
            const res = await fetch("https://yesno.wtf/api");
            const json = await res.json();
            answer.value = json.answer;
            img.value = json.image;
            // 클린업을 위한 콜백: 값을 변경하기 직전에 호출된다.
            onCleanup(cleanup);
          } catch (error) {
            answer.value = "앗! Cound not reach the API" + error;
          }
        });
        return { answer, question, img };
      },
    }).mount("#app");
  </script>
</html>

question이 변경되면 비동기로 질문에 대한 답을 가져와서 answer에 할당한다. 여기서는 onCleanup 콜백을 사용하고 있는데 사용 위치와 무관하게 값 변경을 감지하자마자 즉시 호출된다. 

computed는 기본적으로 위와 같은 일을 처리하기 어렵고 별도의 plugin을 설치해야 한다.(기본적으로 지원하지 않는 이유가 있겠죵? ㅎ)

 

deep 설정

일반적으로 반응형 data에 대한 watch는 deep:true가 기본이어서 "깊은 감시"를 하기 때문에 대상의 속성이 변경되더라도 잘 감시한다. 즉 아래와 같은 경우 obj의 user가 가진 name 값이 변경되거나 새로운 속성이 추가될 때 잘 감시된다.

const obj = reactive({ user: { name: "hong gil dong" } });
const message = ref("");

watch(
    obj.user,
    (nv, ov) => {
      message.value = `정보 변경 기존: 신규: ${nv.name}, ${nv.age}, ${nv == ov}`;
    }   
);

 

하지만 getter 함수를 통한 감시의 경우는 "얕은 감시"가 이뤄진다. 따라서 이 경우는 속성의 변경, 추가 시는 감시되지 못한다. 대신 user 자체가 변경되어야만 감시된다. 이런 상황에서 만약 속성 변경에 대한 내용까지 감시하고 싶다면 option 객체의 deep 속성을 true로 설정해서 "깊은 감시"가 이뤄지게 해야 한다.

const obj = reactive({ user: { name: "hong gil dong" } });
const message = ref("");
watch(
    //obj.user,
    () => obj.user,   // 이 경우는 얕은 감시만 진행
    (nv, ov) => {
        message.value = `정보 변경 기존: 신규: ${nv.name}, ${nv.age}, ${nv == ov}`;
    },
    { deep: true }    // 깊은 감시를 위해서 deep 속성 추가
);

 

getter 함수의 사용

그런데 getter 함수는 언제 사용할 수 있을까? obj.user로 감시하면 편한데 굳이 ()=>obj.user로 감시해야 할까? getter를 사용해야 할 때는 반응형 객체의 특정한 비 반응형 속성(obj.user.age)만 감지할 때나 연산의 결과를 감시해야 할 때이다.
watch(
    //obj.user.age,     // 그냥은 불가
    () => obj.user.age, // getter 함수 사용 필요
    (nv, ov) => {
        message3.value = `reactive 내부 속성 감시(기본):${nv}, ${nv}, ${nv == ov}`;
    },
    {
        immediate: true,
    }
);

 

method vs computed vs watch

vue에서 어떤 동작으로 결과를 만들어 내는 다양한 요소들을 학습했는데 비슷한듯 다르므로 한번 정리할 필요가 있을것 같다.

  method computed watch
결과 리턴 O / X O X (watch를 중지하는 handler 반환 가능)
함수 호출 화면에서 함수 실행 시 호출됨 화면에서 속성 선언 시, 종속값 변경 시 호출됨 참조 모델 변경 시 자동 호출됨
결과 캐싱 X O X
일반적으로 데이터가 자주 바뀌는 짧은 작업 데이터가 자주 바뀌지 않는 짧은 작업 시간이 걸리는 비동기 작업에 유리
주요 용도 여기 저기.. 가지고 있는 데이터의 조작 부가 효과에 의한 데이터 가져오기

세녀석의 용도를 대략 정리해보면 아래와 같다.(주관적 ㅎ)

  • method:
    • DOM에서 발생하는 이벤트를 처리하거나 여러 가지 공통된 기능들을 작성할 때
  • computed:
    • 필터링, 캐싱, 포멧팅
    • 기존 데이터를 이용해서 새로운 데이터를 구성할 때(많은 직원 정보가 배열에 있는데 임원급만 가져올 때)
    • 여러 개로 구성된 값을 하나로 묶어서 template에서 사용할 때(주소: 우편번호 + 도로명주소 + 상세주소)
  • watch:
    • 데이터 변경을 수신하고 무언가 부가적인 작업을 해야 할 때
    • 상위 컴포넌트가 넘겨준 prop을 감시할 때
    • 변경된 데이터를 이용해 시간이 오래 걸리는 작업/ 비 동기적인 작업을 할 때

 

watchEffect

 

watchEffect

watch()는 lazy한 감시로 소스의 변경이 발생하기 전까지 동작하지 않는 반면 watchEffect는 반응형 의존성을 자동으로 감시하면서 초기에 즉시 사이드 이펙트를 실행한다. 또한 watch가 감시 대상 소스를 지정하는 반면 watchEffect는 실행 코드에서 사용중인 반응형 객체가 변경되면 즉시 동작한다.

다음 코드를 살펴보면 watchEffect에서는 "단지" ref인 num과 name을 사용하고 있다. 즉 누구를 감시하겠다고 설정조차 하지 않는다.

const num = ref(1);
const name = ref('hong');
watchEffect(() => {
  // 단순히 반응형 데이터를 사용하는 코드
  console.log(num.value);
  console.log(name.value);
});

하지만 일단 앱을 실행시키자 마자 watchEffect의 코드가 1회 진행된다. 이후 num이나 name을 변경하면 언제나 wachEffect가 잘 호출됨을 확인할 수 있다.

 

watch vs watchEffect

watch와 watchEffect는 둘 다 반응형 의존성(반응형 데이터)변경 시 사이드 이펙트를 수행할 수 있는 기능으로 주요 차이점은 반응형 의존성을 추적하는 방식이다.

  • watch: 명시적으로 설정된 소스만을 추적하며 콜백은 소스가 실제로 변경된 경우에만 실행된다. 이 과정에서 의존성 추적(소스)과 사이드 이펙트(콜백)을 분리하여 콜백이 실행되어야 하는 시기를 상대적으로 정확히 파악할 수 있다.
  • watchEffect: 의존성 추적과 사이드 이펙트를 하나의 단계로 결합해서 작성한다. 별도의 소스 등록 없이 관련된 모든 반응형 속성을 자동으로 추적하여 간결하지만 콜백이 실행되어야 하는 시기가 좀 덜 명시적이다.

 

다음은 url이 변경될 때 ajax로 요청을 날리는 코드를 watch와 watchEffect로 처리해본 형태이다.

const url = ref('https://jsonplaceholder.typicode.com/todos/1');
const fetchTodo = async (by) => {
  const response = await fetch(url.value);
  const json = await response.json();
  console.log(`${by}: ${JSON.stringify(json)}`);
};

watch(url, () => fetchTodo('watch'), { immediate: true });

watchEffect(() => fetchTodo('watchEffect'));

url의 변경에 따라 출력되는 로그는 아래와 같다.

// 처음 실행 시
watch:       {"userId":1,"id":1,"title":"delectus aut autem","completed":false}
watchEffect: {"userId":1,"id":1,"title":"delectus aut autem","completed":false}

// url 변경 시
watch:       {"userId":1,"id":2,"title":"quis ut nam facilis et officia qui","completed":false}
watchEffect: {"userId":1,"id":2,"title":"quis ut nam facilis et officia qui","completed":false}

 

'Vue 3.0 > 02.Essentials' 카테고리의 다른 글

[vue 3] 13.app의 config 객체  (0) 2022.07.05
[vue 3] 12.Vue 객체의 라이프 사이클  (0) 2022.07.04
[vue 3] 10.computed  (0) 2022.07.02
[vue 3] 09.DOM 요소에 직접 접근  (0) 2022.06.28
[vue 3] 08.v-on  (0) 2022.06.27
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.