-
Vue.js: computed 는 어떻게 동작하는가? :: 마이구미Vue.js 2018. 4. 21. 15:11반응형
이 글은 Vue.js 의 computed 속성이 어떻게 동작하는지에 대해 다뤄본다.
computed 속성의 동작 원리를 알기 위해서는 Vue 의 반응형 시스템에 대한 사전 지식이 필요하다.
큰 범위에서는 Vue 의 반응형 시스템의 관한 주제가 된다.
참고한 링크의 번역이 아닌 본인이 재구성한 점을 참고바란다.
참고 링크 - https://skyronic.com/blog/vuejs-internals-computed-properties
See the Pen computed in vue.js by leejunghyun (@mygumi) on CodePen.
위 코드는 버튼을 클릭할때마다 count 변수가 1씩 증가한다. (네트워크 환경에 따라 로딩이 지연될 수 있으니 조금만 기다려주길..)
결과적으로 각각 다른 3가지 방식이지만 클릭할때마다 모두 즉각적으로 반응해 화면을 갱신하고 있다.
data() { return { count: 0 } }, computed: { computedCountReturn() { return 'computedCountReturn - ' + this.count; }, computedCountNotReturn() { let text = 'computedCountNotReturn - ' + this.count; return text; }, },
위와 같은 결과로 인해 한번쯤은 고민할 수 있다.
"computed 는 어떻게 동작하길래 이러한 결과가 가능한가?"
더 나아간다면,
"computed 는 어떻게 종속성을 가지게 될까?"
이것이 가능한 이유는 computed 속성의 함수들이 count 에 종속성(Dependency)을 가지기 때문이다.(count 에 의존하고 있다)
이것이 글의 핵심이자 결론이다.
지금 당장은 몰라도 된다.
글을 끝까지 읽어보면 충분히 이해할 수 있을 것이다.
* 종속성이란 실체 간의 관계가 얼마나 연관되어 있는가를 의미한다. 흔히 "A는 B에 의존한다", "종속한다", "의존성을 가진다" 같은 의미이다.
조금 더 넓은 관점에서 보면 Vue 의 반응형 시스템(reactive system) 덕분에 위와 같은 결과를 낼 수 있다.
공식 문서에서 반응형 시스템에 대해 표현한 그림은 다음과 같다.
Vue.js 에서 일반 객체는 "Observable" 이라고 불리는 형태로 변환하는 기본 구성을 가진다.
*Observable 은 옵서버(Observer) 패턴을 이해하면 된다.
이러한 흐름을 통해 상태 변화를 추적할 수 있게 된다.
내부의 코드 형태로는 크게 Observer, getter, setter 를 통해 가능하게 된다.
실질적인 흐름은 getter와 setter 의 용도를 이해하는 것만으로도 충분하다.
위 그림의 이해는 비교적 간단하다.
getter 를 통해 종속성을 추적하고, setter 를 통해 변경을 알리게 된다.
이에 Watcher 는 리렌더링을 위해 트리거하는 모습을 볼 수 있다.
위 과정 속에 computed 속성의 동작 원리가 자연스럽게 내재되어있다.
지금부터는 위 과정의 흐름을 설명하기 위해 간단한 버전의 코드를 작성할 것이다.
즉, 어떻게 getter 를 통해 종속성을 추적하고, setter 를 통해 변경을 알리는 지 코드를 통해 확인해보자.
만약 상황에 있어, 사람이 존재하는데 나이에 따라 미성년자와 성인을 분류하고 싶다.
또한 나이가 변할때마다 이에 따른 분류도 변화해야한다.
이러한 상황일 때 Vue.js 에서는 다음과 같이 만들것이다.
data() { return { person: { age: 18,
country: "Brazil" } } }, computed: { kind() { if (this.person.age > 18) { return 'Adult'; } else { return 'Minor'; } } }
위 코드는 age 가 변하게 되면 kind 함수가 호출되어 age 에 따라 값을 반환하게 된다.
위와 같은 시나리오에서 data 와 computed 속성이 순수 자바스크립트로 어떠한 방식으로 구현할 수 있는지 확인해본다.
크게 2가지 함수를 구현한다.
- defineReactive - reactive 속성을 정의하는 함수
- defineComputed - computed 속성을 정의하는 함수
우선 reactive 속성을 정의하는 defineReactive 함수를 정의해보자.
여기서 reactive 속성은 Vue 인스턴스의 data 속성을 말할 수 있다.
function defineReactive(obj, key, val) {Object.defineProperty(obj, key, {get: function() {return val;},set: function(newValue) {val = newValue;}});}var person = {};defineReactive(person, "age", 25);defineReactive(person, "country", "Brazil");// 이후에 computed 로 정의될 함수function kind() {// getter(person.age)if (person.age < 18) {return "minor";} else {return "adult";}}// setter(person.country)person.country = "Russia";cs 위처럼 객체의 각 프로퍼티에 직접 접근하게 하지 않고, getter 와 setter 를 가지게한다.
실제 Vue 내부 코드에서도 Object.defineProperty 를 통해 getter 와 setter 를 변환한다.
그리고 kind 함수는 computed 함수를 뜻한다.
즉, 이를 위해 computed 속성을 정의하는 defineComputed 함수를 정의하는 함수를 만들어보자.
function defineComputed(obj, key, computeFunc, updateCallback) {Object.defineProperty(obj, key, {get: function() {return computeFunc();},set: function() {// computed 속성은 set 기능이 존재하지 않는다.}});}defineComputed(person, // 대상 객체"status", // computed 속성을 위한 객체 속성function() {// 실제로 실행된 결과를 반환하는 함수(kind)console.log("status getter called");if (person.age < 18) {return "minor";} else {return "adult";}},function(newValue) {console.log("status has changed to", newValue);});console.log("The person's status is: ", person.status);cs defineComputed 를 통해 person.status 는 computed 함수(kind)로 정의된다.
그 결과 person.status 를 접근하면 원하는 반환값을 얻을 수 있다.
하지만 문제점은 다음과 같다.
- 매번 status 와 같은 getter 를 접근해야 computed 함수를 호출할 수 있다.
- 종속성의 대상(age)의 변화를 추적할 수 없다. 즉, 업데이트 시기를 알 수 없다.
실제 computed 함수였다면, 종속하고 있는 person.age(reactive 속성) 가 변한다면, computed 함수가 호출되어 minor 또는 adult 반환 값을 보내준다.
하지만 여기서는 직접 접근을 해야하고, 업데이트 시기도 알 수 없다.
단순히 함수를 getter 로 접근하게 만든 것뿐이다.
우리는 다음과 같은 결과를 바라고 있다.
person.age = 16;
// person.status 는 "Minor" 변경됨;
person.age = 30
// person.status 는 "Adult" 변경됨;
위와 같은 결과를 내기 위해서는 추적 기능이 필요하다.
이를 위해 Dep 이라는 글로벌 객체를 선언한다.
var Dep = {target: null};cs 이는 종속성을 추적하기 위한 종속성 추적기(Dependency Tracker) 라고 보면 된다.
추적기 추가로 인해 defineComputed 를 업데이트해보자.
function defineComputed (obj, key, computeFunc, callbackFunc) {let onDependencyUpdated = function () {// TODO}Object.defineProperty (obj, key, {get: function () {// 이 함수는 호출되면 종속성을 가지는 대상에 설정된다.Dep.target = onDependencyUpdated;let value = computeFunc ();Dep.target = null;return value;},set: function () {}})}cs computed 함수를 글로벌 추적기에 등록한 후, 종속하고 있는 대상의 getter(age) 를 통해 종속성을 등록하게 된다.
관련된 defineReactive 함수를 업데이트해보자.
function defineReactive(obj, key, val) {// 여기(age or country or ...)에 종속성을 가지는 모든 computed 리스트const deps = [];Object.defineProperty(obj, key, {get: function() {if (Dep.target && deps.indexOf(Dep.target) == -1) {// 종속성 추가deps.push(target);}return val;},set: function(newValue) {val = newValue;// 여기에 종속성을 가지는 모든 computed 에게 변경을 알림deps.forEach(changeFunction => {// 업데이트deps[i]();});}});}cs 결과적으로 computed 함수는 person.age 에 종속하게 되었다.
그로 인해, age 변화에 있어, comptued 함수가 호출되는 일이 발생한다.
하지만 person.country 에 종속하고 있는 것은 없기에 country 변화에 있어, 다른 별도의 호출은 발생하지 않는다.
단계별로 표현하면 다음과 같다.
1. person.status 를 호출하여 Dep.target 에 callback(onDependencyUpdated) 함수를 등록한다.
2. person.status 호출로 computed 함수가 실행된다. 그 과정 속에서 종속하는 getter(person.age) 가 호출된다.
3. 호출된 getter는 본인(person.age)에 대한 종속성을 deps(종속성 관리 리스트) 에 기록한다.
4. computed 함수(person.status)는 반환값을 받는 전체 과정은 다음과 같다.
전체 코드는 다음을 참고하면 된다.
http://jsbin.com/vevupup/embed?js,console
원리는 간단하다.
person.age 에 접근할 수 있는 경로는 age 에 대한 getter 이다.
자신의 경로인 getter 접근한다는 것 자체를 자신(age)에게 종속하고 있다고 판단해 기록한다.
그 후, 자신이 변경된다면 기록된 정보를 통해 종속하는 대상에게 자신의 변경을 알리는 것이다.
computed 속성은 주의해서 사용해야한다.
이해하지 못하고 쓴다면, 무분별한 종속에 의해 예기치 못한 오류를 가져올 수 있다.
하나의 computed 에는 하나의 종속성을 가지게 하는 것이 좋다.
반응형'Vue.js' 카테고리의 다른 글
[Nuxt.js] Vuetify Custom Icon 적용하기 :: 마이구미 (0) 2021.08.28 Vue.js: 트랜지션(transition) 어떻게 사용하는가? :: 마이구미 (0) 2018.08.03 Vue.js Mixins: 믹스인은 왜 필요한가? :: 마이구미 (0) 2017.12.09 Vue.js Slot: 슬롯은 왜 필요한가? :: 마이구미 (4) 2017.12.02 비 부모-자식 통신 eventBus :: 마이구미 (0) 2017.10.26