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를 사용할 수도 있어.