발행(Publication) 고급편

Sidebar 13.5

번역 완료율

이 장에서는:

  • 발행을 다루는 고급 패턴을 배운다.
  • 유연한 발행과 구독에 대하여 알아본다.
  • 이제 발행과 구독의 상호 작용에 대하여 잘 이해하게 되었을 것이다. 이제 보조 바퀴는 빼고 보다 고급의 시나리오를 알아보자.

    컬렉션을 여러 번 발행하기

    발행(publication)에 대한 이전 장에서, 우리는 보다 일반적인 발행과 구독 패턴의 몇 가지를 보았다. 그리고 _publishCursor 함수를 이용하여 사이트를 매우 쉽게 구축할 수 있음을 배웠다.

    먼저, _publishCursor가 정확하게 무슨 일을 하는 지 돌이켜보자: 이것은 주어진 커서에 일치하는 모든 도큐먼트들을 가진다. 그리고 이들을 클라이언트 쪽의 같은 이름을 가진 컬렉션으로 보낸다. publication이란 이름은 어디에도 포함되지 않았음을 주목하라.

    이것은 우리가 어떤 컬렉션이든지 클라이언트와 서버를 연결하는 복수의 발행을 만들 수 있음을 의미한다.

    우리는 이런 패턴을 페이지 만들기 장에서 이미 본 적이 있다. 그 때 우리는 현재 보여지는 post 발행에 추가하여 전체 post의 페이징된 부분 집합을 발행하였다.

    또 다른 유사한 용례로는 한 아이템의 전체 상세와 함께 도큐먼트의 규모가 큰 집합에 대한 개요를 발행하는 경우이다:

    컬렉션을 두 번 발행하기
    컬렉션을 두 번 발행하기
    Meteor.publish('allPosts', function() {
      return Posts.find({}, {fields: {title: true, author: true}});
    });
    
    Meteor.publish('postDetail', function(postId) {
      return Posts.find(postId);
    });
    

    클라이언트가 (postDetail 구독에 올바른 postId를 보내기 위해 autorun을 사용하는) 이 두 발행에 구독할 때, 그 ‘posts’ 컬렉션은 두 개의 소스에서 얻어진다: 첫 구독으로부터 제목과 저자명의 목록, 두 번째에서 post의 전체 상세 데이터.

    postDetail에 의해 발행된 post가 (비록 그 속성의 일부만이기 하지만) allPosts에 의해서도 발행된다는 것을 알 지도 모른다. 그런데 미티어는 필드를 결합하면서 발생하는 중복을 처리하여 중복된 post가 없도록 한다.

    이것은 대단하다. 왜나면 post 요약본의 목록을 화면에 그릴 때, 우리에게 필요한 것을 보여주기에 충분한 데이터를 가진 객체들로 처리하기 때문이다. 그런데, 단일 post에 대한 페이지를 렌더링할 때, 우리는 그것을 보여주기에 필요한 모든 것을 가지고 있다. 물론 클라이언트에서 이 경우 모든 post 목록에서 이용가능한 모든 필드를 기대할 수는 없다 –- 이건 제대로 걸렸다!

    다양한 도큐면트 속성들에만 제한을 받는 것은 아님을 유념하기 바란다. 두 개의 발행에서 동일한 속성을 발행할 수 있지만, 아이템들을 다르게 주문해야 한다.

    Meteor.publish('newPosts', function(limit) {
      return Posts.find({}, {sort: {submitted: -1}, limit: limit});
    });
    
    Meteor.publish('bestPosts', function(limit) {
      return Posts.find({}, {sort: {votes: -1, submitted: -1}, limit: limit});
    });
    
    server/publications.js

    하나의 발행에 여러 번 구독하기

    우리는 방금 하나의 컬렉션을 여러 번 발행하는 방법을 알아보았다. 또 다른 패턴으로 유사한 결과을 얻을 수 있다: 하나의 발행을 만들고 여기에 여러 번 구독하기.

    Microscope에서 우리는 posts 발행에 여러 번 구독하지만, Iron Router는 각각의 구독을 설정하고 분리한다. 하지만 동시에 여러 번 구독을 할 수 없는 이유는 없다.

    예를 들면, post의 최신 목록과 최고 목록을 동시에 로드하기를 원한다고 하자:

    하나의 발행에 두 번 구독하기
    하나의 발행에 두 번 구독하기

    우리는 단일 발행을 설정한다:

    Meteor.publish('posts', function(options) {
      return Posts.find({}, options);
    });
    

    그리고 이 발행에 여러 번 구독한다. 사실 이것은 우리가 Microscope에서 대체로 정확하게 하고 있는 것이다:

    Meteor.subscribe('posts', {submitted: -1, limit: 10});
    Meteor.subscribe('posts', {baseScore: -1, submitted: -1, limit: 10});
    

    그럼 무슨 일이 벌어지는가? 각 브라우저는 개의 다른 구독을 연다. 각각은 서버에서 동일한 발행에 연결되어 있다.

    각 구독은 그 발행에 다른 매개변수를 전달하는데, 각 시점마다 (다른) 도큐먼트들이 posts 컬렉션으로부터 추출되어 클라이언트 쪽 컬렉션으로 보내진다.

    단일 구독에서의 복수의 컬렉션

    joins을 사용하는 MySQL같은 전통적인 관계형 데이터베이스와는 달리, Mongo와 같은 NoSQL 데이터베이스는 본질적으로 비정규화임베딩이 불가피하다. 미티어에서 이것이 어떻게 되는지 살펴보도록 하자.

    구체적인 사례를 보자. 우리는 post에 댓글을 달았다. 그리고 사용자가 열람중인 단일 post에 달린 댓글 목록만을 발행하였다.

    그런데, 우리가 프론트 페이지에 모든 post 목록(이 목록들은 페이징에 따라 달라질 수 있다는 점을 기억하라)의 댓글들을 보여주길 원한다고 가정해보자. 이 사례는 post에 comment 목록을 임베딩해야 하는 이유를 제공한다. 그리고 사실 댓글의 갯수를 비정규화하여 넣은 이유이기도 하다.

    물론 우리는 post에 댓글을 임베딩하고 Comments 컬렉션을 완전히 제거할 수도 있었다. 하지만, 우리가 이전에 비정규화 장에서 본 바와 같이 그렇게 하는 것은 별도의 컬렉션으로 처리할 때의 여러 잇점을 잃는 결과를 가져온다.

    그러나, 별도의 컬렉션 상태로 유지하면서 댓글을 임베딩하는 것이 가능한 구독을 포함하는 기법이 있다.

    프론트 페이지의 post 목록과 함께, 각 post의 상위 두 개의 댓글 목록을 구독하려고 하는 경우를 가정해보자.

    이를 독립된 댓글 발행으로 처리하기는 어려울 것이다. 특히 post 목록이 (말하자면, 최근 10개와 같이) 제한을 가지면 그렇다. 우리는 아래와 같은 발행을 작성해야 한다:

    단일 구독에서의 두 컬렉션
    단일 구독에서의 두 컬렉션
    Meteor.publish('topComments', function(topPostIds) {
      return Comments.find({postId: topPostIds});
    });
    

    이것은 성능 측면에서 문제가 될 것이다. 이 발행은 topPostIds 목록이 변경될 때마다 무효화되고 새로 구성되어야 하기 때문이다.

    하지만, 이를 해결하는 방안이 있다. 컬렉션 당 하나 이상의 발행을 가질 수도 있지만, 발행 당 하나 이상의 컬렉션을 가질 수도 있다는 사실을 이용한다:

    Meteor.publish('topPosts', function(limit) {
      var sub = this, commentHandles = [], postHandle = null;
    
      // send over the top two comments attached to a single post
      function publishPostComments(postId) {
        var commentsCursor = Comments.find({postId: postId}, {limit: 2});
        commentHandles[post._id] = 
          Meteor.Collection._publishCursor(commentsCursor, sub, 'comments');
      }
    
      postHandle = Posts.find({}, {limit: limit}).observeChanges({
        added: function(id, post) {
          publishPostComments(post._id);
          sub.added('posts', id, post);
        },
        changed: function(id, fields) {
          sub.changed('posts', id, fields);
        },
        removed: function(id) {
          // stop observing changes on the post's comments
          commentHandles[id] && commentHandles[id].stop();
          // delete the post
          sub.removed('posts', id);
        }
      });
    
      sub.ready();
    
      // make sure we clean everything up (note `_publishCursor`
      //   does this for us with the comment observers)
      sub.onStop(function() { postsHandle.stop(); });
    });
    

    주목할 것은, 이 발행에서 어떤 값도 리턴하지 않으며, sub에게 (.added()와 기타 동료들을 통해) 수동적으로 메시지를 보내기만 한다는 점이다. 따라서 _publishCursor 에게 커서를 리턴하여 이를 처리하도록 요구할 필요가 없다.

    이제 post를 발행할 때마다, 우리는 자동으로 여기에 연결된 상위 두 개의 댓글을 발행한다. 그리고 이 모든 것이 한 번의 구독요청으로 이루어진다!

    미티어가 이 방식을 아직은 이해하기 쉽게 제공하지는 못하지만, Atmosphere의 publish-with-relations 패키지를 검토하여 보기 바란다. 이것은 이 패턴을 보다 사용하지 쉽게 구현하는 것을 목표로 하고 있다.

    다른 컬렉션들을 연결하기

    구독의 유연성에 대한 새로운 지식은 그 밖의 무엇을 줄까? 글쎄, _publishCursor를 사용하지 않는다면 서버의 소스 컬렉션이 클라이언트의 타겟 컬렉션과 동일한 이름을 가져야 한다는 제약을 따를 필요는 없다.

    두 개의 구독에 대한 하나의 컬렉션
    두 개의 구독에 대한 하나의 컬렉션

    이를 하려는 한 가지 이유는 Single Table Inheritance이다.

    우리가 post로부터 다양한 형태의 객체 - 각 객체는 공통의 필드를 저장하지만 그 내용은 약간 다를 수 있다 - 를 참조하는 경우를 상상해보자. 예를 들면, 우리는 Tumblr같은 블로깅 엔진을 구축하고 있는데, 거기서 각 post는 ID, timestamp, 그리고 제목을 가진다; 여기에 추가로 image, video, link 또는 텍스트까지 있다.

    우리는 이 모든 객체를 단일의 'resources' 컬렉션에 저장할 수 있다. 이 때 객체의 종류를 가리키는 type 속성(video, image, link, 등)을 사용한다.

    그리고 서버에서는 단일 Resources 컬렉션 형태로 있지만, 클라이언트에서는 이를 복수의 Videos, Images, 등의 컬렉션들로 변환할 수 있다. 이를 처리하는 마술같은 코드는 다음과 같다:

      Meteor.publish('videos', function() {
        var sub = this;
    
        var videosCursor = Resources.find({type: 'video'});
        Meteor.Collection._publishCursor(videosCursor, sub, 'videos');
    
        // _publishCursor doesn't call this for us in case we do this more than once.
        sub.ready();
      });
    

    우리는 _publishCursor에게 그 커서가 (리턴) 하는 비디오 목록을 발행하라고 지시하고 있다. 한 편으로는, 클라이언트에 resources 컬렉션을 발행하기 보다는 'resource'에서 'videos'로 발행을 하라고 지시하는 것이다.

    이렇게 하는 것이 좋은 생각일까? 우리가 판단할 일은 아니다. 어쨋든, 미티어를 최대한 활용하기 위해서라면 가능한 것이 무엇인지를 아는 것은 좋다!