-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy path12-pagination.md.erb
535 lines (401 loc) · 24.8 KB
/
12-pagination.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
---
title: 페이지 만들기
slug: pagination
date: 0012/01/01
number: 12
contents: 미티어의 구독에 대하여 더 상세하게 배운다. 그리고 이를 이용하여 데이터를 제어하는 법을 배운다.|무한 방식의 페이지 만들기를 구현한다.|`iron-router-progress`패키지를 이용하여 멋진 iOS 스타일의 프로그레스 바를 구현한다.|Post 페이지에 대한 직접 링크를 처리하는 특별한 구독을 만든다.
paragraphs: 67
---
Microscope는 훌륭해 보인다. 그리고 우리는 이것이 세계에 모습을 드러낼 때 큰 환영을 기대한다.
그래서 우리는 이것이 등장할 때, 등록될 수 많은 post들로 인한 성능에의 영향을 걱정을 할 수도 있다!
우리는 앞서 클라이언트 쪽의 컬렉션이 서버에 있는 데이터의 부분집합을 가지는 방법에 대하여 다룬 적이 있다. 그리고 알림과 댓글 컬렉션에서 이 방식을 구현하여 왔다.
현재까지는 여전히 우리는 모든 post를 연결된 모든 사용자에게 발행하고 있다. 결국 수 천 개의 링크가 등록된다면 이것은 문제가 될 것이다. 이를 위해서 우리의 post에 대하여 페이징 처리를 할 필요가 있다.
### 더 많은 Post 추가하기
우선, 초기 설정 데이터에 페이징이 의미를 가지는 충분한 post 목록을 로드하도록 한다:
~~~js
// 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
});
}
}
~~~
<%= caption "server/fixtures.js" %>
<%= highlight "15~24" %>
`meteor reset`을 실행하고 나면, 다음과 같은 화면을 보게 될 것이다:
<%= screenshot "12-1", "더미 데이터 보이기. " %>
<%= 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` 구독만 남겨둔다:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('notifications')]
}
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "5" %>
그 다음, 경로에 매개변수 `postsLimit`를 추가한다. 매개변수 명 다음에 `?`를 추가하는 것은 이것이 선택적이라는 의미이다. 그러므로 route는 `http://localhost:3000/50`뿐 아니라 이전의 `http://localhost:3000`도 수용한다.
~~~js
//...
Router.route('/:postsLimit?', {
name: 'postsList',
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "3" %>
유의할 사항은 `/:paramter?`의 형태를 가지는 경로는 모든 가능한 경로와 다 맞는다는 점이다. 각 route가 순차적으로 파싱이 이루어지면서 현재 경로와 맞는 것을 찾으므로, route의 순서를 특수성을 줄이는 방향으로 정렬을 시켜야 한다.
다른 말로 표현하면, `/posts/:_id` 와 같이 특정한 route를 타겟으로 하는 route는 먼저 오고 `postList` route는 거의 모든 경로와 맞게 되므로 마지막으로 이동하여야 한다.
이제 올바른 데이터를 찾고 구독하는 어려운 문제를 해결해야 할 시간이다. 우리는 매개변수 `postsLimit`의 값이 없는 경우 여기에 초기값을 지정하는 방법을 처리해야 한다. 초기값으로는 “5”를 지정하여 페이징처리 과정에서 충분한 여유를 두려고 한다.
~~~js
//...
Router.route('/:postsLimit?', {
name: 'postsList',
waitOn: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
}
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "7~9" %>
주목할 점은 `posts` 발행의 이름 다음에 JavaScript 객체({limit: postsLimit})를 매개변수로 전달하고 있는 것이다. 이 객체는 서버쪽의 `Posts.find()`문에 대하여 `options` 매개변수를 제공할 것이다. 서버쪽 코드를 이것을 구현한 것으로 바꾸어 보자:
~~~js
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});
});
~~~
<%= caption "server/publications.js" %>
<%= highlight "1~7" %>
<% note do %>
### 매개변수 전달
위의 발행 코드는 요컨데 `find()`문의 매개변수 `options`에 대하여 클라이언트(여기서는 `{limit: postsLimit}`)가 어떤 Javascript 객체를 보내더라도 신뢰할 수 있다고 서버에게 말하는 것이다. 그러므로 사용자가 브라우저 콘솔을 통해서 임의의 options을 전송하는 것도 가능하다.
우리의 경우, 이것은 상대적으로 해가 되지는 않는데, 사용자가 할 수 있는 것이 post를 다르게 재정렬하는 것이거나 또는 (우리가 처음에 원했던) 한계치를 변경하는 것이기 때문이다.
그러나, 발행되지 않은 필드에 데이터를 저장할 때에는 이 패턴을 사용해서는 안되는 데, 사용자가 그것에 접근하려고 `fields` 옵션을 조작할 수 있기 때문이다. 그리고 아마도 그것을 `find()` 문의 셀렉터 매개변수에 사용하는 것도 동일한 보안상의 이유로 피했을 것이다.
보다 안전한 패턴은 데이터를 확실하게 통제하기 위하여 전체 객체 대신에 개별 매개변수를 전달하는 것이다:
~~~js
Meteor.publish('posts', function(sort, limit) {
return Posts.find({}, {sort: sort, limit: limit});
});
~~~
<% end %>
이제 route 수준에서 구독하려면, 같은 자리에 데이터 컨텍스트를 설정하는 것이 의미가 있다. 우리는 이전의 패턴에서 약간 벗어나서 `data` 함수가 커서를 리턴하는 대신에 Javascript 객체를 리턴하도록 할 것이다. 이렇게 하여 우리가 *named* data context를 생성하는데, 이를 `posts`라 부른다.
이 의미는 단순히 템플릿 내에서 `this`로 은연중에 사용하는 대신에 데이터 컨텍스트에서 `posts`를 이용할 수 있게 될 것이다. 이 작은 요소는 제외하면 나머지 코드는 익숙할 것이다:
~~~js
//...
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})
};
}
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "9~14" %>
라우터 수준에서 데이터 컨텍스트를 지정하였으므로, 우리는 `posts_list.js` 파일 내에 있는 `posts` 템플릿 헬퍼를 안전하게 제거할 수 있다. 그리고 우리가 데이터 컨텍스트를 `posts`라고 (헬퍼와 동일한 이름으로) 명명하였으므로 `postsList` 템플릿을 건들 필요도 없다!
다시 보자. 새로운 개선된 router.js 코드는 다음과 같다:
~~~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'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "6,25~37" %>
<%= commit "12-2", "제한을 걸기 위해서 postsList route에 코드를 추가했다." %>
이 새로운 페이징 시스템을 체험해보자. 이제 URL 매개변수를 바꾸기만 하면 홈페이지의 post 숫자를 임의로 조정하여 보여줄 수 있는 상태가 되었다. 예를 들어, `http://localhost:3000/3`으로 접속해보면, 다음과 같은 화면을 보게 될 것이다:
<%= screenshot "12-2", "주소창에서 페이징 갯수를 조절하기. " %>
<% note do %>
### 복수의 페이지가 아닌 이유?
구글 검색결과와 같이 연속되는 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를 가지게 된다.
이런 이유로, 전통적인 페이징 방식은 미티어에서는 별 의미가 없다.
<% end %>
### Route Controller 만들기
다음의 `var limit = parseInt(this.params.postsLimit) || 5;` 줄이 두 번 반복되었음을 눈치챘을지 모르겠다. 여기에, 숫자 “5”이 하드코딩된 것도 바람직하지 않다. 이것이 다는 아니다. 할 수만 있다면 DRY(Don't Repeat Yourself) 원칙을 따르는 것은 항상 좋으니까, 이것을 어떻게 고치면 좋을 지 알아보자.
Iron Router의 새로운 관점, *Route Controller*를 소개한다. Route controller는 어떤 route도 상속받을 수 있는 멋진 재사용가능한 패키지에 라우팅 기능들을 모아서 그룹을 만드는 간단한 방법이다. 여기서는 단일 route에 대하여만 이것을 사용하지만, 다음 장에서는 이 기능이 얼마나 편리한 지를 보게될 것이다.
~~~js
//...
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'
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "3~18, 25" %>
단계별로 해보자. 첫째, `RouteController`를 확장한 controller를 만든다. 다음, `template` 속성을 전에 했던 것처럼 지정한다. 그 다음, `increment` 속성을 지정한다.
그리고 새로 `limit` 함수를 만들어 현재 목록 한계치를 리턴하게 한다. 그리고 options 객체를 리턴하는 `findOptions` 함수를 만든다. 지금은 불필요한 일 같지만 나중에 이를 사용할 것이다.
다음 단계로, 이전처럼 `waitOn`과 `data` 함수를 정의한다. 여기서 새로 만든 `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을 사용하는데 동일한 기법을 사용할 것이다.
~~~js
//...
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
};
}
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "15~25" %>
이 마술같은 라우터를 깊이있게 들여다보자. (현재 작동하는 `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가 더 있는 지를 보여주는 것이다:
~~~html
<template name="postsList">
<div class="posts">
{{#each posts}}
{{> postItem}}
{{/each}}
{{#if nextPath}}
<a class="load-more" href="{{nextPath}}">Load more</a>
{{/if}}
</div>
</template>
~~~
<%= caption "client/templates/posts/posts_list.html" %>
<%= highlight "7~10" %>
Post 목록 화면은 다음과 같이 보일 것이다:
<%= screenshot "12-3", "“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 구독의 로딩이 완료되는 시점을 템플릿에게 알려준다.
~~~js
//...
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
};
}
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "12~14, 23" %>
그리고 템플릿에서 이 `ready` 변수를 검사하여 post 목록의 하단에 새로운 post 목록을 로드하는 동안 spinner를 보여준다:
~~~html
<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>
~~~
<%= caption "client/templates/posts/posts_list.html" %>
<%= highlight "10~12" %>
<%= commit "12-5", "spinner를 추가하여 페이징을 더 멋지게 만들었다." %>
### Post 열람페이지 접속
우리는 현재 초기 설정값으로 첫 5개의 post를 보여주게 하고 있다. 그런데, 특정 post페이지로 브라우징하면 무슨 일이 일어날까?
<%= screenshot "12-4", "빈 템플릿." %>
해보면, 빈 post 템플릿으로 채워진 페이지를 보게 될 것이다. 이것은 말이 된다: 우리는 라우터에게 `postsList` 경로를 로드할 때 `posts` 발행을 구독하라고 했지만, `postPage` 경로에 대하여는 지시한 바가 없다.
그러나 지금까지, 우리가 알고 있는 방법은 `n` 개의 최근 post 목록에 구독하는 것이다. 그러면 한 개의 단일 post에 대하여 서버에 어떻게 요청할까? 여기서 약간의 비밀을 알려줄 것이다: 각 컬렉션에 한 개 이상의 발행이 가능하다!
이 빠진 부분을 채워넣기 위해서, 우리는 `_id`로 구하는 하나의 post만을 발행하는 새로운 `singlePost` 구독을 만든다.
~~~js
Meteor.publish('posts', function(options) {
return Posts.find({}, options);
});
Meteor.publish('singlePost', function(id) {
check(id, String)
return Posts.find(id);
});
//...
~~~
<%= caption "server/publications.js" %>
<%= highlight "5~7" %>
이제 클라이언트 쪽의 post 목록을 구독한다. 우리는 이미 `postPage` route의 `waitOn` 함수에 `comments` 발행을 구독하고 있다. 그래서 여기에 `singlePost`에 대한 구독을 추가한다. 그리고 같은 데이터를 요구하는 `postEdit` route에도 동일한 구독을 추가하는 것을 잊지 말라.:
~~~js
//...
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); }
});
//...
~~~
<%= caption "lib/router.js" %>
<%= highlight "6~9,16~18" %>
<%= commit "12-6","항상 올바른 post를 열람하도록 단일 post 구독을 사용하였다." %>
페이징이 구현되니 우리 앱은 이제 더 이상 확장성 문제로 어려움을 겪지 않는다. 그리고 사용자들은 전보다 더 많은 링크를 등록할 수 있다. 이제 이 링크에 순위를 매기는 방법이 있다면 좋지 않을까? 여러분이 생각하시는 대로, 이것이 바로 다음 장의 주제이다!