0. 강좌 소개

무한 스크롤을 구현하다 보면 자연스럽게 만나게 되는 문제가 있는데 그것은 브라우저 뒤로가기 문제입니다. 실제로 이 문제 때문에 무한 스크롤에 대한 부정적인 의견을 보인 개발자들도 있고, 이들에 의해 무한스크롤에 대한 나름 의미 있는 비판도 있었습니다. 하지만 시간이 지나고, www.discourse.org와 같이 이 뒤로가기 문제를 해결한 서비스가 하나둘씩 나타나게 되고, 모바일 환경에서 사용자 들이 느끼는 직관성 때문에 무한스크롤은 가장 보편적인 페이징 기법으로 자리매김 하고 있는 것 같습니다.

Meteor를 본격적으로 사용해볼 생각을 하면서 저 역시 이 문제를 해결해야만 했습니다.
하지만 국내 문서를 이리저리 뒤져봤지만 이 문제를 깔끔하게 해결한 답은 아직 찾지 못했습니다. 그래도 Meteor 커뮤니티나, 혹은 Stack Overflow 등에 올라온 여러 내용들을 조합하고, 응용해서 나름 이 문제를 해결한 결과물을 만들게 되었습니다. 이 강좌는 이 무한스크롤에 대한 저의 삽질기가 되겠습니다. 1~4번까지는 무한스크롤을 만드는 방법에 관한 내용입니다. 그리고 마지막 5번은 무한스크롤의 문제점과 이 해결방법에 관한 내용입니다. 이미 무한스크롤을 사용하시는 분은 5번만 보셔도 상관 없을 것 같습니다.


1. 환결 설정

1.1 폴더 구조

자 그럼 본격적으로 강좌를 시작해 보겠습니다.

일단 Meteor에 대한 기본 설치는 다음 강좌를 참고해 주시기 바랍니다. 아래 강좌에서 설치에 대한 부분만 참고 하시면 되겠습니다.
https://freeseamew.github.io/2017/07/02/meteor-account-tutorial-1/

설치후에 다음과 같은 구조로 파일들을 만들어 주세요.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
meteorAccount
├── .meteor
├── client
│ ├── styles
│ │ └── main.css
│ ├── contents
│ │ ├── post-lists.html
│ │ ├── post-lists.js
│ │ ├── post-view.html
│ │ └── post-view.js
│ ├── autoscroll.js
│ ├── main-layout.html
│ └── main-layout.js
├── lib
│ ├── methods
│ │ └── postMethods.js
│ ├── collections.js
│ └── routes.js
├── node_module
├── public
└── server
├── fixture.js
└── pubs.js

1.2 설치 페키지

이번 예제에 사용할 페키지들은 다음과 같습니다.
우선 라우터 관련 페키지를 설치해 주세요.

설치 패키지 : 라우터 관련

  • meteor add kadira:flow-router
  • meteor add arillo:flow-router-helpers
  • meteor add kadira:blaze-layout

다음은 Subscription 캐쉬 입니다. 이부분에 대해서는 강좌중에 설명드리도록 하겠습니다.

설치 패키지 : Subscription 캐쉬

  • meteor add meteorhacks:subs-manager
  • meteor add msavin:mongol

마지막으로 템플릿에 사용할 bootstrap 관련 패키지를 설치하면 기본 설정은 끝입니다.

설치 패키지 : 기타

  • meteor add twbs:bootstrap@=3.3.6

2. 기본 서버 작업

2.1 collection 작성

이번 예제에 사용할 콜랙션은 posts라는 단일 콜랙션을 사용할 것입니다. 다음을 참고로 해당 콜랙션을 만들어 주세요. 참고로 Meteor에서의 콜랙션은 일종의 데이터베이스에서의 테이블에 해당하는 부분입니다. Meteor에서는 collection설정을 통해 서버의 몽고디비와 클라이언트의 미니몽고를 연동해줍니다. 이부분은 다른 강좌에서 좀더 깊이있게 다루도록 하겠습니다. 간단하게는 일종의 데이터베이스 설정? 정도로 이해해 주시면 되겠습니다.

lib/collections.js

1
Posts = new Mongo.Collection('posts');

2.2 pubs 작성

pubs는 Meteor에서 데이터 베이스의 내용을 가져오는 방법중에 하나입니다. pub(발행)/sub(구독)이라는 메커니즘을 통해서 필요한 데이터를 몽고디비에서 미니몽고로 받아오는 구조입니다. 이부분도 나중에 다른 강좌를 통해서 조금더 자세히 다루도록 하겠습니다.

우선 posts는 리스트를 뿌려주는 일종의 쿼리부분이 되겠습니다. 일반 데이터베이스 였다면 select 컬럼들 from 테이블 limit 보여줄 개수 정도의 쿼리를 호출한다고 생각하시면 되겠습니다.

다음으로 postDetail은 리스트에서 선택된 post의 내용을 보여주는 부분입니다.
select 컬럼들 from 테이블명 where id=id 이런 식의 쿼리에 해당하는 내용입니다.

이제 아래 코드를 입력해 주세요.

server/pubs.js

1
2
3
4
5
6
7
Meteor.publish('posts', function (postCnt) {
return Posts.find({}, {limit: postCnt, sort: {postDate: -1}});
});
Meteor.publish('postDetail', function (id) {
return Posts.find({_id: id}); // 여기서 findOne를 사용하면 두번 리턴되는 문제 발생
});

2.3 methods 작성

method는 pub/sub과 함께 데이터베이스를 다루는 방법입니다. pub/sub이 리스트 형식의 데이터를 가져오는데 주로 사용된다면, method는 쓰기, 수정, 삭제에 주로 사용됩니다.
여기서는 select count(id) from 테이블에 해당하는 쿼리를 처리하기 위해 사용했습니다.

lib/methods/postMethods.js

1
2
3
4
5
6
Meteor.methods({
getPostCountAll: function () {
return Posts.find().count();
}
});

3. 라우팅 세팅

라우팅에 대해 간단히 설명드리면 http://localhost:3000/postList 주소로 url을 입력하면 postList라는 template이 mainLayout이라는 곳에 보여 진다는 의미를 가집니다. 다시 설명 하자면, url주소에 따라 postList, postView 등의 페이지로 이동하게 하는 기능이 되겠습니다.

라우팅될 페이지는 총 2개의 페이지가 되겠습니다. /postList는 무한스크롤 페이지가 되겠고, /postView는 리스트에서 선택된 목록의 내용을 보는 페이지가 되겠습니다. 그리고 / 의 경우는 아이피 주소로 처음 접근한 사용자를 리스트 페이지로 보내주도록 하는 기능입니다.

postView의 경우 :id로 표시되는 부분이 있는데요. 이부분은 상세보기 페이지의 id 값이 되겠습니다. 리스트에서 a href로 링크를 걸어줄때 http://localhost:3000/postView/iFcDNXidApCtyCFRt 이런 형태의 주소가 될텐데요. 여기서 postView/ 뒤에 있는 값이 ID값이라고 인식시켜 주는 방법입니다. 그리고 이 키 값을 var id = FlowRouter.getParam('id'); 이런 식으로 변수에 담아 사용 할 수 있습니다.

이제 routes.js를 열고 다음을 입력해 주세요.

lib/routes.js

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
FlowRouter.route('/', {
name: 'main',
action: function() {
FlowRouter.go('/postList');
}
});
FlowRouter.route('/postList', {
name: 'main',
action: function() {
BlazeLayout.render('mainLayout', {
content:'postList'
});
}
});
FlowRouter.route('/postView/:id', {
name: 'postView',
action: function () {
var id = FlowRouter.getParam('id');
BlazeLayout.render('mainLayout', {
content: 'postView'
});
}
});

다음은 위에 잠시 설명한 주소에 따라 템플릿을 뿌려줄 일종의 부모 템플릿을 설정하는 파일이 되겠습니다. client/main-layout.js 에 파일을 만들고 내용을 입력해 주세요.

client/main-layout.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template name="mainLayout">
<header class="navbar navbar-default navbar-fixed-top table-header-bg" >
<div class="container ">
<div class="navbar-header">
<a href="/" class="navbar-brand" name="btn-home">Infinite Scroll</a>
</div>
<div class="collapse navbar-collapse navbar-right navbar-ex1-collapse">
<div class="navbar-form navbar-right" ng-controller="authCtrl as auth ">
</div>
</div>
</div>
</header>
<div class="container">
{{> Template.dynamic template=content }}
</div>
</template>

4. 리스트 및 내용보기 페이지 작성

4.1 포스트 리스트

이제 리스트 페이지를 작성해 보도록 하겠습니다.
우선 이번프로젝트에 사용할 css 파일을 작성하도록 하겠습니다. 다음 링크의 css파일의 내용을 복사해 주세요.

client/styles/main.css

https://github.com/freeseamew/meteor-infinit-scroll/blob/master/client/styles/main.css

다음으로 아래를 참고로 post-list.html 파일을 만들겠습니다. 이 파일에는 postList, postListItem, postLoadingItem 의 3가지 template을 사용할 예정입니다. 참고로 Meteor는 기본적으로 blaze라는 프론트엔드 엔진을 사용하는데, post-list.html, post-list.js 이런 식으로 동일 템플릿에 해당하는 html과 이 템플릿에 대응하는 js 파일로 이루어 지는 구조입니다.

코드를 설명드리면 #each list 에 해당하는 부분은 list라는 데이터를 each문을 사용해서 >postListItem 이 템플릿을 반복시켜 주는 기능을 합니다. 그리고 #if endOfData 에서는 만약 뿌려줄 리스트가 더이상 없을 경우 마지막 페이지라는 것을 표시해줍니다. 또 #if postLoadingEffect#unless Template.subscriptionsReady는 데이터가 로딩중이거나, 템플릿이 준비가 완료되지 않을 경우 로딩 효과를 불러오는 역할을 합니다.

client/contents/post-lists.html

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
<template name="postList">
<div class="row margin-top-20">
<div class="col-xs-12 col-md-12 table-border tableHeight padding-0 " >
<table class="table" >
<thead class=" table-header-bg">
<tr>
<th class="tb-title">제목</th>
<th class="align-center tb-user">사용자</th>
<th class="align-center tb-view">조회</th>
<th class="align-center tb-comment">댓글</th>
<th class="align-centr tb-recommend">추천</th>
<th class="align-right tb-date">날짜</th>
</tr>
</thead>
<tbody id="tableBoardList" ss>
{{#each list}}
{{>postListItem}}
{{/each}}
{{#if endOfData}}
<p>더이상 데이터가 없습니다.</p>
{{/if}}
</tbody>
</table>
{{#if postLoadingEffect}}
{{>postLoadingItem}}
{{/if}}
{{#unless Template.subscriptionsReady}}
{{>postLoadingItem}}
{{/unless}}
</div>
</div>
</template>

다음은 같은 파일에 리스트 내용에 해당하는 템플릿을 만들어 보겠습니다. 위에서 설명드린 each를 통해 반복되면서 표시되는 리스트의 내용이라고 보시면 되겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
.
.
[위 상략]
<template name="postListItem">
<tr id="my-infinite-scrolling-list" pageCnt="{{pageCnt}}" >
<td class="tb-title"><p><a href="{{pathFor '/postView/:id' id=_id}}" name="#{{_id}}" class="goView" >{{postTitle}}</a></p></td>
<td class="align-center tb-user"><p><a href="{{pathFor '/postView/:id' id=_id}}" class="goView">{{postUserName}}</a></p></td>
<td class="align-center tb-view"><p><a href="{{pathFor '/postView/:id' id=_id}}" class="goView">{{postViewCount}}</a></p></td>
<td class="align-center tb-comment"><p><a href="{{pathFor '/postView/:id' id=_id}}" class="goView">{{postCommentCount}}</a></p></td>
<td class="align-center tb-recommend"><p><a href="{{pathFor '/postView/:id' id=_id}}" class="goView">{{postLikeCount}}</a></p></td>
<td class="align-right tb-date"><p><a href="{{pathFor '/countTest'}}" class="goView">{{postDate}}</a></p></td>
</tr>
</template>

마지막 템플릿은 로딩효과를 표시해줄 템플릿 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
.
.
.
[위 상략]
<template name="postLoadingItem">
<div class="loading-bar">
<div class="loader"></div>
</div>
</template>
`client/contents/post-lists.html`

이제 템플릿을 제어할 js 파일을 만들어 보도록 하겠습니다. 우선 Template.postList.onCreated(function () { 안의 내용을 작성하겠습니다. 각 내용에 필요한 부분들은 주석을 통해 설명해 두었습니다. 사실 ReactiveVar나 pub/sub에 관한 자세한 내용은 나중에 따로 강좌를 만들어 설명해 드리도록 하겠습니다. 하지만 작동 원리에 대한 시나리오는 간단히 설명드리겠습니다.

self.autorun(function () {}안에 정의된 self.subscribe('posts', postCnt.get());가 이 시스템의 가장 핵심적인 부분입니다. Meteor는 절차적인 방식과 함께 선언적인 방식을 함께 지원하는 플랫폼 입니다. 여기서의 pub/sub 을 사용하는 방법은 선언적인 방식을 이용해 페이징을 구현한 부분입니다. 만약 일반적인 절차적인 방식으로 게시글의 리스트를 불러온다면 페이지를 추가로 불러올 부분에서 데이터베이스를 호출할 function을 호출하는 방법으로 작동이 될 것입니다. 하지만, 여기서는 self.subscribe('posts', postCnt.get()); 이렇게 선언을 해주고 페이지를 늘려야 하는 경우 postCnt.set(postCnt.get() + 10); 이런식으로 postCnt만 조정해주면 서버에서 필요한 부분만 추가되는 식으로 작동합니다. 즉 subscribe를 선언해두고 Reactivar로 정의된 부분만 변경해 주면 위치에 상관없이 리스트의 데이터가 변화된다고 생각하시면 되겠습니다. 일단 이부분에 대한 제 강좌가 없으므로 다음 링크를 통해 간단하게 나마 원리를 보실 수 있을 것입니다.

링크 : pub/sub 설명

client/contents/post-lists.js

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
Template.postList.onCreated(function () {
window.postCountAll = new ReactiveVar(0); // post의 총 개수를 저장할 저장소
window.postLoadingEffect = new ReactiveVar(false); // 로딩 효과를 나타낼지 말지를 결정할 저장소
window.scrollRock = new ReactiveVar(false); // 스크롤바가 로딩중에는 더이상 추가로딩을 하지 않도록 할 저장소
window.postCnt = new ReactiveVar(20); // 불러올 포스트 개수 세팅
var self = this;
Meteor.call('getPostCountAll', function (err, result) {
if(err) {
console.log(err.message);
}
else {
return postCountAll.set(result); // 포스트 개수를 method에서 불러와 postCountAll에 저장.
}
});
self.autorun(function () {
self.subscribe('posts', postCnt.get()); // posts 리스트를 불로올 posts pub를 호출
});
window.onscroll = function (ev) {
if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight) { // 스크롤이 바닥에 닿으면 다음 페이지 진행
if(!(postCountAll.get() < postCnt.get()) && scrollRock.get() === false ) { // 페이지가 끝나지 않았거나 로딩중이 아닐때에만 페이지수를 증가하는 이벤트 발생
scrollRock.set(true); // 페이지 증가 전에 scrollRock을 true로 해서 잠금.
postLoadingEffect.set(true);
Meteor.setTimeout(function () {
postLoadingEffect.set(false);
scrollRock.set(false);
}, 1000);
postCnt.set(postCnt.get() + 10); // 패아자룰 10개 증가 시켜 줌.
console.log('postCnt: ' + postCnt.get()); // 페이지에 대한 로그
}
}
}
});

다음은 Template.postList.helpers({}) 로 템플릿에 데이터를 전달하는 부분입니다.

list는 리스트에 뿌려질 포스트 목록 데이터 입니다. endOfData는 현재 불러온 포스트 개수가 전체 포스트 개수보다 크거나 같으면 false를 리턴해 더이상 페이지 증가 이벤트가 작동하지 않도록 하는 역할을 합니다. postLoadingEffect는 true일 경우 로딩을 보여줄지 말지에 대한 true, false를 나타내는 역할을 합니다.

client/contents/post-lists.js

1
2
3
4
5
6
7
8
9
10
11
12
Template.postList.helpers({
list: function () {
return Posts.find({}, {sort: {postDate: -1}});
},
endOfData : function () {
return postCountAll.get() < postCnt.get() ? true : false;
},
postLoadingEffect: function () {
return postLoadingEffect.get();
}
});

4.2 포스트 내용보기

다음은 내용보기 입니다. 리스트의 링크를 클릭하면 선택된 리스트의 내용을 보여주는 페이지가 되겠습니다.
우선 템플릿부터 작성하도록 하겠습니다.

client/contents/post-view.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template name="postView">
{{#with contentView }}
<div class="row margin-top-20">
<div class="col-xs-12 col-md-12 " >
<div class="content-wrap">
<div class="post-content-top">
<h1>{{postTitle}}</h1>
</div>
<div class="post-content-top">
<p>{{postUserName}}</p>
<p class="post-date">{{postDate}}</p>
</div>
<div class="post-content-middle">
{{{postContent}}}
</div>
</div>
</div>
</div>
{{/with}}
</template>

그리고 대응하는 js 파일을 작성합니다. post-list보다는 단순하지만 구조는 비슷합니다. self.subscribe('postDetail', postIdSet.get());를 선언하고 postIdSet을 받아 내용을 전달하는 것이 전부입니다. 참고로 postIdSet은 route에서 전달받고 있습니다. (위에 라우팅 부분 참고)

client/contents/post-view.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Template.postView.onCreated(function () {
var self = this;
self.autorun(function ()
{
self.subscribe('postDetail', postIdSet.get());
});
});
Template.postView.helpers({
contentView: function()
{
var postOne = Posts.findOne({_id: postIdSet.get()});
return postOne;
}
});

4.3 데이터 주입

이제 기본적인 페이지들은 모두 만들었습니다. 다음으로 해야할 일은 리스트로 사용할 데이터를 주입하는 일입니다.
코드를 설명드리면 Meteor가 시작될 때 디비에 데이터가 없으면 fixture를 for 문으로 돌면서 데이터베이스에 주입하는 소스입니다. 다음 링크에서 소스를 복사해 사용하시면 되겠습니다.

링크 : fixture.js

server/fixture.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var fixture = [
.
.
.
데이터
.
.
.
]
Meteor.startup(() => {
if(Posts.find().count() === 0) {
console.log('데이터가 존재하지 않습니다 fixture 데이터를 입력합니다.');
for(var i=0, len=250; i<len; i++) {
Posts.insert((fixture[i]));
}
}
});

이제 터미널 창에서 다음 명령을 실행해 서버를 실행해 보겠습니다. 정상적으로 작동한다면, http://localhost:3000 로 페이지에 접속할 수 있을 것입니다.

1
meteor run

여기까지는 무한스크롤을 구성하는 강좌였습니다. 이어서 바로 다음 강좌에서는 무한스크롤의 문제점과 그 해결방안에 대해서 다루어 보도록 하겠습니다.

소스코드 링크
https://github.com/freeseamew/meteor-infinit-scroll