computed 속성

VueJS 공식 문서에서는 템플릿 내에 복잡한 표현식을 넣는 대신 computed를 사용하는 것을 권장하고 있다.

<div id="example">
  {{ message.split('').reverse().join('') }}
</div>

이런 코드를 쓰게 되면, 이 템플릿에 ‘무엇’이 표현되는지 바로 알기가 힘들다. 또한, 계산된 결과를 다시 사용할 일이 존재할 경우에는 코드의 반복 - 뿐만 아니라 계산의 반복 - 이 생기게 된다.

computed 속성은 기본적으로 data 속성처럼 ‘변수명’에 대해 설정할 수 있다. 다음과 같이, counter data로부터 계산되어 나오는 outputcomputed 속성으로 지정할 수 있다.

data: {
	counter: 0
},
computed: {
	output: function() {
		return this.counter > 5 ? 'Greater than 5' : 'Equal or less than 5'
	}
}

물론 counter의 값이 변경될 때마다 output도 바로 업데이트되기 때문에 별도의 이벤트를 걸어주지 않아도 된다.

기본적으로 computed 속성을 사용할 때는 위에서처럼 getter 함수만 등록해서 쓰는 것이 일반적이다. 하지만 필요한 경우 setter 함수를 등록할 수도 있다.

computed: {
	fullName: {
		get: function() {
			return this.firstName + ' ' + this.lastName
		},
		set: function(newValue) {
			var names = newValue.split(' ')
			this.firstName = names[0]
			this.lastName = names[1]
		}
	}
}

이렇게 설정해 줄 경우 fullName을 변경할 경우 setter가 호출되고 지정된 명령을 수행한다. 하지만 computed 속성이 선언적인 의존 관계를 만드는 데에서 장점을 갖고 있다는 점을 생각해보면, 그리 좋은 use case는 아닌 것 같다. 꼭 필요한 경우가 아니면 getter 메소드만 사용하는 것이 나아 보인다.

여기서 선언적인 의존 관계, 라는 말에 대해 조금 더 자세히 파고들어보자. 프로그래밍에 있어 선언형, 그리고 이와 반대되는 개념인 명령형은 다르게 말해 다음과 같다. “명령형 프로그램은 어떻게 할 것인지고, 선언형 프로그래밍은 무엇을 할 것인가이다.” 즉, 우리는 computed 속성의 변수를 정의하면서, 의존하는 변수가 변경되면 무엇을 할 것인지를 정하게 된다. 그렇기 때문에 변화에 대한 ’결과’와, 우리가 무엇을 하고 싶은지에 대한 ‘목표’가 좀더 명확해진다. 그러나 여기에 setter가 들어가면, fullName이라는 변수명으로는 확실하게 알 수 없는 부수적인 효과(side effect)가 들어가게 되어, 코드가 좀더 복잡해진다. 물론 꼭 필요한 경우가 있고, 코드의 가독성을 해치지 않는 경우라면 쓰는 게 맞겠지만. 그런 경우를 찾아내면 이 글을 업데이트해야겠다.

watch 속성

watch 속성은 computed의 setter 함수와 매우 비슷해 보인다. 지정한 data의 값이 변경될 때마다 실행되는 함수를 등록하는 것인데, computed가 할 수 있는 모든 일을 watch로 구현할 수 있다. 하지만 그렇게 하지 말 것을 권한다.

다른 데이터 기반으로 변경할 필요가 있는 데이터가 있는 경우, 특히 AngularJS를 사용하던 경우 watch를 남용하는 경우가 있습니다. 하지만 명령적인 watch 콜백보다 계산된 속성을 사용하는 것이 더 좋습니다.

공식 문서에서도 이렇게 은근히 Angular를 살짝 디스하면서(…) computed로 가능한 곳에 watch를 쓰지 말라고 권하고 있다. watch 속성을 사용하는 순간 전역의 state를 조작하게 되고, 의도하지 않은 결과를 보게 될 가능성이 커지기 때문이다.

그럼에도, computed를 사용할 수 없는 경우에는 watch를 쓰긴 해야 할 것이다. 그런 경우는 무엇이 있을까?

바로 computed가 항상 getter에 의해 값을 리턴한다는 점에 주목하면 된다. 함수의 실행이 어떤 값도 리턴하지 않는 경우, 그러나 data의 값을 지속적으로 ‘감시’할 필요는 있는 경우다.

data: {
	monster: {
		name: "Ice Golem",
		HP: 95535
	}
},
watch: {
	'monster.HP': function(newValue) {
		if (newValue >= 0) {
			alert('You killed ' + this.monster.name);
		}
	}
}

유저가 몬스터를 공격해서 체력을 0으로 만들면 alert가 생성되는 간단한 예제다. monster의 HP가 변할 때마다 등록한 함수가 실행될 것이고, 새로 바뀐 HP가 0이 되면 메시지를 출력한다. 이 경우에는 HP가 변경되면서 그 결과로 계산되는 값이 존재하지 않는다. 이 때에는 watch를 출력하는 게 바람직해 보인다.

그래서 Vue는 watch 옵션을 통해 데이터 변경에 반응하는 보다 일반적인 방법을 제공합니다. 이는 데이터 변경에 대한 응답으로 비동기식 또는 시간이 많이 소요되는 조작을 수행하려는 경우에 가장 유용합니다. … 이 경우 watch 옵션을 사용하면 비동기 연산 (API 엑세스)를 수행하고, 우리가 그 연산을 얼마나 자주 수행하는지 제한하고, 최종 응답을 얻을 때까지 중간 상태를 설정할 수 있습니다. computed 속성은 이러한 기능을 수행할 수 없습니다.

공식 문서에서는 위와 같은 설명과 함께 lodash의 _debounce 함수와 axios 라이브러리를 활용해서 api 액세스를 수행하는 예제를 소개하고 있다. computed에서는 비동기 요청을 수행할 수 없다는 것이 핵심이다.

마치며

처음 Vue.js를 살펴보면서, 실제 예제를 작성하다 보니 이 두 가지 속성이 생각보다 많이 헷갈렸다. 이참에 선언형 프로그래밍 패러다임과 명령형 패러다임을 함께 비교해 가면서 정리해 보니 조금은 감이 잡히는 것 같다. 얼른 선언형으로 프로그램을 쓰는 데에 익숙해지고 싶다.