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

Addy Osmani
Addy Osmani

소개

혁명이 다가오고 있습니다. 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()

모든 것을 항상 더티 체크할 필요 없이 AND를 선택할 경우 원시 데이터 객체 (일반 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';

콘솔을 살펴보면 유용한 정보가 반환됩니다. 변경된 속성, 변경된 방식, 새 값이 무엇인지 알고 있습니다.

콘솔 보고서

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

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

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

보고서 변경을 클릭합니다.

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

delete todoModel.completed;
완료됨

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

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

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);
알림 콘솔

여기서는 데이터 속성의 값이 변경('update')할 때를 보고합니다. 객체의 구현에서 보고하기로 선택한 기타 모든 값 (notifier.notifyChange())입니다.

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

관찰자라면 누군가가 중간에 와 있을 때 호출되는 것을 원하지 않는 것이 이상적입니다. 일관되지 않은 상태에서 작업을 하라는 메시지가 표시되지 않도록 해야 합니다. 더 많은 오류 검사를 실행하게 됩니다. 더 많은 나쁜 상황을 허용하려고 시도하며 일반적으로 다루기 어려운 모델입니다. 비동기는 처리하기가 더 어렵지만 결국에는 더 나은 모델입니다.

이 문제의 해결 방법은 종합 변경 레코드입니다.

종합 변경 레코드

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

종합 변경 레코드

접근자와 계산된 속성 관찰은 O.o()의 또 다른 부분인 notifier.notify를 사용하여 해결할 수 있습니다. 대부분의 관측 시스템은 파생 값을 관찰하는 일종의 방식을 원합니다. 방법은 다양합니다. O.o는 '올바른' 방식에 관해 판단하지 않습니다. 계산된 속성은 내부 (비공개) 상태가 변경되면 알리는 접근자여야 합니다.

다시 말하지만, Webdev는 라이브러리가 계산된 속성에 대한 알림과 다양한 접근 방식을 쉽게 만들고 상용구를 줄이는 데 도움이 될 것으로 기대해야 합니다.

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

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);

관찰한 후 변경합니다. 세상에, 정말 재미있어요. 정말 많아!

// 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 }
대규모 변경사항

'perform function' 내부의 모든 작업은 '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()의 폴리필로, 네이티브 구현이 있는 경우 이를 사용하지만 그렇지 않은 경우에는 폴리필하고 그 위에 유용한 슈가링을 포함합니다. 변경사항을 요약하고 변경사항에 대한 보고서를 제공하는 전체적인 뷰를 제공합니다. 데이터 레이크가 노출하는 두 가지 강력한 요소는 다음과 같습니다.

  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의 향상된 변경 감지에 관한 설계 문서를 작성했습니다. 장기적 접근 방식은 Object.observe()가 Chrome 안정화 버전으로 출시되면 이를 활용하여 그때까지 자체적인 변경 감지 방식인 Watchtower.js를 선택하는 것입니다. 정말 흥미진진합니다.

결론

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

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

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

리소스

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