페이지 만들기

12

번역 완료율

이 장에서는:

  • 미티어의 구독에 대하여 더 상세하게 배운다. 그리고 이를 이용하여 데이터를 제어하는 법을 배운다.
  • 무한 방식의 페이지 만들기를 구현한다.
  • `iron-router-progress`패키지를 이용하여 멋진 iOS 스타일의 프로그레스 바를 구현한다.
  • Post 페이지에 대한 직접 링크를 처리하는 특별한 구독을 만든다.
  • Microscope는 훌륭해 보인다. 그리고 우리는 이것이 세계에 모습을 드러낼 때 큰 환영을 기대한다.

    그래서 우리는 이것이 등장할 때, 등록될 수 많은 post들로 인한 성능에의 영향을 걱정을 할 수도 있다!

    우리는 앞서 클라이언트 쪽의 컬렉션이 서버에 있는 데이터의 부분집합을 가지는 방법에 대하여 다룬 적이 있다. 그리고 알림과 댓글 컬렉션에서 이 방식을 구현하여 왔다.

    현재까지는 여전히 우리는 모든 post를 연결된 모든 사용자에게 발행하고 있다. 결국 수 천 개의 링크가 등록된다면 이것은 문제가 될 것이다. 이를 위해서 우리의 post에 대하여 페이징 처리를 할 필요가 있다.

    더 많은 Post 추가하기

    우선, 초기 설정 데이터에 페이징이 의미를 가지는 충분한 post 목록을 로드하도록 한다:

    // Fixture data 
    if (Posts.find().count() === 0) {
    
      //...
    
      Posts.insert({
        title: 'The Meteor Book',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://themeteorbook.com',
        submitted: new Date(now - 12 * 3600 * 1000),
        commentsCount: 0
      });
    
      for (var i = 0; i < 10; i++) {
        Posts.insert({
          title: 'Test post #' + i,
          author: sacha.profile.name,
          userId: sacha._id,
          url: 'http://google.com/?q=test-' + i,
          submitted: new Date(now - i * 3600 * 1000),
          commentsCount: 0
        });
      }
    }
    
    server/fixtures.js

    meteor reset을 실행하고 나면, 다음과 같은 화면을 보게 될 것이다:

    더미 데이터 보이기.
    더미 데이터 보이기.

    Commit 12-1

    페이징이 필요할 만큼의 충분한 post 목록을 추가했다.

    무한 방식의 페이지

    우리는 “무한” 방식의 페이징을 구현할 것이다. 이것이 의미하는 바는, 처음에는 화면에 10개의 posts를 보여주고 아랫쪽에 “더 보기” 링크를 보여준다. 이 링크를 누르면 10개의 추가 목록을 아래에 붙인다. 이렇게 무한 반복한다. 이 의미는 화면에 보여주는 post의 갯수를 지정하는 하나의 매개변수로 전체 페이징을 제어할 수 있다는 것이다.

    이제 필요한 것은 서버에게 이 매개변수에 대하여 알려주는 방법이다. 그래서 클라이언트에 얼마나 많은 post를 보낼 것인지를 알 수 있게 한다. 우리는 이미 라우터에서 posts 발행을 구독해왔다. 그래서 우리는 이를 이용하여 라우터가 페이징도 조작할 수 있게 할 것이다.

    이를 설정하는 가장 쉬운 방법은 post의 갯수를 경로의 일부로, http://localhost:3000/25와 같은 형태의 폼으로 URL을 지정하는 것이다. 다른 방식에 비해서 이런 URL을 사용하는 또 하나의 장점은 현재 25개의 post를 보여주는데 실수로 브라우저 창을 새로고침한다면, 화면에는 여전히 25개의 post가 다시 보일 것이다.

    이를 적절하게 구현하려면, 우리가 post에 구독하는 방식을 바꿀 필요가 있다. 우리가 댓글 장에서 했던 방식과 마찬가지로, 우리는 구독 코드를 라우터 수준에서 route 수준으로 이동할 필요가 있다.

    이 모두를 한 번에 받아들이기에는 많아 보이지만, 코드를 보면 훨씬 명확해질 것이다.

    우선, Router.configure() 블록에서 posts 발행에 구독하는 것을 중지할 것이다. 그리고, Meteor.subscribe('posts')를 삭제하고 notifications 구독만 남겨둔다:

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() { 
        return [Meteor.subscribe('notifications')]
      }
    });
    
    lib/router.js

    그 다음, 경로에 매개변수 postsLimit를 추가한다. 매개변수 명 다음에 ?를 추가하는 것은 이것이 선택적이라는 의미이다. 그러므로 route는 http://localhost:3000/50뿐 아니라 이전의 http://localhost:3000도 수용한다.

    //...
    
    Router.route('/:postsLimit?', {
      name: 'postsList',
    });
    
    //...
    
    lib/router.js

    유의할 사항은 /:paramter?의 형태를 가지는 경로는 모든 가능한 경로와 다 맞는다는 점이다. 각 route가 순차적으로 파싱이 이루어지면서 현재 경로와 맞는 것을 찾으므로, route의 순서를 특수성을 줄이는 방향으로 정렬을 시켜야 한다.

    다른 말로 표현하면, /posts/:_id 와 같이 특정한 route를 타겟으로 하는 route는 먼저 오고 postList route는 거의 모든 경로와 맞게 되므로 마지막으로 이동하여야 한다.

    이제 올바른 데이터를 찾고 구독하는 어려운 문제를 해결해야 할 시간이다. 우리는 매개변수 postsLimit의 값이 없는 경우 여기에 초기값을 지정하는 방법을 처리해야 한다. 초기값으로는 “5”를 지정하여 페이징처리 과정에서 충분한 여유를 두려고 한다.

    //...
    
    Router.route('/:postsLimit?', {
      name: 'postsList',
      waitOn: function() {
        var limit = parseInt(this.params.postsLimit) || 5; 
        return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
      }
    });
    
    //...
    
    lib/router.js

    주목할 점은 posts 발행의 이름 다음에 JavaScript 객체({limit: postsLimit})를 매개변수로 전달하고 있는 것이다. 이 객체는 서버쪽의 Posts.find()문에 대하여 options 매개변수를 제공할 것이다. 서버쪽 코드를 이것을 구현한 것으로 바꾸어 보자:

    Meteor.publish('posts', function(options) {
      check(options, {
        sort: Object,
        limit: Number
      });
      return Posts.find({}, options);
    });
    
    Meteor.publish('comments', function(postId) {
      check(postId, String);
      return Comments.find({postId: postId});
    });
    
    Meteor.publish('notifications', function() {
      return Notifications.find({userId: this.userId});
    });
    
    server/publications.js

    매개변수 전달

    위의 발행 코드는 요컨데 find()문의 매개변수 options에 대하여 클라이언트(여기서는 {limit: postsLimit})가 어떤 Javascript 객체를 보내더라도 신뢰할 수 있다고 서버에게 말하는 것이다. 그러므로 사용자가 브라우저 콘솔을 통해서 임의의 options을 전송하는 것도 가능하다.

    우리의 경우, 이것은 상대적으로 해가 되지는 않는데, 사용자가 할 수 있는 것이 post를 다르게 재정렬하는 것이거나 또는 (우리가 처음에 원했던) 한계치를 변경하는 것이기 때문이다.

    그러나, 발행되지 않은 필드에 데이터를 저장할 때에는 이 패턴을 사용해서는 안되는 데, 사용자가 그것에 접근하려고 fields 옵션을 조작할 수 있기 때문이다. 그리고 아마도 그것을 find() 문의 셀렉터 매개변수에 사용하는 것도 동일한 보안상의 이유로 피했을 것이다.

    보다 안전한 패턴은 데이터를 확실하게 통제하기 위하여 전체 객체 대신에 개별 매개변수를 전달하는 것이다:

    Meteor.publish('posts', function(sort, limit) {
      return Posts.find({}, {sort: sort, limit: limit});
    });
    

    이제 route 수준에서 구독하려면, 같은 자리에 데이터 컨텍스트를 설정하는 것이 의미가 있다. 우리는 이전의 패턴에서 약간 벗어나서 data 함수가 커서를 리턴하는 대신에 Javascript 객체를 리턴하도록 할 것이다. 이렇게 하여 우리가 named data context를 생성하는데, 이를 posts라 부른다.

    이 의미는 단순히 템플릿 내에서 this로 은연중에 사용하는 대신에 데이터 컨텍스트에서 posts를 이용할 수 있게 될 것이다. 이 작은 요소는 제외하면 나머지 코드는 익숙할 것이다:

    //...
    
    Router.route('/:postsLimit?', {
      name: 'postsList',
      waitOn: function() {
        var limit = parseInt(this.params.postsLimit) || 5; 
        return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
      },
      data: function() {
        var limit = parseInt(this.params.postsLimit) || 5; 
        return {
          posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
        };
      }
    });
    
    //...
    
    lib/router.js

    라우터 수준에서 데이터 컨텍스트를 지정하였으므로, 우리는 posts_list.js 파일 내에 있는 posts 템플릿 헬퍼를 안전하게 제거할 수 있다. 그리고 우리가 데이터 컨텍스트를 posts라고 (헬퍼와 동일한 이름으로) 명명하였으므로 postsList 템플릿을 건들 필요도 없다!

    다시 보자. 새로운 개선된 router.js 코드는 다음과 같다:

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() { 
        return [Meteor.subscribe('notifications')]
      }
    });
    
    Router.route('/posts/:_id', {
      name: 'postPage',
      waitOn: function() {
        return Meteor.subscribe('comments', this.params._id);
      },
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/posts/:_id/edit', {
      name: 'postEdit',
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/submit', {name: 'postSubmit'});
    
    Router.route('/:postsLimit?', {
      name: 'postsList',
      waitOn: function() {
        var limit = parseInt(this.params.postsLimit) || 5; 
        return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
      },
      data: function() {
        var limit = parseInt(this.params.postsLimit) || 5; 
        return {
          posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
        };
      }
    });
    
    var requireLogin = function() {
      if (! Meteor.user()) {
        if (Meteor.loggingIn()) {
          this.render(this.loadingTemplate);
        } else {
          this.render('accessDenied');
        }
      } else {
        this.next();
      }
    }
    
    Router.onBeforeAction('dataNotFound', {only: 'postPage'});
    Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
    
    lib/router.js

    Commit 12-2

    제한을 걸기 위해서 postsList route에 코드를 추가했다.

    이 새로운 페이징 시스템을 체험해보자. 이제 URL 매개변수를 바꾸기만 하면 홈페이지의 post 숫자를 임의로 조정하여 보여줄 수 있는 상태가 되었다. 예를 들어, http://localhost:3000/3으로 접속해보면, 다음과 같은 화면을 보게 될 것이다:

    주소창에서 페이징 갯수를 조절하기.
    주소창에서 페이징 갯수를 조절하기.

    복수의 페이지가 아닌 이유?

    구글 검색결과와 같이 연속되는 10개의 post를 각각 보여주는 대신에 “무한 페이징” 방식을 사용하는 이유는 무엇일까? 그것은 사실은 미티어가 받아들인 실시간 패러다임 때문이다.

    구글 결과 페이징 패턴을 사용하여 Posts 컬렉션에 대한 페이징을 하는데, 현재 2 페이지에 위치하여 10번째에서 20번째의 post 목록을 보여주고 있다고 가정해보자. 만약 다른 사용자가 이전 10개의 post 중의 어느 것이라도 삭제한다면 무슨 일이 일어날까?

    우리 앱은 실시간이므로, 데이터 세트가 변경될 것이다. 10번 post는 이제 9번 post가 되므로, 우리 시야에서 빠지는 반면에, 11번 post는 범위안에 있게 된다. 궁극적 결과는 사용자는 아무런 이유도 없이 post목록이 갑자기 변하는 것을 보게 될 것이다!

    우리가 설사 이런 기묘한 UX를 받아들인다 해도, 전통적인 페이징 방식은 기술적 이유로도 구현하기가 어렵다.

    이전 예제로 돌아가보자. 우리는 Posts 컬렉션에서 10번째에서 20번째 까지의 post 목록을 발행하고 있다. 클라이언트에서 어떻게 그 post 목록을 찾을까? 클라이언트 쪽의 데이터 세트에는 10개의 post만 있으므로 10번째에서 20번째까지의 post 목록을 추출하지 못한다.

    한 가지 해법은 서버에서 그 10개의 post 목록을 발행하는 것이다. 그리고 클라이언트 쪽에서 Posts.find()를 호출하여 모든 발행 post 목록을 가져온다.

    이것은 하나의 구독만 있다면 동작한다. 그러나 곧 해보겠지만, post 구독을 하나 이상의 갯수로 시작하면 어떻게 하나?

    하나의 구독이 10에서 20까지의 post 목록을 요구한다고 해보자. 그리고 또 다른 것이 30에서 40까지의 목록을 요구한다고 하자. 이제 클라이언트에서 어느 구독에 속하는 지 모른채로 총 20개의 post를 가지게 된다.

    이런 이유로, 전통적인 페이징 방식은 미티어에서는 별 의미가 없다.

    Route Controller 만들기

    다음의 var limit = parseInt(this.params.postsLimit) || 5; 줄이 두 번 반복되었음을 눈치챘을지 모르겠다. 여기에, 숫자 “5”이 하드코딩된 것도 바람직하지 않다. 이것이 다는 아니다. 할 수만 있다면 DRY(Don’t Repeat Yourself) 원칙을 따르는 것은 항상 좋으니까, 이것을 어떻게 고치면 좋을 지 알아보자.

    Iron Router의 새로운 관점, Route Controller를 소개한다. Route controller는 어떤 route도 상속받을 수 있는 멋진 재사용가능한 패키지에 라우팅 기능들을 모아서 그룹을 만드는 간단한 방법이다. 여기서는 단일 route에 대하여만 이것을 사용하지만, 다음 장에서는 이 기능이 얼마나 편리한 지를 보게될 것이다.

    //...
    
    PostsListController = RouteController.extend({
      template: 'postsList',
      increment: 5, 
      postsLimit: function() { 
        return parseInt(this.params.postsLimit) || this.increment; 
      },
      findOptions: function() {
        return {sort: {submitted: -1}, limit: this.postsLimit()};
      },
      waitOn: function() {
        return Meteor.subscribe('posts', this.findOptions());
      },
      data: function() {
        return {posts: Posts.find({}, this.findOptions())};
      }
    });
    
    //...
    
    Router.route('/:postsLimit?', {
      name: 'postsList'
    });
    
    //...
    
    lib/router.js

    단계별로 해보자. 첫째, RouteController를 확장한 controller를 만든다. 다음, template 속성을 전에 했던 것처럼 지정한다. 그 다음, increment 속성을 지정한다.

    그리고 새로 limit 함수를 만들어 현재 목록 한계치를 리턴하게 한다. 그리고 options 객체를 리턴하는 findOptions 함수를 만든다. 지금은 불필요한 일 같지만 나중에 이를 사용할 것이다.

    다음 단계로, 이전처럼 waitOndata 함수를 정의한다. 여기서 새로 만든 findOptions 함수를 사용한다.

    마지막 단계로 할 일은 새로운 controller에 대한 route를 controller 속성에 postsList로 지정하는 것이다.

    Commit 12-3

    postsLists route 코드를 RouteController로 리팩토링하였다.

    더보기 링크 추가하기

    페이징이 작동한다. 그리고 코드도 좋다. 한 가지 문제만 남았다: URL을 수동으로 바꾸는 것 말고는 페이징을 실제로 이용할 방법이 없다. 이 상태로는 훌륭한 사용자 체험을 줄 수 없다. 이 문제를 해결해보자.

    우리가 원하는 것은 단순하다. Post 목록의 아랫쪽에 “load more” 버튼을 추가할 것이다. 이 버튼을 클릭할 때마다 보여지는 post 목록 갯수가 5씩 증가한다. 따라서 현재 위치가 URL http://localhost:3000/5인 주소에 있다면, “load more” 버튼을 누르면 http://localhost:3000/10 주소로 이동해야 한다. 벌써 이걸 알았다면, 산수 좀 할 줄 안다고 믿어주겠다!

    이전과 같이, 우리의 route에 페이징 로직을 추가하려고 한다. 우리가 익명의 커서를 사용하지 않고 명시적으로 데이터 콘텍스트를 명명했던 때가 언제인지 기억하는가? 글쎄, data 함수가 커서만 전달할 수 있다는 그런 규칙은 없다. 그러므로 “load more” 버튼의 URL을 사용하는데 동일한 기법을 사용할 것이다.

    //...
    
    PostsListController = RouteController.extend({
      template: 'postsList',
      increment: 5, 
      postsLimit: function() { 
        return parseInt(this.params.postsLimit) || this.increment; 
      },
      findOptions: function() {
        return {sort: {submitted: -1}, limit: this.postsLimit()};
      },
      waitOn: function() {
        return Meteor.subscribe('posts', this.findOptions());
      },
      posts: function() {
        return Posts.find({}, this.findOptions());
      },
      data: function() {
        var hasMore = this.posts().count() === this.postsLimit();
        var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
        return {
          posts: this.posts(),
          nextPath: hasMore ? nextPath : null
        };
      }
    });
    
    //...
    
    lib/router.js

    이 마술같은 라우터를 깊이있게 들여다보자. (현재 작동하는 PostsListController controller에서 상속받을) postsList route는 매개변수 postsLimit를 가진다는 점을 기억하기 바란다.

    그러므로 this.route.path(){postsLimit: this.postsLimit() + this.increment}를 지정하는 것은 postsList route에 그 Javascript 객체를 데이터 컨텍스트로 사용하는 경로를 구축하라는 의미이다.

    다른 말로 표현하면, 이것은 우리가 은연중에 this를 우리가 만든 데이터 컨텍스트로 바꾸는 것을 제외하면 {{pathFor 'postsList'}} Handlebars 헬퍼를 사용하는 것과 같다.

    우리는 그 경로를 채택하고 이를 템플릿의 데이터 컨텍스트에 추가하되, 보여줄 post들이 더 있을 때에만 그렇다. 우리가 하는 방식은 다소 기교적이다.

    this.limit()는 우리가 보여주려는 post의 현재 갯수를 리턴하는데 이 값은 현재 URL에 있는 값이거나 또는 URL에 매개변수가 없을 경우의 초기 설정값 (5)이다.

    한 편, this.posts는 현재 커서를 가리키므로, this.posts.count()는 커서에 실제로 존재하는 posts의 갯수를 가리킨다.

    그러므로 여기서 우리가 말하려는 것은 n개의 post를 요구하면 n개를 얻고, 계속 “load more” 버튼을 보여준다. 그러나 n개를 요구했는데 n개보다 적은 갯수를 얻게되면, 한계에 온 것을 의미하고 버튼을 보여주는 것을 중단하라는 것이다.

    말하자면, 우리 시스템은 다음의 경우에는 실패한다: 데이터베이스에 있는 아이템의 갯수가 정확하게 n개일 경우다. 만약 이 경우라면, 클라이언트는 n개를 요구하고 n개를 받고, 계속 “load more” 버튼을 보여주지만, 남은 아이템이 없다는 것은 모르고 있는 상태가 된다.

    안타깝지만, 이 문제를 간단하게 우회하는 방법은 없어서, 현재까지는 이 덜 완벽한 구현에 만족할 수 밖에 없다.

    남은 할 일은 post 목록의 아랫쪽에 “load more” 링크를 추가하는 것이다. 그리고 로드할 post가 더 있는 지를 보여주는 것이다:

    <template name="postsList">
      <div class="posts">
        {{#each posts}}
          {{> postItem}}
        {{/each}}
    
        {{#if nextPath}}
          <a class="load-more" href="{{nextPath}}">Load more</a>
        {{/if}}
      </div>
    </template>
    
    client/templates/posts/posts_list.html

    Post 목록 화면은 다음과 같이 보일 것이다:

    “load more” 버튼.
    “load more” 버튼.

    Commit 12-4

    Controller에 nextPath()을 추가하고 이를 post의 페이지 이동을 구현했다.

    더 나은 프로그레스 바

    페이징은 현재 잘 작동하지만, 한 가지 짜증나는 일이 있다: “load more” 버튼을 누를 때마다 라우터는 더 많은 post를 요구하고, Iron Router의 waitOn 기능으로 인해 새로운 데이터를 받을 때까지 loading 템플릿이 구동된다. 이 결과로 로딩될 때마다 페이지의 탑으로 이동해서 아랫쪽으로 스크롤해야 하는 사태가 일어난다.

    그러므로 먼저, Iron Router에게 구독을 waitOn하지 않도록 해야 한다. 대신 구독을 subscriptions hook 내부에 정의할 것이다.

    또한 데이터 컨텍스트에 this.postsSub.ready를 참조하는 ready 변수를 만들어 전달할 것이다. 이것은 post 구독의 로딩이 완료되는 시점을 템플릿에게 알려준다.

    //...
    
    PostsListController = RouteController.extend({
      template: 'postsList',
      increment: 5, 
      postsLimit: function() { 
        return parseInt(this.params.postsLimit) || this.increment; 
      },
      findOptions: function() {
        return {sort: {submitted: -1}, limit: this.postsLimit()};
      },
      subscriptions: function() {
        this.postsSub = Meteor.subscribe('posts', this.findOptions());
      },
      posts: function() {
        return Posts.find({}, this.findOptions());
      },
      data: function() {
        var hasMore = this.posts().count() === this.postsLimit();
        var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
        return {
          posts: this.posts(),
          ready: this.postsSub.ready,
          nextPath: hasMore ? nextPath : null
        };
      }
    });
    
    //...
    
    lib/router.js

    그리고 템플릿에서 이 ready 변수를 검사하여 post 목록의 하단에 새로운 post 목록을 로드하는 동안 spinner를 보여준다:

    <template name="postsList">
      <div class="posts">
        {{#each posts}}
          {{> postItem}}
        {{/each}}
    
        {{#if nextPath}}
          <a class="load-more" href="{{nextPath}}">Load more</a>
        {{else}}
          {{#unless ready}}
            {{> spinner}}
          {{/unless}}
        {{/if}}
      </div>
    </template>
    
    client/templates/posts/posts_list.html

    Commit 12-5

    spinner를 추가하여 페이징을 더 멋지게 만들었다.

    Post 열람페이지 접속

    우리는 현재 초기 설정값으로 첫 5개의 post를 보여주게 하고 있다. 그런데, 특정 post페이지로 브라우징하면 무슨 일이 일어날까?

    빈 템플릿.
    빈 템플릿.

    해보면, 빈 post 템플릿으로 채워진 페이지를 보게 될 것이다. 이것은 말이 된다: 우리는 라우터에게 postsList 경로를 로드할 때 posts 발행을 구독하라고 했지만, postPage 경로에 대하여는 지시한 바가 없다.

    그러나 지금까지, 우리가 알고 있는 방법은 n 개의 최근 post 목록에 구독하는 것이다. 그러면 한 개의 단일 post에 대하여 서버에 어떻게 요청할까? 여기서 약간의 비밀을 알려줄 것이다: 각 컬렉션에 한 개 이상의 발행이 가능하다!

    이 빠진 부분을 채워넣기 위해서, 우리는 _id로 구하는 하나의 post만을 발행하는 새로운 singlePost 구독을 만든다.

    Meteor.publish('posts', function(options) {
      return Posts.find({}, options);
    });
    
    Meteor.publish('singlePost', function(id) {
      check(id, String)
      return Posts.find(id);
    });
    
    //...
    
    server/publications.js

    이제 클라이언트 쪽의 post 목록을 구독한다. 우리는 이미 postPage route의 waitOn 함수에 comments 발행을 구독하고 있다. 그래서 여기에 singlePost에 대한 구독을 추가한다. 그리고 같은 데이터를 요구하는 postEdit route에도 동일한 구독을 추가하는 것을 잊지 말라.:

    //...
    
    Router.route('/posts/:_id', {
      name: 'postPage',
      waitOn: function() {
        return [
          Meteor.subscribe('singlePost', this.params._id),
          Meteor.subscribe('comments', this.params._id)
        ];
      },
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    Router.route('/posts/:_id/edit', {
      name: 'postEdit',
      waitOn: function() { 
        return Meteor.subscribe('singlePost', this.params._id);
      },
      data: function() { return Posts.findOne(this.params._id); }
    });
    
    //...
    
    lib/router.js

    Commit 12-6

    항상 올바른 post를 열람하도록 단일 post 구독을 사용하였다.

    페이징이 구현되니 우리 앱은 이제 더 이상 확장성 문제로 어려움을 겪지 않는다. 그리고 사용자들은 전보다 더 많은 링크를 등록할 수 있다. 이제 이 링크에 순위를 매기는 방법이 있다면 좋지 않을까? 여러분이 생각하시는 대로, 이것이 바로 다음 장의 주제이다!