computed
템플릿 내에 표현식을 넣으면 편리하지만, 표현식을 사용하는 것은 간단한 연산일 때만 이용하는 것이 좋습니다. 너무 많은 연산을 템플릿 안에서 하게 되면 코드가 비대해지고 유지보수가 어려워지기 때문입니다.
다음의 템플릿 코드는 더 이상 간단하고 명료하지 않다고 느껴지는데, 표현식 내부에서 복잡한 연산을 하고 있어 message
를 역순으로 표시한다는 것을 직관적으로 알기 어렵기 때문입니다. 만약 템플릿의 다른 곳에서 message
를 역순으로 표시할 일이 더 있다면 코드는 더 복잡해질 수 밖에 없습니다.
<div id="example"> {{ message.split('').reverse().join('') }} </div>
그래서 위의 코드와 같이 복잡한 로직을 사용하는 템플릿이라면, 반드시 Vue 인스턴스의 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
의 값에 의존합니다.
console.log(vm.reversedMessage) // => '요세하녕안' vm.message = 'Goodbye' console.log(vm.reversedMessage) // => 'eybdooG'
Vue는 일반 속성처럼 computed 속성에도 템플릿에서 데이터 바인딩을 할 수 있는데, Vue는 vm.reversedMessage
가 vm.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
속성은 계산해야 하는 목표 데이터를 정의하는 방식으로 소프트웨어 공학에서 이야기하는 ‘선언형 프로그래밍’ 방식이기 때문입니다.
<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
방식과 비교해보면 조금 더 쉽게 알 수 있습니다. 일반적으로 선언형 프로그래밍이 명령형 프로그래밍보다 코드 반복이 적은 등 조금 더 우수한 것으로 평가됩니다.
<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.firstName
과 vm.lastName
이 그에 따라 업데이트되는 것을 확인할 수 있습니다.
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 옵션을 통해 데이터 변경에 반응하는 보다 일반적인 방법을 제공하는데, 이는 데이터 변경에 대한 응답으로 비동기식이나, 시간이 많이 소요되는 조작을 수행하려는 경우에 유용한 방법입니다.
<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를 사용할 수도 있습니다.