• 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 reactive


    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) 함수를 등록한다.


    vue.js computed


    2. person.status 호출로 computed 함수가 실행된다. 그 과정 속에서 종속하는 getter(person.age) 가 호출된다.


    vue.js computed


    3. 호출된 getter는 본인(person.age)에 대한 종속성을 deps(종속성 관리 리스트) 에 기록한다.


    vue.js computed


    4. computed 함수(person.status)는 반환값을 받는 전체 과정은 다음과 같다. 


    vue.js computed


    전체 코드는 다음을 참고하면 된다.

    http://jsbin.com/vevupup/embed?js,console


    원리는 간단하다.

    person.age 에 접근할 수 있는 경로는 age 에 대한 getter 이다.

    자신의 경로인 getter 접근한다는 것 자체를 자신(age)에게 종속하고 있다고 판단해 기록한다.

    그 후, 자신이 변경된다면 기록된 정보를 통해 종속하는 대상에게 자신의 변경을 알리는 것이다.


    computed 속성은 주의해서 사용해야한다.

    이해하지 못하고 쓴다면, 무분별한 종속에 의해 예기치 못한 오류를 가져올 수 있다.

    하나의 computed 에는 하나의 종속성을 가지게 하는 것이 좋다.

    반응형

    댓글

Designed by Tistory.