반응성(Reactivity) 고급편

Sidebar 11.5

번역 완료율

이 장에서는:

  • 미티어에서 반응형 데이터 소스를 만드는 방법을 배운다.
  • 반응형 데이터 소스의 간단한 예제를 만들어 본다.
  • Deps와 AngularJS를 비교해본다.
  • 의존성 추적 코드를 직접 작성하는 경우는 거의 없지만, 의존성 해소가 진행되는 경로를 추적하는데는 반응성을 이해하는 것이 확실히 도움이 된다.

    현재 사용자의 페이스북 친구들 중에서 몇 명이 Microscope의 각 post에 “좋아요"를 눌렀는 지를 추적한다고 상상해보자. 사용자를 페이스북으로 인증하고, 적절한 API를 호출하고, 필요한 데이터를 파싱하는 작업은 이미 구현했다고 가정하자. 이제 우리는 좋아요의 숫자를 리턴하는 클라이언트 쪽의 비동기 함수 getFacebookLikeCount(user, url, callback)를 가지고 있다.

    이런 함수에 대하여 기억해 둘 중요한 사항은 이것이 비 반응형이고 비 실시간이라는 점이다. 이것은 페이스북에 HTTP 요청을 보내고, 일부 데이터를 가져오며, 비동기 콜백에서 애플리케이션에 적용할 수 있게 한다. 그러나 그 기능은 페이스북에서 숫자가 변경되어도 스스로 재실행되지 않을 것이다. 그 데이터가 변해도 우리 UI는 변경되지 않을 것이다.

    이를 해결하려고, 몇 초 간격으로 setInterval를 사용하여 함수를 호출한다:

    currentLikeCount = 0;
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId).url, 
          function(err, count) {
            if (!err)
              currentLikeCount = count;
          });
      }
    }, 5 * 1000);
    

    currentLikeCount 변수를 체크할 때마다, 우리는 5초 정도의 오차를 가지고 정확한 숫자를 얻을 것이라고 기대할 수 있다. 이제 우리는 이 변수를 헬퍼에서 다음과 같이 사용할 수 있다:

    Template.postItem.likeCount = function() {
      return currentLikeCount;
    }
    

    그런데, currentLikeCount이 변경될 때 템플릿을 다시 그리도록 어떤 것도 아직 지시하지 않는다. 비록 변수가 스스로 변경된다는 점에서는 실시간에 준한다고 하겠지만, 이것이 반응형은 아니므로 나머지 미티어 에코시스템과 적절하게 통신하는 것은 아니다.

    반응성 추적: 컴퓨테이션(Computation)

    미티어의 반응성은 의존성(dependency) - 컴퓨테이션 집합을 추적하는 데이터 구조 - 에 의해서 중개된다.

    이전의 반응성 사이드바 장에서 보았듯이, 컴퓨테이션은 반응형 데이터를 사용하는 코드영역이다. 이 경우, postItem 템플릿에 대한 컴퓨테이션이 은연중에 만들어진 바 있다. 그 템플릿 매니저의 모든 헬퍼는 그 컴퓨테이션 내부에서 작동한다.

    컴퓨테이션을 반응형 데이터에 "관심을 가지는” 코드 영역이라고 생각해도 좋다. 데이터가 변경될 때, (invalidate()에 의해서) 알림을 받는 것이 이 컴퓨테이션이고 무슨 일을 할 지를 결정하는 것이 컴퓨테이션이다.

    변수를 반응형 함수로 바꾸기

    변수 currentLikeCount을 반응형 데이터 소스로 바꾸려면, 이에 대한 의존성을 가지는 모든 컴퓨테이션을 추적해야 한다. 따라서 이것을 변수에서 (임의의 값을 리턴하는) 함수로 바꾸어야 한다는 요구가 발생한다:

    var _currentLikeCount = 0;
    var _currentLikeCountListeners = new Tracker.Dependency();
    
    currentLikeCount = function() {
      _currentLikeCountListeners.depend();
      return _currentLikeCount;
    }
    
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
          function(err, count) {
            if (!err && count !== _currentLikeCount) {
              _currentLikeCount = count;
              _currentLikeCountListeners.changed();
            }
          });
      }
    }, 5 * 1000);
    

    우리가 한 것은 _currentLikeCountListeners의 의존성을 설정한 것인데, 이는 currentLikeCount()이 내부에서 사용된 모든 컴퓨테이션을 추적한다. _currentLikeCount값이 변경되면, 그 의존성에 따라 changed() 함수를 호출하는데 이는 모든 추적된 컴퓨테이션을 무효화(invalidate)시킨다.

    이 컴퓨테이션들은 진행하면서 각 케이스별로 그 변경 사항을 처리한다.

    이것이 간단한 반응형 데이터소스를 위한 전형으로 보였다면, 제대로 본 것이다. 미티어는 이 과정을 보다 쉽게 할 수 있는 (직접 컴퓨테이션을 사용해야 할 필요없이, 단지 autorun을 사용하면 되도록) 빌트인 도구를 제공한다. 플랫폼 패키지에 reactive-var라는 이름의 패키지가 있는데 이것이 바로 currentLikeCount() 함수가 하는 작업을 처리한다. 이것을 다음과 추가한다:

    meteor add reactive-var
    

    그리고 이것으로 우리 코드를 다소 단순화할 수 있다:

    var currentLikeCount = new ReactiveVar();
    
    Meteor.setInterval(function() {
      var postId;
      if (Meteor.user() && postId = Session.get('currentPostId')) {
        getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
          function(err, count) {
            if (!err) {
              currentLikeCount.set(count);
            }
          });
      }
    }, 5 * 1000);
    

    이제 헬퍼에서 currentLikeCount.get()를 호출해보면, 이전과 같이 작동할 것이다. 그리고 또 다른 패키지 reactive-dict가 있는데, 이것은 (Session과 거의 같은) 반응형의 키-밸류 저장소를 제공하는 데, 이것 역시 매우 유용할 것이다.

    Tracker와 Angular 비교

    Angular는 구글의 직원들이 개발한 클라이언트 쪽의 반응형 렌더링 라이브러리이다. 미티어의 의존성 추적에 의한 접근 방식을 Angular와 비교해보면 이것의 접근 방식은 완전히 다르다.

    우리는 미티어의 모델이 컴퓨테이션이라 불리는 코드 블록을 사용하는 것을 보았다. 이 컴퓨테이션들은 적절한 시점에 무효화(invalidate)하는 특별한 “반응형” 데이터 소스들(함수들)에 의해서 추적된다. 그래서 이 데이터 소스는 명시적으로 그것이 의존하는 모두에게 invalidate()를 호출해야 하는 시점을 알린다. 이 시점은 일반적으로 데이터가 변경되었을 때이지만, 그 데이터소스는 잠재적으로 다른 이유로도 무효화를 구동할 지 말지를 결정할 수 있다는 점을 유의하기 바란다.

    추가로, 컴퓨테이션이 보통은 무효화될 때 재실행하지만, 독자가 원하는 방식으로 행동하도록 설정할 수 있다. 이 모든 것이 반응성에 대한 고수준의 제어를 가능하게 한다.

    Angular에서, 반응성은 scope 객체에 의해서 이루어진다. Scope는 특별한 메서드들을 가진 평이한 JavaScript 객체로 간주된다.

    임의의 scope에서 어떤 값에 반응적으로 의존하기를 원하면, scope.$watch를 호출하여 (scope의 일부 영역에) 관심있는 expression을 제공하고 그 expression이 변경되는 매 시점마다 실행되는 listener 함수를 제공한다. 따라서 그 expression의 값이 변경되는 매 시점에 하고자 하는 바를 명시적으로 정확하게 지정한다.

    위의 페이스북 예제로 돌아가면, 우리는 다음과 같이 작성한다:

    $rootScope.$watch('currentLikeCount', function(likeCount) {
      console.log('Current like count is ' + likeCount);
    });
    

    물론, 미티어에서 컴퓨테이션을 거의 설정하지 않는 것처럼, Angular에서는 ng-model directives{{expressions}}이 변경될 때 다시 그리기를 할 watch들을 자동적으로 설정하기 때문에 $watch를 명시적으로 자주 호출하지는 않는다.

    그러한 반응형 값이 변경되었을 때, scope.$apply()가 호출되어야 한다. 이것은 그 scope의 모든 watcher를 재평가하지만, expression의 값이 변경된 watcher의 listener 함수를 호출하기만 한다.

    그러므로 scope.$apply()dependency.changed()와 유사한데, scope의 수준에서 동작한다는 점이 다르며, 어떤 listener가 재평가되어야 하는지를 정확하게 말할 수 있는 제어능력을 주지는 않는다. 말하자면, 이 제어 능력의 부족으로 Angular는 어떤 listener가 재평가가 필요한지를 정확하게 결정하는 데 있어서 매우 똑똑하고 효율적인 능력을 가지게 되었다.

    Angular에서 getFacebookLikeCount() 함수 코드를 작성한다면 다음과 같은 형태가 될 것이다:

    Meteor.setInterval(function() {
      getFacebookLikeCount(Meteor.user(), Posts.find(postId), 
        function(err, count) {
          if (!err) {
            $rootScope.currentLikeCount = count;
            $rootScope.$apply();
          }
        });
    }, 5 * 1000);
    

    확실히 미티어는 대부분의 복잡한 것들도 처리한다. 그리고 많은 작업을 하지 않고도 반응성의 잇점을 얻을 수 있다. 하지만, 희망하건데 더 발전하고 싶다면, 이들 패턴에 대하여 공부하는 것이 도움이 될 것이다.