Object.observe()를 사용한 데이터 결합 회전

소개

혁명이 다가오고 있습니다. 데이터 결합에 관해 알고 있다고 생각하는 모든 것을 바꿀 새로운 JavaScript 기능이 추가되었습니다. 또한 수정 및 업데이트를 위해 모델을 관찰하는 MVC 라이브러리의 수가 변경됩니다. 속성 관찰에 관심이 있는 앱의 성능을 개선할 준비가 되셨나요?

좋습니다. 더 이상 지체하지 않고 Object.observe()Chrome 36 안정화 버전에 출시되었음을 알려드립니다. [와우. THE CROWD GOES WILD].

향후 ECMAScript 표준의 일부인 Object.observe()는 별도의 라이브러리가 필요하지 않은 비동기식 JavaScript 객체 변경사항 관찰 메서드입니다. 이를 통해 관찰자는 관찰된 객체 집합에 발생한 변경사항 집합을 설명하는 시간순 변경 레코드 시퀀스를 수신할 수 있습니다.

// Let's say we have a model with data
var model = {};

// Which we then observe
Object.observe(model, function(changes){

    // This asynchronous callback runs
    changes.forEach(function(change) {

        // Letting us know what changed
        console.log(change.type, change.name, change.oldValue);
    });

});

변경사항이 있을 때마다 다음과 같이 보고됩니다.

변경사항이 보고되었습니다.

Object.observe() (O.o() 또는 Oooooooo라고 부름)를 사용하면 프레임워크 없이 양방향 데이터 결합을 구현할 수 있습니다.

그렇다고 해서 사용하지 말라는 것은 아닙니다. 비즈니스 로직이 복잡한 대규모 프로젝트의 경우 사전 설정된 프레임워크는 매우 중요하므로 계속 사용해야 합니다. 프레임워크는 신규 개발자의 오리엔테이션을 간소화하고, 코드 유지관리가 줄어들며, 일반적인 작업을 수행하는 방법에 패턴을 적용합니다. 뷰가 필요하지 않은 경우 이미 O.o()를 활용하는 Polymer와 같이 더 작고 구체적인 라이브러리를 사용할 수 있습니다.

프레임워크나 MV* 라이브러리를 많이 사용하는 경우에도 O.o()를 사용하면 동일한 API를 유지하면서 더 빠르고 간단한 구현으로 상당한 성능 개선을 얻을 수 있습니다. 예를 들어 작년에 Angular에서 모델이 변경되는 벤치마크에서 더티 체크가 업데이트당 40ms가 소요되고 O.o()가 업데이트당 1~2ms가 소요된다는 사실을 발견했습니다 (20~40배 더 빨라짐).

복잡한 코드가 많이 필요하지 않은 데이터 결합을 사용하면 더 이상 변경사항을 폴링할 필요가 없으므로 배터리 수명이 더 길어집니다.

O.o()에 관해 이미 알고 있다면 기능 소개로 건너뛰거나 이 기능으로 해결할 수 있는 문제에 관해 자세히 알아보세요.

관찰할 항목

데이터 관찰은 일반적으로 다음과 같은 특정 유형의 변경사항을 주시하는 것을 의미합니다.

  • 원시 JavaScript 객체 변경사항
  • 속성이 추가, 변경, 삭제될 때
  • 배열에 요소가 스플라이스되어 있는 경우
  • 객체의 프로토타입 변경

데이터 결합의 중요성

모델-뷰 컨트롤 분리를 고려할 때 데이터 결합이 중요해집니다. HTML은 훌륭한 선언적 메커니즘이지만 완전히 정적입니다. 이상적으로는 데이터와 DOM 간의 관계를 선언하고 DOM을 최신 상태로 유지하는 것이 좋습니다. 이렇게 하면 애플리케이션의 내부 상태 또는 서버 간에 DOM을 통해 데이터를 주고받는 반복적인 코드를 작성하는 데 드는 많은 시간을 절약할 수 있습니다.

데이터 결합은 데이터 모델의 여러 속성과 뷰의 여러 요소 간의 관계를 연결해야 하는 복잡한 사용자 인터페이스가 있는 경우에 특히 유용합니다. 이는 오늘날 빌드하는 단일 페이지 애플리케이션에서 매우 일반적입니다.

브라우저에서 데이터를 기본적으로 관찰하는 방법을 베이킹함으로써 JavaScript 프레임워크 (및 개발자가 작성하는 소형 유틸리티 라이브러리)에 오늘날 사용되는 느린 해킹에 의존하지 않고 모델 데이터의 변경사항을 관찰하는 방법을 제공합니다.

오늘날의 세계

더티 체크

이전에 데이터 결합을 본 적이 있나요? 웹 앱을 빌드하는 데 최신 MV* 라이브러리 (예: Angular, Knockout)를 사용하는 경우 모델 데이터를 DOM에 결합하는 데 익숙할 것입니다. 참고로 다음은 데이터와 UI가 항상 동기화되도록 phones 배열 (JavaScript에 정의됨)의 각 전화의 값을 목록 항목에 바인딩하는 전화번호 목록 앱의 예입니다.

<html ng-app>
  <head>
    ...
    <script src='angular.js'></script>
    <script src='controller.js'></script>
  </head>
  <body ng-controller='PhoneListCtrl'>
    <ul>
      <li ng-repeat='phone in phones'>
        
        <p></p>
      </li>
    </ul>
  </body>
</html>

컨트롤러의 JavaScript는 다음과 같습니다.

var phonecatApp = angular.module('phonecatApp', []);

phonecatApp.controller('PhoneListCtrl', function($scope) {
  $scope.phones = [
    {'name': 'Nexus S',
     'snippet': 'Fast just got faster with Nexus S.'},
    {'name': 'Motorola XOOM with Wi-Fi',
     'snippet': 'The Next, Next Generation tablet.'},
    {'name': 'MOTOROLA XOOM',
     'snippet': 'The Next, Next Generation tablet.'}
  ];
});

기본 모델 데이터가 변경될 때마다 DOM의 목록이 업데이트됩니다. Angular는 이를 어떻게 실행하나요? 백그라운드에서는 더티 검사라는 작업을 실행합니다.

더티 검사

더티 체크의 기본 개념은 데이터가 변경될 수 있는 시점에 라이브러리가 다이제스트 또는 변경 주기를 통해 변경되었는지 확인해야 한다는 것입니다. Angular의 경우 다이제스트 주기는 변경사항이 있는지 확인하기 위해 감시하도록 등록된 모든 표현식을 식별합니다. 모델의 이전 값을 알고 있으며, 이전 값이 변경되면 변경 이벤트가 실행됩니다. 개발자에게는 사용하기 쉽고 구성하기에 꽤 좋은 원시 JavaScript 객체 데이터를 사용할 수 있다는 이점이 있습니다. 단점은 알고리즘 동작이 좋지 않고 비용이 많이 들 수 있다는 점입니다.

더티 검사

이 작업의 비용은 관찰된 총 객체 수에 비례합니다. 더러운 검사를 많이 해야 할 수 있습니다. 또한 데이터가 변경되었을 수 있는 경우 더티 검사를 트리거하는 방법이 필요할 수 있습니다. 프레임워크는 이를 위해 여러 가지 영리한 트릭을 사용합니다. 언제 완벽하게 될지는 알 수 없습니다.

웹 생태계는 자체 선언적 메커니즘(예:

  • 제약 조건 기반 모델 시스템
  • 자동 지속성 시스템 (예: IndexedDB 또는 localStorage의 변경사항 지속)
  • 컨테이너 객체 (Ember, Backbone)

컨테이너 객체는 프레임워크가 내부에서 데이터를 보유하는 객체를 만드는 곳입니다. 데이터에 대한 접근자가 있으며 설정하거나 가져온 내용을 캡처하고 내부적으로 브로드캐스트할 수 있습니다. 이 방법은 효과적입니다. 비교적 성능이 우수하고 알고리즘 동작이 좋습니다. Ember를 사용하는 컨테이너 객체의 예는 다음과 같습니다.

// Container objects
MyApp.president = Ember.Object.create({
  name: "Barack Obama"
});
 
MyApp.country = Ember.Object.create({
  // ending a property with "Binding" tells Ember to
  // create a binding to the presidentName property
  presidentNameBinding: "MyApp.president.name"
});
 
// Later, after Ember has resolved bindings
MyApp.country.get("presidentName");
// "Barack Obama"
 
// Data from the server needs to be converted
// Composes poorly with existing code

여기서 변경된 사항을 찾는 데 드는 비용은 변경된 항목 수에 비례합니다. 또 다른 문제는 이제 이 다른 종류의 객체를 사용하고 있다는 점입니다. 일반적으로 관찰 가능하도록 서버에서 가져온 데이터를 이러한 객체로 변환해야 합니다.

대부분의 코드는 원시 데이터에서 작동할 수 있다고 가정하기 때문에 기존 JS 코드와는 잘 작동하지 않습니다. 이러한 전문적인 종류의 객체에는 적용되지 않습니다.

Introducing Object.observe()

이상적으로는 두 가지 방법의 장점을 모두 활용하는 것이 좋습니다. 즉, 원시 데이터 객체 (일반 JavaScript 객체)를 지원하면서 데이터를 관찰할 수 있는 방법을 선택할 수 있고 항상 모든 항목을 더티 체크할 필요가 없습니다. 알고리즘 동작이 우수한 항목 잘 구성되고 플랫폼에 빌드된 항목 이것이 Object.observe()의 장점입니다.

이를 통해 객체를 관찰하고, 속성을 변경하고, 변경된 사항에 관한 변경 보고서를 확인할 수 있습니다. 이제 이론은 충분히 알아봤으니 코드를 살펴보겠습니다.

Object.observe()

Object.observe() 및 Object.unobserve()

모델을 나타내는 간단한 기본 JavaScript 객체가 있다고 가정해 보겠습니다.

// A model can be a simple vanilla object
var todoModel = {
  label: 'Default',
  completed: false
};

그런 다음 객체에 변형 (변경)이 있을 때마다 콜백을 지정할 수 있습니다.

function observer(changes){
  changes.forEach(function(change, i){
      console.log('what property changed? ' + change.name);
      console.log('how did it change? ' + change.type);
      console.log('whats the current value? ' + change.object[change.name]);
      console.log(change); // all changes
  });
}

그런 다음 O.o()를 사용하여 이러한 변경사항을 관찰할 수 있습니다. 객체를 첫 번째 인수로, 콜백을 두 번째 인수로 전달합니다.

Object.observe(todoModel, observer);

먼저 Todos 모델 객체를 변경해 보겠습니다.

todoModel.label = 'Buy some more milk';

콘솔을 살펴보면 유용한 정보가 반환됩니다. 어떤 속성이 변경되었는지, 어떻게 변경되었는지, 새 값이 무엇인지 알 수 있습니다.

콘솔 보고서

와! 더티 체킹은 이제 안녕! 비석에 Comic Sans로 새겨야 합니다. 다른 속성을 변경해 보겠습니다. 이번 completeBy:

todoModel.completeBy = '01/01/2014';

변경 보고서가 다시 수신된 것을 확인할 수 있습니다.

보고서 변경

좋습니다. 이제 객체에서 'completed' 속성을 삭제하기로 결정하면 어떻게 될까요?

delete todoModel.completed;
완료됨

반환된 변경사항 보고서에는 삭제에 관한 정보가 포함되어 있습니다. 예상대로 속성의 새 값은 이제 정의되지 않습니다. 이제 속성이 추가된 시점을 확인할 수 있습니다. 삭제된 경우 기본적으로 객체의 속성 ('new', 'deleted', 'reconfigured')의 세트와 프로토타입 변경 (proto)입니다.

다른 관찰 시스템과 마찬가지로 변경사항 수신 대기를 중지하는 메서드도 있습니다. 이 경우 Object.unobserve()이며, O.o()와 동일한 서명을 갖지만 다음과 같이 호출할 수 있습니다.

Object.unobserve(todoModel, observer);

아래에서 볼 수 있듯이 이 작업이 실행된 후 객체에 변경사항을 적용해도 더 이상 변경 레코드 목록이 반환되지 않습니다.

변형

관심 있는 변경사항 지정

관찰된 객체의 변경사항 목록을 가져오는 방법에 관한 기본사항을 살펴봤습니다. 객체에 적용된 모든 변경사항이 아니라 일부 변경사항에만 관심이 있는 경우 어떻게 해야 하나요? 모든 사용자에게 스팸 필터가 필요합니다. 관찰자는 수락 목록을 통해 듣고 싶은 변경사항 유형만 지정할 수 있습니다. 다음과 같이 O.o()의 세 번째 인수를 사용하여 지정할 수 있습니다.

Object.observe(obj, callback, optAcceptList)

이를 사용하는 방법의 예를 살펴보겠습니다.

// Like earlier, a model can be a simple vanilla object

var todoModel = {
  label: 'Default',
  completed: false

};


// We then specify a callback for whenever mutations 
// are made to the object
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })

};

// Which we then observe, specifying an array of change 
// types we're interested in

Object.observe(todoModel, observer, ['delete']);

// without this third option, the change types provided 
// default to intrinsic types

todoModel.label = 'Buy some milk'; 

// note that no changes were reported

하지만 이제 라벨을 삭제하면 다음과 같은 유형의 변경사항이 보고됩니다.

delete todoModel.label;

O.o()에 허용 유형 목록을 지정하지 않으면 기본적으로 '내장' 객체 변경 유형 (add, update, delete, reconfigure, preventExtensions (확장 불가능한 객체가 관찰되지 않는 경우))이 됩니다.

알림

O.o()에는 알림 개념도 있습니다. 휴대전화에서 받는 성가신 알림과는 달리 유용합니다. 알림은 변형 관찰자와 유사합니다. 마이크로 태스크가 끝날 때 발생합니다. 브라우저 컨텍스트에서는 거의 항상 현재 이벤트 핸들러의 끝에 있습니다.

일반적으로 하나의 작업 단위가 완료되고 관찰자가 작업을 시작할 수 있으므로 적절한 타이밍입니다. 턴 기반 처리 모델입니다.

알림을 사용하는 워크플로는 다음과 같습니다.

알림

객체의 속성이 가져오거나 설정될 때의 맞춤 알림을 정의하는 데 실제로 notifier가 사용되는 방법의 예를 살펴보겠습니다. 여기에서 댓글을 확인해 주세요.

// Define a simple model
var model = {
    a: {}
};

// And a separate variable we'll be using for our model's 
// getter in just a moment
var _b = 2;

// Define a new property 'b' under 'a' with a custom
// getter and setter

Object.defineProperty(model.a, 'b', {
    get: function () {
        return _b;
    },
    set: function (b) {

        // Whenever 'b' is set on the model
        // notify the world about a specific type
        // of change being made. This gives you a huge
        // amount of control over notifications
        Object.getNotifier(this).notify({
            type: 'update',
            name: 'b',
            oldValue: _b
        });

        // Let's also log out the value anytime it gets
        // set for kicks
        console.log('set', b);

        _b = b;
    }
});

// Set up our observer
function observer(changes) {
    changes.forEach(function (change, i) {
        console.log(change);
    })
}

// Begin observing model.a for changes
Object.observe(model.a, observer);
알림 콘솔

여기서는 데이터 속성의 값이 변경될 때 ('업데이트') 보고합니다. 객체의 구현에서 보고하기로 선택한 기타 항목 (notifier.notifyChange())

웹 플랫폼에서 수년간 쌓은 경험에 따르면 동기식 접근 방식이 가장 이해하기 쉽기 때문에 가장 먼저 시도해야 합니다. 문제는 근본적으로 위험한 처리 모델을 생성한다는 점입니다. 코드를 작성할 때 객체의 속성을 업데이트한다고 가정해 보겠습니다. 이때 객체의 속성을 업데이트하는 코드가 임의의 코드를 초대하여 원하는 대로 실행하게 되는 상황은 바람직하지 않습니다. 함수 중간에 가정사항이 무효화되는 것은 바람직하지 않습니다.

관찰자인 경우 다른 사용자가 어떤 작업을 하고 있는 중에 전화를 받는 것은 바람직하지 않습니다. 일관되지 않은 상태에서 작업을 하라는 메시지가 표시되지 않도록 해야 합니다. 더 많은 오류 검사를 실행하게 됩니다. 더 많은 나쁜 상황을 허용하려고 시도하며 일반적으로 다루기 어려운 모델입니다. 비동기는 처리하기 더 어렵지만 결국에는 더 나은 모델입니다.

이 문제를 해결하는 방법은 합성 변경 레코드입니다.

합성 변경 레코드

기본적으로 접근자 또는 계산된 속성을 사용하려면 이러한 값이 변경될 때 알리는 것이 개발자의 책임입니다. 약간의 추가 작업이 필요하지만 이 메커니즘의 일종의 퍼스트 클래스 기능으로 설계되었으며 이러한 알림은 기본 데이터 객체의 나머지 알림과 함께 전송됩니다. 데이터 속성에서

합성 변경 레코드

액세서와 계산된 속성을 관찰하는 문제는 O.o()의 또 다른 부분인 notifier.notify로 해결할 수 있습니다. 대부분의 관찰 시스템은 파생된 값을 관찰하는 어떤 형태로든지 원합니다. 방법은 다양합니다. O.o는 '올바른' 방법에 관해 판단하지 않습니다. 계산된 속성은 내부 (비공개) 상태가 변경될 때 알림을 제공하는 접근자여야 합니다.

다시 한번 강조하지만 웹 개발자는 라이브러리가 계산된 속성에 대한 알림과 다양한 접근 방식을 쉽게 만들고 상용구를 줄이는 데 도움이 되기를 기대해야 합니다.

다음 예인 원 클래스를 설정해 보겠습니다. 여기서는 원이 있고 반지름 속성이 있다고 가정합니다. 이 경우 반지름은 접근자이며 값이 변경되면 실제로 값이 변경되었다고 자체적으로 알립니다. 이 변경사항은 이 객체 또는 다른 객체의 다른 모든 변경사항과 함께 전송됩니다. 기본적으로 객체를 구현하는 경우 합성 또는 계산된 속성을 사용하거나 이러한 속성이 작동하는 방식에 관한 전략을 선택해야 합니다. 이렇게 하면 시스템 전체에 맞게 조정됩니다.

코드를 건너뛰어 DevTools에서 작동하는지 확인합니다.

function Circle(r) {
  var radius = r;
 
  var notifier = Object.getNotifier(this);
  function notifyAreaAndRadius(radius) {
    notifier.notify({
      type: 'update',
      name: 'radius',
      oldValue: radius
    })
    notifier.notify({
      type: 'update',
      name: 'area',
      oldValue: Math.pow(radius * Math.PI, 2)
    });
  }
 
  Object.defineProperty(this, 'radius', {
    get: function() {
      return radius;
    },
    set: function(r) {
      if (radius === r)
        return;
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
 
  Object.defineProperty(this, 'area', {
    get: function() {
      return Math.pow(radius, 2) * Math.PI;
    },
    set: function(a) {
      r = Math.sqrt(a/Math.PI);
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
}
 
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })
}
합성 변경 레코드 콘솔

액세스자 속성

접근자 속성에 관한 간단한 참고사항 앞서 데이터 속성의 값 변경사항만 관찰할 수 있다고 언급했습니다. 계산된 속성 또는 접근자는 아닙니다. 그 이유는 JavaScript에는 접근자에 대한 값 변경이라는 개념이 없기 때문입니다. 접근자는 함수 모음일 뿐입니다.

접근자에 할당하면 JavaScript가 그곳에서 함수를 호출하기만 하며 접근자의 관점에서는 아무것도 변경되지 않습니다. 일부 코드가 실행될 기회를 제공했을 뿐입니다.

문제는 의미론적으로 위의 값 - 5 할당을 볼 수 있다는 것입니다. 여기서 무슨 일이 있었는지 알아야 합니다. 사실 이 문제는 해결할 수 없습니다. 다음 예는 그 이유를 보여줍니다. 임의의 코드일 수 있으므로 시스템에서 이 코드의 의미를 알 수 있는 방법은 없습니다. 이 경우 원하는 작업을 실행할 수 있습니다. 값에 액세스할 때마다 값이 업데이트되므로 변경되었는지 묻는 것은 큰 의미가 없습니다.

하나의 콜백으로 여러 객체 관찰

O.o()로 가능한 또 다른 패턴은 단일 콜백 관찰자 개념입니다. 이렇게 하면 단일 콜백을 여러 객체의 '관찰자'로 사용할 수 있습니다. 콜백은 '마이크로태스크 종료' 시 관찰하는 모든 객체의 전체 변경사항을 전달받습니다(변경 관찰자와의 유사성 참고).

하나의 콜백으로 여러 객체 관찰

대규모 변경사항

정말 큰 앱을 개발하고 있고 정기적으로 대규모 변경사항을 처리해야 할 수도 있습니다. 객체는 수많은 속성 변경사항을 브로드캐스트하는 대신 더 많은 속성에 영향을 미치는 더 큰 의미론적 변경사항을 더 간결하게 설명할 수 있습니다.

O.o()는 이미 소개한 두 가지 유틸리티인 notifier.performChange()notifier.notify()의 형태로 이를 지원합니다.

대규모 변경사항

수학 유틸리티 (multiply, increment, incrementAndMultiply)를 사용하여 Thingy 객체를 정의하는 경우 대규모 변경사항을 설명하는 방법을 예로 살펴보겠습니다. 유틸리티가 사용될 때마다 작업 모음이 특정 유형의 변경사항으로 구성되어 있음을 시스템에 알립니다.

예: notifier.performChange('foo', performFooChangeFn);

function Thingy(a, b, c) {
  this.a = a;
  this.b = b;
}

Thingy.MULTIPLY = 'multiply';
Thingy.INCREMENT = 'increment';
Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply';


Thingy.prototype = {
  increment: function(amount) {
    var notifier = Object.getNotifier(this);

    // Tell the system that a collection of work comprises 
    // a given changeType. e.g
    // notifier.performChange('foo', performFooChangeFn);
    // notifier.notify('foo', 'fooChangeRecord');
    notifier.performChange(Thingy.INCREMENT, function() {
      this.a += amount;
      this.b += amount;
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.INCREMENT,
      incremented: amount
    });
  },

  multiply: function(amount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.MULTIPLY, function() {
      this.a *= amount;
      this.b *= amount;
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.MULTIPLY,
      multiplied: amount
    });
  },

  incrementAndMultiply: function(incAmount, multAmount) {
    var notifier = Object.getNotifier(this);

    notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() {
      this.increment(incAmount);
      this.multiply(multAmount);
    }, this);

    notifier.notify({
      object: this,
      type: Thingy.INCREMENT_AND_MULTIPLY,
      incremented: incAmount,
      multiplied: multAmount
    });
  }
}

그런 다음 객체에 두 개의 관찰자를 정의합니다. 하나는 변경사항을 모두 포착하는 관찰자이고 다른 하나는 정의된 특정 수락 유형 (Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENT_AND_MULTIPLY)에 대해서만 다시 보고하는 관찰자입니다.

var observer, observer2 = {
    records: undefined,
    callbackCount: 0,
    reset: function() {
      this.records = undefined;
      this.callbackCount = 0;
    },
};

observer.callback = function(r) {
    console.log(r);
    observer.records = r;
    observer.callbackCount++;
};

observer2.callback = function(r){
    console.log('Observer 2', r);
}


Thingy.observe = function(thingy, callback) {
  // Object.observe(obj, callback, optAcceptList)
  Object.observe(thingy, callback, [Thingy.INCREMENT,
                                    Thingy.MULTIPLY,
                                    Thingy.INCREMENT_AND_MULTIPLY,
                                    'update']);
}

Thingy.unobserve = function(thingy, callback) {
  Object.unobserve(thingy);
}

이제 이 코드로 놀아볼 수 있습니다. 새 Thingy를 정의해 보겠습니다.

var thingy = new Thingy(2, 4);

그런 다음 변경사항을 관찰합니다. OMG, so fun. 너무 많은 것들이 있습니다.

// Observe thingy
Object.observe(thingy, observer.callback);
Thingy.observe(thingy, observer2.callback);

// Play with the methods thingy exposes
thingy.increment(3);               // { a: 5, b: 7 }
thingy.b++;                        // { a: 5, b: 8 }
thingy.multiply(2);                // { a: 10, b: 16 }
thingy.a++;                        // { a: 11, b: 16 }
thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 }
대규모 변경사항

'실행 함수' 내의 모든 항목은 'big-change'의 작업으로 간주됩니다. 'big-change'를 수락하는 관찰자는 'big-change' 레코드만 수신합니다. 그렇지 않은 관찰자는 '함수 실행'이 실행한 작업으로 인한 기본 변경사항을 수신합니다.

배열 관찰

객체의 변경사항을 관찰하는 방법을 알아봤는데 배열은 어떨까요? 좋은 질문이에요. 누군가 '좋은 질문입니다'라고 말할 때 훌륭한 질문을 했다고 스스로를 칭찬하느라 답변을 듣지 못하는 경우가 많습니다. 여담이지만 배열을 사용하는 새로운 메서드도 있습니다.

Array.observe()는 자체에 대한 대규모 변경사항(예: 스플라이스, unshift 또는 길이를 암시적으로 변경하는 모든 작업)을 '스플라이스' 변경 레코드로 처리하는 메서드입니다. 내부적으로 notifier.performChange("splice",...)를 사용합니다.

다음은 모델 '배열'을 관찰하고 기본 데이터가 변경될 때와 마찬가지로 변경사항 목록을 가져오는 예입니다.

var model = ['Buy some milk', 'Learn to code', 'Wear some plaid'];
var count = 0;

Array.observe(model, function(changeRecords) {
  count++;
  console.log('Array observe', changeRecords, count);
});

model[0] = 'Teach Paul Lewis to code';
model[1] = 'Channel your inner Paul Irish';
배열 관찰

성능

O.o()의 계산 성능 영향을 생각하는 방법은 읽기 캐시로 생각하는 것입니다. 일반적으로 캐시는 다음과 같은 경우에 적합합니다 (중요도 순).

  1. 읽기 빈도가 쓰기 빈도보다 큽니다.
  2. 쓰기 중에 발생하는 일정한 양의 작업을 읽기 중에 알고리즘적으로 더 나은 성능을 위해 교환하는 캐시를 만들 수 있습니다.
  3. 쓰기 속도가 일정하게 느려지는 것은 허용됩니다.

O.o()는 1)과 같은 사용 사례를 위해 설계되었습니다.

더티 검사를 하려면 관찰 중인 모든 데이터의 사본을 유지해야 합니다. 즉, O.o()에서는 발생하지 않는 더티 체크의 구조적 메모리 비용이 발생합니다. 더티 체크는 적절한 임시 솔루션이지만 애플리케이션에 불필요한 복잡성을 야기할 수 있는 근본적으로 불안정한 추상화입니다.

왜냐하면 데이터가 변경되었을 있는 경우 언제든지 더티 검사를 실행해야 합니다. 이를 실행하는 매우 강력한 방법은 없으며 모든 접근 방식에는 심각한 단점이 있습니다 (예: 폴링 간격을 확인하면 시각적 아티팩트가 발생하고 코드 문제 간에 경합 상태가 발생할 수 있음). 더티 검사에는 관찰자의 전역 레지스트리도 필요하므로 메모리 누수 위험과 O.o()가 방지하는 해체 비용이 발생합니다.

몇 가지 수치를 살펴보겠습니다.

아래 벤치마크 테스트 (GitHub에서 사용 가능)를 사용하면 더티 체크와 O.o()를 비교할 수 있습니다. 이 테스트는 관찰된 객체 집합 크기 대 변형 수 그래프로 구성됩니다. 일반적인 결과는 더티 검사 성능은 관찰된 객체 수에 비례하는 반면 O.o() 성능은 변경된 수에 비례한다는 것입니다.

더티 체크

더티 검사 성능

Object.observe()가 사용 설정된 Chrome

실적 관찰

Object.observe() 다각형 채우기

좋습니다. O.o()는 Chrome 36에서 사용할 수 있지만 다른 브라우저에서는 어떻게 되나요? Google에서 지원해 드립니다. Polymer의 Observe-JS는 O.o()의 폴리필로, 기본 구현이 있는 경우 이를 사용하지만 그렇지 않은 경우에는 폴리필하고 그 위에 유용한 슈가링을 포함합니다. 변화를 요약하고 변경된 사항에 관한 보고서를 제공하는 전 세계의 집계 보기를 제공합니다. 이 API에서 제공하는 두 가지 강력한 기능은 다음과 같습니다.

  1. 경로를 관찰할 수 있습니다. 즉, 특정 객체에서 'foo.bar.baz'를 관찰하고 싶다고 말하면 해당 경로의 값이 변경될 때 알려줍니다. 경로에 도달할 수 없는 경우 값이 정의되지 않은 것으로 간주됩니다.

지정된 객체의 경로에서 값을 관찰하는 예는 다음과 같습니다.

var obj = { foo: { bar: 'baz' } };

var observer = new PathObserver(obj, 'foo.bar');
observer.open(function(newValue, oldValue) {
  // respond to obj.foo.bar having changed value.
});
  1. 배열 스플라이스에 대해 알려줍니다. 배열 스플라이스는 기본적으로 이전 버전의 배열을 새 버전의 배열로 변환하기 위해 배열에서 실행해야 하는 최소 스플라이스 작업 집합입니다. 이는 변환 유형 또는 배열의 다른 뷰입니다. 이전 상태에서 새 상태로 이동하는 데 필요한 최소 작업량입니다.

배열의 변경사항을 최소 스플라이스 집합으로 보고하는 예:

var arr = [0, 1, 2, 4];

var observer = new ArrayObserver(arr);
observer.open(function(splices) {
  // respond to changes to the elements of arr.
  splices.forEach(function(splice) {
    splice.index; // index position that the change occurred.
    splice.removed; // an array of values representing the sequence of elements which were removed
    splice.addedCount; // the number of elements which were inserted.
  });
});

프레임워크 및 Object.observe()

앞서 언급한 대로 O.o()를 사용하면 프레임워크와 라이브러리가 이 기능을 지원하는 브라우저에서 데이터 결합의 성능을 크게 개선할 수 있습니다.

Ember의 Yehuda Katz와 Erik Bryn은 O.o() 지원 추가가 Ember의 단기 로드맵에 포함되어 있다고 확인해 주었습니다. Angular의 Misko Hervy는 Angular 2.0의 향상된 변경 감지에 관한 설계 문서를 작성했습니다. Google의 장기적인 접근 방식은 Object.observe()가 Chrome 안정화 버전에 도달하면 이를 활용하고 그때까지는 자체 변경 감지 접근 방식인 Watchtower.js를 선택하는 것입니다. 정말 기대됩니다.

결론

O.o()는 지금 바로 사용할 수 있는 강력한 웹 플랫폼입니다.

앞으로 더 많은 브라우저에 이 기능이 도입되어 JavaScript 프레임워크가 네이티브 객체 관찰 기능에 액세스하여 성능을 개선할 수 있기를 바랍니다. Chrome을 타겟팅하는 경우 Chrome 36 이상에서 O.o()를 사용할 수 있으며 이 기능은 향후 Opera 출시에서도 사용할 수 있습니다.

JavaScript 프레임워크 작성자에게 Object.observe()에 관해 문의하고 Object.observe()를 사용하여 앱의 데이터 결합 성능을 개선할 계획을 문의해 보세요. 앞으로 흥미진진한 일이 많이 있을 것입니다.

리소스

의견과 검토를 제공해 주신 라파엘 와인스타인, 제이크 아치볼드, 에릭 비델만, 폴 킨란, 비비안 크롬웰님께 감사드립니다.