Vue computed와 watch 속성

복잡한 연산을 위한 두 속성과 차이점

computed

템플릿 내에 표현식을 넣으면 편리하지만, 표현식을 사용하는 것은 간단한 연산일 때만 이용하는 것이 좋아. 너무 많은 연산을 템플릿 안에서 하게 되면 코드가 비대해지고 유지보수가 어려워져.

다음의 템플릿 코드는 더 이상 간단하고 명료하지 않다고 느껴지는데, 표현식 내부에서 복잡한 연산을 하고 있어 message를 역순으로 표시한다는 것을 직관적으로 알기 어렵기 때문이야. 만약 템플릿의 다른 곳에서 message를 역순으로 표시할 일이 더 있다면 코드는 더 복잡해질 수 밖에 없어.

표현식 내부에서 복잡한 연산을 수행
<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

그래서 위의 코드와 같이 복잡한 로직을 사용하는 템플릿이라면, 반드시 Vue 인스턴스의 computed 속성을 사용하는 것이 좋아.

computed 기본 사용법

computed 샘플 예제
<div id="example">
  <p>원본 메시지: "{{ message }}"</p>
  <p>역순으로 표시한 메시지: "{{ reversedMessage }}"</p>
</div>

<script>
var vm = new Vue({
  el: '#example',
  data: {
    message: '안녕하세요'
  },
  computed: {
    // 계산된 getter
    reversedMessage: function () {
      // `this`는 vm 인스턴스를 가리킴
      return this.message.split('').reverse().join('')
    }
  }
})
</script>

위 예제에서는 computed 속성에 reversedMessage를 선언했는데, reversedMessage에 작성한 함수는 다음과 같이 vm.reversedMessage속성에 대한 getter 함수로 사용되고, vm.reversedMessage의 값은 항상 vm.message의 값에 의존해.

vm 인스턴스의 reversedMessage 함수 테스트
console.log(vm.reversedMessage) // => '요세하녕안'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'

Vue는 일반 속성처럼 computed 속성에도 템플릿에서 데이터 바인딩을 할 수 있는데, Vue는 vm.reversedMessagevm.message에 의존하는 것을 알고 있기 때문에 vm.message가 바뀔 때 vm.reversedMessage에 의존하는 바인딩을 모두 업데이트하게 돼.

여기서 중요한 것은 선언적인 의존 관계를 만들었다는 것인데, computed 속성의 getter 함수는 사이드 이펙트가 없기 때문에 코드를 테스트하거나 이해하기 쉽다는 장점이 있어.

computed 속성의 캐싱과 메소드

computed로 표현식에 데이터 바인딩을 한 것 처럼, 메소드를 추가한 후 표현식에서 메소드를 호출해도 같은 결과를 얻을 수 있어.

표현식에서 메소드 호출하기
<div id="example">
    <p>뒤집힌 메시지: "{{ reversedMessage() }}"</p>
</div>

<script>
var vm = new Vue({
    el: '#example',
    data: {
        message: '안녕하세요'
    },
    methods: {
      reversedMessage: function () {
        return this.message.split('').reverse().join('')
      }
    }
})
</script>

computed 속성을 사용하는 방법과 methods에 같은 함수를 정의해서 사용하는 방법은 모두 동일한 결과를 얻을 수 있는 방법이야. 하지만 computed 속성은 종속 대상을 따라 캐싱이 된다는 차이가 있는데, computed 속성은 해당 속성이 종속된 대상이 변경될 때만 함수를 실행하게 돼.

computed 속성을 사용하는 경우에는 message가 변경되지 않는 한, computed 속성에 있는 reversedMessage를 여러 번 요청해도 계산을 다시 하지 않고, 대신 캐싱되어 있던 계산 결과를 즉시 반환하게 돼.

computed 속성의 이런 특징은 다음 코드의 Date.now() 처럼 아무 곳에도 의존하지 않는 computed 속성의 경우에는 절대로 업데이트되지 않기 때문에 주의할 필요가 있어.

절대로 업데이트 되지 않는 코드
computed: {
  now: function () {
    return Date.now()
  }
}

반면 메소드를 호출하는 경우에는 렌더링을 다시 할 때마다 항상 함수를 실행하기 때문에 위와 같은 코드는 메소드로 정의해 사용하는 것이 좋아.

캐싱의 용도

캐싱은 왜 필요한 걸까? 만약 거대한 배열을 반복해서 다루고 많은 계산을 하기 때문에 계산에 시간이 많이 걸리는 A라는 computed 속성이 있는 경우라면 computed의 캐싱 기능은 아주 유용하게 사용될 수 있어.

A라는 computed 속성은 계산에 시간이 많이 걸리는데, 이 A라는 속성에 의존하는 다른 computed 속성 값이 존재하는 경우에 캐싱을 하지 않으면 A라는 속성은 getter 함수를 꼭 필요한 것보다 더 많이 실행하게 될꺼야.

캐싱은 이렇게 리소스가 많이 들어가는 비싼 계산의 결과를 저장하고, 이를 다시 재활용함으로써 시스템의 리소스 낭비를 막을 수 있다는 장점이 있어. 반대로 캐싱이 필요하지 않다면 computed 속성 대신 메소드를 사용하면 되겠지?

computed와 watch

Vue는 Vue 인스턴스의 데이터 변경을 관찰하고 이에 반응하는 watch 속성을 제공하고 있는데, 일반적으로 명령적인 watch 콜백보다는 computed 속성을 사용하는 것이 더 좋다고 해.

watch 속성은 감시할 데이터를 지정하고 그 데이터가 바뀌면 지정된 함수를 실행하는 방식으로 소프트웨어 공학에서는 이런 방식을 ‘명령형 프로그래밍’ 방식이지만, computed 속성은 계산해야 하는 목표 데이터를 정의하는 방식으로 소프트웨어 공학에서 이야기하는 ‘선언형 프로그래밍’ 방식이기 때문이야.

watch 방식의 코드
<div id="demo">{{ fullName }}</div>

<script>
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})
</script>

위 코드와 같이 watch 방식은 명령형이고 코드를 반복하는데, 다음의 computed 방식과 비교해보면 조금 더 쉽게 알 수 있을 꺼야. 일반적으로 선언형 프로그래밍이 명령형 프로그래밍보다 코드 반복이 적은 등 조금 더 우수한 것으로 평가된다고 해.

computed 방식의 코드
<div id="demo">{{ fullName }}</div>

<script>
var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})
</script>

computed 속성과 setter 함수

computed 속성은 기본적으로 getter 함수만 가지고 있지만, 필요한 경우에는 setter 함수를 만들어서 쓸 수 있어. 다음과 같이 set 함수를 추가하고 vm.fullName = 'John Doe'를 실행하면 설정자가 호출되고 vm.firstNamevm.lastName이 그에 따라 업데이트되는 것을 확인할 수 있어.

computed 속성에서 setter 함수 만들기
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

watch

앞에서도 살펴본 것 처럼 대부분의 경우에는 computed 속성을 사용하는 것이 더 적합하지만, 상황에 따라 사용자가 만든 감시자가 필요한 경우가 있을 수 있어. 그래서 Vue는 watch 옵션을 통해 데이터 변경에 반응하는 보다 일반적인 방법을 제공하는데, 이는 데이터 변경에 대한 응답으로 비동기식이나, 시간이 많이 소요되는 조작을 수행하려는 경우에 유용한 방법이야.

watch 활용 예제
<div id="watch-example">
  <p>
    yes/no 질문을 물어보세요:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>

<script src="https://unpkg.com/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://unpkg.com/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
  el: '#watch-example',
  data: {
    question: '',
    answer: '질문을 하기 전까지는 대답할 수 없습니다.'
  },
  watch: {
    // 질문이 변경될 때 마다 기능 실행
    question: function (newQuestion) {
      this.answer = '입력을 기다리는 중...'
      this.debouncedGetAnswer()
    }
  },
  created: function () {
    this.debouncedGetAnswer = _.debounce(this.getAnswer, 500)
  },
  methods: {
    getAnswer: function () {
      if (this.question.indexOf('?') === -1) {
        this.answer = '질문에는 일반적으로 물음표가 포함 됩니다. ;-)'
        return
      }
      this.answer = '생각중...'
      var vm = this
      axios.get('https://yesno.wtf/api')
        .then(function (response) {
          vm.answer = _.capitalize(response.data.answer)
        })
        .catch(function (error) {
          vm.answer = '에러! API 요청에 오류가 있습니다. ' + error
        })
    }
  }
})
</script>

위 코드의 created 속성에 있는 _.debounce라는 메소드는 lodash가 제공하는 기능으로, 특히 시간이 많이 소요되는 작업을 실행할 수 있는 빈도를 제한하는 메소드인데, 코드에서는 yesno.wtf/api에 액세스 하는 빈도를 제한하고, 사용자가 ajax 요청을 하기 전에 타이핑을 완전히 마칠 때까지 기다리는 동작을 구현하기 위해 사용됐어.

watch 속성은 API 엑세스와 같은 비동기 연산을 수행하고, 그 연산을 얼마나 자주 수행하는지 제한하고, 최종 응답을 얻을 때까지 중간 상태를 설정할 수 있어. 하지만 computed 속성은 이런 기능은 수행할 수 없고, watch 대신 명령형 vm.$watch API를 사용할 수도 있어.

답글 남기기