본문으로 건너뛰기

양방향 데이터 바인딩의 3가지 구현 방식

무료2017-02-12#JS#双向数据绑定#data binding#angular dirty checking#vue data binding#angular data binding#js数据绑定

데이터 모델 제어, setter를 이용한 변화 감지, 더티 체킹(Dirty Checking) 등 3가지 서로 다른 방식으로 양방향 데이터 바인딩을 구현합니다.

서문

상태가 없던 시절부터 직접 상태를 관리하고 DOM을 조작해 뷰를 업데이트하던 시절을 거쳐, 양방향 데이터 바인딩에 이르기까지 다양한 프론트엔드 솔루션은 데이터와 뷰 사이의 연결 방식을 단순화하기 위해 끊임없이 발전해 왔습니다. 현재의 추세는 DOM의 비중을 줄이고 데이터 상태를 강화하며, "진정한" 로직에 집중하는 방향으로 나아가고 있습니다.

1. 이정표

Server-Side Rendering: Reset The Universe

There is no change. The universe is immutable.

상태가 없으며 프론트엔드의 역할이 미미하던 시절입니다.

뷰 제어와 로직 기능이 모두 서버에서 제공되었고, 프론트엔드는 폼 검증과 같은 간단한 인터랙션만 처리하며 사용자의 행동을 서버에 직접 전달했습니다.

프론트엔드: 다음 단계 버튼 눌렀어요. 프론트엔드 업무 끝.
서버: 알겠어, 여기 다음 페이지야.

로직 상태가 모두 서버에 있었기에 프론트엔드 코드는 단 한 줄 if (isValid) form.submit(); else alert('invalid')뿐이었습니다.

P.S. SSR을 본 지 정말 오래되었네요...

First-gen JS: Manual Re-rendering

I have no idea what I should re-render. You figure it out.

프론트엔드가 상태를 관리하고 DOM을 수동으로 조작하여 뷰를 업데이트하던 시절입니다.

프론트엔드 코드가 많아지면서 상태 관리가 필요해졌고, 이 시기의 프론트엔드 프레임워크(예: Backbone.js, Ext JS, Dojo)들은 "베스트 프랙티스"에 따라 MVC를 분리하고자 했습니다.

프론트엔드 프레임워크: 데이터랑 템플릿을 주면 내가 렌더링해 줄게.
개발자: 알았어, 여기.
프론트엔드 프레임워크: 사용자가 방금 버튼 눌렀는데, 어디를 다시 렌더링해야 할지 모르겠어. 네가 직접 해.
개발자: document.getXXX().xxx()

프레임워크가 데이터와 뷰를 분리해 주긴 했지만, 이후의 상태 업데이트는 수동으로 DOM을 조작해야 했습니다. 프레임워크가 최초 렌더링만 담당하고 상태 변화를 추적하거나 감시하지 않았기 때문입니다.

Ember.js: Data Binding

I know exactly what changed and what should be re-rendered because I control your models and views.

수동 DOM 조작의 번거로움을 해결하기 위한 방안이 등장했습니다.

프레임워크가 DOM 조작을 약화시키고 데이터에만 집중하기 위해 데이터와 뷰 사이의 매핑 관계를 파악하고자 했습니다.

프론트엔드 프레임워크: 데이터랑 템플릿을 주면 내가 렌더링해 줄게.
개발자: 알았어, 여기.
프론트엔드 프레임워크: 이게 아니고, 데이터는 내가 주는 상자에 담아야 해.
개발자: 알았어, 여기.
개발자: 나 데이터 수정했어.
프론트엔드 프레임워크: 확인했어, 페이지 내용 업데이트 완료.

프레임워크가 데이터를 감싸는 데이터 모델을 제공하여, 이후의 추가, 삭제, 수정은 반드시 프레임워크 API를 거치도록 했습니다. 덕분에 프레임워크는 데이터가 변했음을 알고 해당 뷰를 업데이트할 수 있었으며, 데이터 변화를 감시할 수 있게 되었습니다.

AngularJS: Dirty Checking

I have no idea what changed, so I'll just check everything that may need updating.

프레임워크가 제공하는 데이터 모델을 쓰는 번거로움을 피하고 상태 변화를 직접 추적하려는 시도입니다.

프론트엔드 프레임워크: 데이터랑 템플릿을 주면 내가 렌더링해 줄게.
개발자: 알았어, 여기.
개발자: data.name을 감시하다가 바뀌면 h1 텍스트를 업데이트해 줘.
프론트엔드 프레임워크: 알았어.
프론트엔드 프레임워크: 바뀌었네, 어떻게 할지 알아. 이전엔 xxx였는데 지금은 aaa니까 h1 내용을 aaa로 바꿀게.

프레임워크가 이전 값을 기록해 두었다가 데이터가 변했을 법한 시점(DOM 조작이나 수동 알림 시)에 최신 값과 이전 값을 비교합니다. 값이 다르면 최신 값으로 뷰를 업데이트하여 뷰와 데이터의 상태를 일치시킵니다.

React: Virtual DOM

더티 체킹의 비효율성(작은 변화에도 전체를 확인해야 함)을 극복하기 위한 다른 방식입니다.

I have no idea what changed so I'll just re-render everything and see what's different now.

번거로운 데이터 모델도 싫고 무식한 더티 체킹도 마음에 들지 않아, 데이터 변화를 감시하지 않는 방법을 찾습니다.

프론트엔드 프레임워크: 데이터랑 템플릿을 주면 내가 렌더링해 줄게.
개발자: 알았어, 여기.
개발자: 나 데이터 바꿨어, 뷰 업데이트 좀 해 줘.
프론트엔드 프레임워크: 알았어, 여기서부터 아래로 알릴게. 자식들이 직접 어떻게 업데이트할지 판단할 거야. 간단한 거(속성 업데이트)는 직접 하고, 복잡한 거(구조 업데이트)는 나한테 말해 줘.
프론트엔드 프레임워크: 오, span 하나 삭제하고 li 2개 삽입하면 되네. 완료.

프레임워크가 언제 데이터가 변했는지 먼저 추적하지 않고, 개발자가 프레임워크에 상태 변화를 직접 알려야 합니다. 그러면 프레임워크는 하위 구조를 훑어보며 무엇을 해야 할지 파악합니다.

Vue

데이터 변화를 감시하는 가장 직관적이고 강력한 방법은 setter를 정의하는 것이 아닐까요?

비록 setter 정의만으로는 모든 데이터 변화를 감시할 수 없지만 대부분의 상황을 커버할 수 있습니다. 예외적인 상황은 제약을 두어 해결합니다.

프론트엔드 프레임워크: 데이터랑 템플릿을 주면 내가 렌더링해 줄게.
개발자: 알았어, 여기.
개발자: data.value = 'new value'
프론트엔드 프레임워크: setter가 값이 바뀌었다고 하네, 뷰를 업데이트할게.
개발자: data.newKey = 'new key value'
프론트엔드 프레임워크: (잠잠)
개발자: $set('newKey', 'new key value')
프론트엔드 프레임워크: 새 속성이 추가되었네, 뷰를 업데이트할게.

프레임워크가 데이터 변화를 추적하여 뷰를 업데이트하고, 일부 상황에서만 수동으로 상태 변화를 알리도록 합니다.

2. setter를 이용한 변화 감지

Vue가 이 방식을 사용하며 기본 아이디어는 다음과 같습니다.

  1. 데이터-뷰: data를 순회하며 setter를 정의하고, setter 내부에서 뷰를 업데이트(update view)합니다.
  2. 뷰-데이터: input 등 양방향 바인딩이 필요한 요소의 이벤트를 감시하고, 핸들러(handler) 내부에서 데이터를 업데이트(update data)합니다.

뷰-데이��� 처리는 선택의 여지 없이 명확합니다. 데이터-뷰 처리는 setter가 대응할 수 없는 상황을 고려해야 합니다.

  • 객체에 새 속성을 추가할 때(data.newKey = 'value') setter가 감지하지 못합니다.
  • delete로 기존 속성을 삭제할 때 setter가 감지하지 못합니다.
  • 배열의 변화를 감지하지 못합니다.

첫 번째 문제는 ES6 Proxy를 사용하기 전까지는 해결할 수 없으며 (Object.observe(), Array.observe()는 폐기됨), 따라서 프레임워크가 추가 알림 API를 제공해야 합니다. 예:

var data = {a: 1};
var vm = new Vue({
  data: data
});
// 추가 API를 통해 프레임워크에 새 속성이 추가되었음을 알림
vm.$set('b', 2);

두 번째 문제는 그리 중요하지 않습니다. delete가 필요한 상황은 드물며, 값을 undefined로 할당하고 약간의 추가 판단 로직을 넣으면 delete를 피할 수 있습니다.

세 번째 문제는 꽤 심각하며 해결하기 쉽지 않습니다. Vue는 원본 Array 메서드를 변조(배열 내용을 바꾸는 메서드들을 가로챔)하여 이를 보완합니다.

/**
 * Intercept mutating methods and emit events
 */
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    //...
    // 감시 코드 주입 부분
    if (inserted) ob.observeArray(inserted)
    // 변화 알림
    ob.dep.notify()
    return result
  })
})

출처: https://github.com/vuejs/vue/blob/dev/src/core/observer/array.js

이렇게 배열 메서드를 통해 내용이 바뀌는 경우는 해결했지만 여전히 두 가지 문제가 남습니다.

  • arr.length=3과 같이 길이를 직접 수정하여 발생하는 변화를 감지하지 못합니다.
  • arr[0] = 1과 같이 인덱스를 통해 값을 직접 수정하는 변화를 감지하지 못합니다.

객체처럼 lengthsetter를 정의하면 어떨까요? 안 됩니다. 배열의 lengthObject.defineProperty()로 변조할 수 없기 때문입니다.

var a = [1, 2, 3];
Object.defineProperty(a, 'length', {
    set(newValue) {
        console.log('change to ' + newValue);
        return newValue;
    },
    get: x => x
});
// Uncaught TypeError: Cannot redefine property: length

length를 통한 배열 수정은 다른 방식(예: splice)으로 대체할 수 있으므로 아주 치명적이진 않으니 일단 넘어갑니다.

인덱스를 통한 배열 원소 수정은 자주 쓰이는 방식이므로 반드시 처리해야 하며, 따라서 배열의 특정 원소 수정을 지원하는 추가 API를 제공해야 합니다.

// 아주 오래전 버전에서는 감시 중인 배열 프로토타입에 $set()과 $remove()를 추가했었습니다.
// 지금은 사라진 것 같고, 전역 Vue.set()을 사용합니다.
Vue.set(vm.arr, 0, 'setted')

이렇게 해야 데이터 변화 감지가 어느 정도 완비됩니다.

구현 시도

배열은 다소 번거로우니 일단 제외하고, 여기서는 input, div와 간단한 데이터 객체 간의 양방향 바인딩을 구현해 보겠습니다.

핵심인 setter 감시 부분입니다.

var bind = function(node, data) {
    var key = node.getAttribute('data-bind');
    if (!key) return console.error('no data-bind key');

    // cache
    var cache = cacheData(data);

    // data to view
    if (cache._cachedNodes) {
        cache._cachedNodes.push(node);
    }
    else {
        cache._cachedNodes = [node];
        // setter: data to view
        Object.defineProperty(data, key, {
            enumerable: true,
            set: function(newValue) {
                cache[key] = newValue;
                // 뷰 업데이트
                updateView(cache, newValue);
                return cache[key];
            },
            get: function() {
                return cache[key];
            }
        });
    }

    // 초기 뷰 설정
    updateView(cache, cache[key]);
};
// 바인딩 수행
bind($input, data);
bind($output, data);

setter 내부에서 뷰를 업데이트(update view)하여 데이터-뷰 바인딩을 완성합니다.

그다음 input 요소의 input 이벤트를 감시하여 뷰-데이터 바인딩을 완성합니다.

// 이벤트: 뷰에서 데이터로
$input.addEventListener('input', function() {
    data[$input.getAttribute('data-bind')] = this.value;
});

물론 실제 상황에서는 input, checkbox, select 등 상태에 영향을 미치는 다양한 뷰 변화를 고려해야 합니다.

데이터를 직접 수정하면 setter가 이를 감지하여 뷰를 업데이트합니다.

// 수동으로 변수 값 수정
$('#btn').onclick = function() {
    data.value = 'updated value ' + Date.now();
};

전체 데모 주소: http://ayqy.net/temp/data-binding/setter.html

3. 데이터 모델 제공

Ember.js가 이 방식을 사용합니다.

프레임워크가 실제 데이터를 감싸는 데이터 모델 세트를 제공하고, 이후의 업데이트는 반드시 데이터 모델 API를 거치도록 하여 프레임워크가 모든 데이터 변화를 파악하고 데이터-뷰 바인딩을 수행합니다. 다소 오래된 방식이며 사용하기 번거롭기 때문에 특별히 더 언급할 것은 없습니다.

구현 시도

마찬가지로 input, div와 문자열 간의 양방향 바인딩을 구현해 보겠습니다.

먼저 데이터 모델을 제공하고 모델 내부에서 뷰를 업데이트합니다.

// 데이터 모델 제공
var MString = function(str) {
    this.value = str;
    this.nodes = [];
};
MString.prototype = {
    bindTo: function(node) {
        this.nodes.push(node);
        this.updateView();
    },
    updateView: function() {
        var VALUE_NODES = ['INPUT', 'TEXTAREA'];

        var nodesLen = this.nodes.length;
        if (nodesLen > 0) {
            for (var i = 0; i < nodesLen; i++) {
                if (VALUE_NODES.indexOf(this.nodes[i].tagName) !== -1) {
                    // input에 값을 할당할 때 커서가 끝으로 튀는 현상 방지
                    if (this.nodes[i].value !== this.value) {
                        this.nodes[i].value = this.value;
                    }
                }
                else {
                    this.nodes[i].innerText = this.value;
                }
            }
        }
    },
    set: function(str) {
        if (str !== this.value) {
            this.value = str;
            // 뷰 업데이트
            this.updateView();
        }
    },
    get: function() {
        return this.value;
    }
};

데이터 모델을 제공하는 것은 사실상 setter를 정의하는 것과 같으며, 변화를 감시한다는 측면에서 데이터 모델을 제공하는 방식은 확실한 방법입니다. 지원되는 데이터 조작은 모두 감시할 수 있고 지원되지 않는 방식으로는 변화를 일으킬 수 없으므로 처리가 매우 간단합니다.

그다음 연결 고리를 만듭니다.

// setter: 데이터에서 뷰로
data[$input.getAttribute('data-bind')].bindTo($input);
data[$output.getAttribute('data-bind')].bindTo($output);

// 이벤트: 뷰에서 데이터로
$input.addEventListener('input', function() {
    data[$input.getAttribute('data-bind')].set(this.value);
});

이후의 데이터 업데이트는 데이터 모델 API를 거쳐야 합니다.

// 수동으로 변수 값 수정
$('#btn').onclick = function() {
    data.value = 'updated value ' + Date.now();
};

전체 데모 주소: http://ayqy.net/temp/data-binding/model.html

4. 더티 체킹(Dirty Checking)

Angular가 이 방식을 사용합니다.

데이터 변화를 직접 감시하지 않고, 수시로 데이터가 변했는지 확인하여 변했다면 해당 뷰를 업데이트합니다.

여기서 문제는 다음과 같습니다.

  • 언제 확인하는가?

    데이터가 변할 법한 시점에 매번 확인합니다. 예를 들어 DOM 조작 후, 인터랙션 이벤트 발생 후 등 데이터 불일치가 의심될 때 전체를 훑어보며 변했는지 확인합니다.

  • 어디가 변했는지 어떻게 아는가?

    어디가 변했는지 모르기 때문에 뷰에 바인딩된 모든 데이터를 하나하나 확인해야 합니다. 물론 "모든 데이터"가 페이지 전체를 의미하는 것은 아니고 컴포넌트 레벨($scope) 단위이므로 성능이 아주 뛰어나진 않아도 대부분의 상황에서는 큰 무리가 없습니다.

만약 데이터를 수동으로 수정하고 즉시 뷰에 반영되기를 원하는데, 더티 체킹 메커니즘이 나중에 실행될 예정이라면 불편함이 발생하므로 더티 체킹을 수동으로 실행해 주어야 합니다.

구현 시도

Angular 스타일을 모방하여 간단하게 만들어 보겠습니다.

var Scope = function() {
    this.$$watchers = [];
};

Scope.prototype.$watch = function(watchExp, listener) {
    this.$$watchers.push({
        watchExp: watchExp,
        listener: listener || function() {}
        // 이후 더티 체킹 과정에서 oldValue를 보관할 last 속성이 추가됩니다.
    });
};

Scope.prototype.$digest = function() {
    var dirty;

    do {
        dirty = false;

        // watcher를 순회하며 last가 변했는지 확인
        for(var i = 0; i < this.$$watchers.length; i++) {
            // watchExp 메서드를 이용해 값을 다시 가져옴
            // key를 직접 쓰지 않고 메서드를 쓰는 게 더 유연함 (watchExp 내부에서 새 값을 가공할 수 있음)
            var newValue = this.$$watchers[i].watchExp(),
                oldValue = this.$$watchers[i].last;

            if(oldValue !== newValue) {
                // 기록해 둔 last가 변했으므로 콜백 실행
                // 첫 digest 시 last는 undefined이므로 최소 한 번은 listener가 실행됨
                this.$$watchers[i].listener(newValue, oldValue);
                // listener 실행으로 인해 다른 변화가 생겼을지 모르니 다시 확인하도록 함
                //! 변화가 발견되면 다시 확인하여 digest 종료 후 last와 데이터가 일치하도록 보장해야 함
                dirty = true;
                // 캐시 값 업데이트
                this.$$watchers[i].last = newValue;
            }
        }
    } while (dirty);
};

watch() 시에 값 추출 메서드와 콜백 함수를 기록해 두고, digest() 과정에서 watcher를 순회하며 값을 다시 추출해 캐시된 값이 변했는지 확인합니다.

한 번의 순회 중 변화가 하나라도 발견되면 다시 확인하여 데이터와 캐시 값이 완전히 일치할 때까지 반복합니다. 이 과정에서 무한 루프(두 데이터 변화가 서로 맞물려 있는 경우)가 발생할 수 있으므로, 최소한 최대 연속 확인 횟수 제한(TTL)을 두어 무한 루프를 방지해야 하지만 여기서는 생략하겠습니다.

P.S. 상세한 Angular 구현 분석은 참고 자료를 확인하세요.

더티 체킹 메커니즘을 이용한 데이터-뷰 바인딩 구현입니다.

var bind = function(scope, node) {
    scope._nodes = scope._nodes || [];
    scope._nodes.push(node);
    var key = node.getAttribute('data-bind');
    if (!key) return console.error('no data bing key');

    // 초기 뷰 업데이트
    updateView(scope, scope[key]);

    // 데이터에서 뷰로
    scope.$watch(function(){
        return scope[key];
    }, function(newValue, oldValue) {
        // console.log(oldValue, newValue);
        updateView(scope, newValue);
    });
};
// 바인딩 수행
bind($scope, $input);
bind($scope, $output);

마찬가지로 뷰-데이터 바인딩을 구현합니다.

// 뷰에서 데이터로
$input.addEventListener('input', function() {
    $scope.value = $input.value;
    $scope.$digest();
});

데이터를 임의로 수정하고 즉시 뷰를 업데이트하려면 수동으로 더티 체킹을 실행해야 합니다.

// 수동으로 변수 값 수정
$('#btn').onclick = function() {
    $scope.value = 'updated value ' + Date.now();
    $scope.$digest();
};

전체 데모 주소: http://ayqy.net/temp/data-binding/dirty-checking.html

5. Virtual DOM

데이터 바인딩 방식 3가지를 모두 소개했습니다. Virtual DOM은 양방향 데이터 바인딩을 구현하는 또 다른 방식이라고 보기는 어렵기 때문입니다 (Virtual DOM이 단방향 데이터 바인딩을 수행하긴 하지만요).

React가 이 방식을 사용하며 Virtual DOM 트리의 업데이트를 다음과 같이 고려합니다.

  • 속성 업데이트: 컴포넌트가 직접 처리
  • 구조 업데이트: 하위 트리(Virtual DOM)를 다시 "렌더링"하고 최소 변경 단계를 찾아 DOM 조작을 묶어서 실제 DOM 트리에 패치 적용

순수하게 데이터 바인딩 측면에서 보면, React의 Virtual DOM은 데이터 바인딩이 없다고 볼 수도 있습니다. setState()가 이전 상태를 유지하지 않고 (상태 폐기) 변화를 추적하지 않으므로 바인딩이라고 부르기 어렵기 때문입니다.

데이터 업데이트 메커니즘 측면에서 보면 React는 데이터 모델을 제공하는 방식(반드시 state를 통해 업데이트해야 함)과 유사합니다.

P.S. 구조 업데이트는 하위 트리를 생성하고 기존 트리와 diff하여 최소 변경 단계를 기록하고 DOM 조작을 모아 실제 DOM을 업데이트하는 것이라고도 할 수 있습니다. 하지만 실제로는 한 번의 재귀적 하향 확인 과정에서 Virtual DOM 트리를 업데이트하면서 동시에 최소 변경 단계를 기록합니다. 그래서 위에서는 두 트리의 diff를 약화시키고 하위 트리 조사를 강조했습니다.

양방향 데이터 바인딩이 없다면 input 시나리오는 어떻게 구현할까요?

var App = React.createClass({
  render: function() {
    return (
      <div>
        <input type="text" value={this.state.text} onChange={this.onChange} />
        <div>{this.state.text}</div>
      </div>
    )
  },
  getInitialState: function() {
    return {text: this.props.text}
  },
  onChange: function(e) {
    this.setState({
      text: e.target.value
    });
  }
});

ReactDOM.render(
  <App text="hoho" />,
  document.getElementById('container')
);

프레임워크가 제공하는 API를 통해 데이터 변화를 수동으로 알리는 방식은 DOM을 직접 조작하는 것과 매우 유사합니다.

var text = 'hoho';
window.onInput = function(e) {
    text = e.target.value;
    document.getElementById('output').innerText = text;
};

document.getElementById('container').innerHTML = '<div><input type="text" value="' + text + '" oninput="onInput(event)" /><div id="output">' + text + '</div></div>';

6. 업데이트 효율 최적화

최초 렌더링은 명확하지만, 이후 업데이트 시 데이터의 변경 사항을 실제 DOM 업데이트에 어떻게 대응시킬지가 관건입니다.

React에서 setState()는 데이터가 이전 것인지, 변했는지 여부에 관심을 두지 않습니다. 상태가 전달되면 이전 상태는 버립니다. 즉, 원본 데이터 객체를 그대로 다시 setState() 하더라도 데이터가 변하지 않았고 뷰를 업데이트할 필요가 없다는 결론을 내리기 위해 하위 트리를 모두 확인해야 합니다. 상태 폐기 메커니즘 때문에 데이터 변화의 세부 사항을 추적하지 않으므로, 동일한 데이터가 들어오더라도 상태가 변하지 않았음을 즉시 확신할 수 없기 때문입니다.

Vue도 비슷한 문제에 직면하지만 상황은 좀 더 낫습니다. setter가 변화를 발견해도 이번 상태 업데이트가 하위 트리에 어떤 영향을 미칠지는 알 수 없습니다. 하지만 데이터 상태를 유지하고 변화를 추적하는 방식의 분명한 장점은 각 데이터와 연결된 뷰를 알고 있다는 점입니다. 즉, 특정 데이터와 관련된 실제 노드 리스트를 이미 알고 있으므로 해당 데이터에 의존하는 모든 실제 노드만 직접 업데이트하면 됩니다. (물론 상태가 "격렬하게" 변할 때 유지보수하며 업데이트하는 것보다 통째로 바꾸는 것이 더 나은 경우에 대한 최적화도 고려해야 합니다.) 따라서 Vue의 watcher에는 의존성 수집 메커니즘이 있어 하향 확인 속도를 높입니다 (의존하지 않는 항목은 확인하지 않음).

Angular는 현재 $scope$$watchers 배열을 확인해야만 상태 변화 여부를 알 수 있으므로 불필요한 확인이 많이 발생합니다. 간단한 값 속성 하나를 업데이트하는 것이 텍스트 노드 하나에만 대응되더라도, $scope 아래의 모든 데이터-뷰 바인딩 관계를 전수 조사해야 합니다. 역시 상태 폐기 메커니즘이며 더티 체킹은 값에만 관심이 있고 데이터 구조에는 관심이 없기 때문입니다. 데이터를 수동으로 수정하고 더티 체킹을 시작하면, 더티 체킹은 방금 수정한 데이터와 확인해야 할 값 사이의 대응 관계를 모르기 때문에 범위를 좁히지 못하고 하나씩 값을 다시 가져와 비교할 수밖에 없습니다.

React의 경우 간단한 최적화 방안이 있는데, 변하지 않은 데이터 확인을 건너뛸 수 있도록 Immutable.js와 같은 불변 데이터 구조를 사용하는 것입니다.

이렇게 하면 state의 속성들을 일일이 확인하지 않고도 변화가 없음을 확신할 수 있습니다. 바로 equals()로 확인해 보고 같다면 더 내려갈 필요가 없는 것이죠.

Om이 바로 이렇게 동작합니다.

I know exactly what didn't change.

불변 데이터 구조를 사용하면 변하지 않은 부분을 빠르게 배제하여 변화가 생긴 부분만 찾아낼 수 있고 diff 효율을 높일 수 있습니다.

P.S. 물론 더티 체킹에서 불변 데이터 구조를 쓰는 것은 의미가 없습니다. 어차피 값을 일일이 가져와 봐야 변했는지 알 수 있고 더티 체킹은 데이터 구조에 전혀 관심이 없기 때문입니다. setter를 이용한 변화 감지에 불변 데이터 구조를 쓰는 것은 너무 경직되어 있고 감시 단위가 세밀하지 못해 장점을 잃게 됩니다. 데이터 모델 제공 방식은 말할 것도 없는데, 데이터 모델이 불변이라면 감시할 변화 자체가 생기지 않기 때문입니다.

7. 참고 자료

이번 참고 자료들은 가치가 매우 높으므로 이어서 정독하시길 권장합니다.

댓글

아직 댓글이 없습니다

댓글 작성