MODERN WEB APPLICATION
WITH METEOR
이재호
Appsoulute 대표
jhlee@appsoulute.com
http://github.com/acidsound
http://spectrumdig.blogspot.kr
INSTALL METEOR
Linux/OS X curl https://install.meteor.com/ | sh 

Windows https://install.meteor.com/windows
첫 METEOR APP
• meteor로 시작하는 명령은
터미널이나 커맨드라인(시작
>실행>cmd)에서 입력합니
다.
• meteor create sogon2x
APP 실행하기
• meteor run / meteor
구현 목표
관심사 기반 마이크로 블로깅 서비스
1. 화면생성
2. 포스트 입력
3. 이벤트 처리
4. 포스트 정렬
5. 사용자 계정
6. 구독/탈퇴
7. 대쉬보드
백문불여일타
(百聞不如一打)
한타 한타 시작해봅시다
TOOL
• 어떤 걸로 코드를 만드실 건
가요?
• ATOM (무료 추천!)
• Sublime text (인기!)
• Webstorm (유료 최고!)
JAVASCRIPT 구조
CLIENT
if (Meteor.isClient) {
}
SERVER
if (Meteor.isServer) {
Meteor.startup(function() {
// code to run on server at startup
});
}
사용자 

브라우저에서
실행합니다.
서버에서
실행합니다.
HTMLTEMPLATE
(mobile first!)
index.html
<head>
<title>sogon2x</title>
<meta name="viewport" content="width=device-
width, initial-scale=1">
</head>
<body>
{{> head}}
{{> main}}
</body>
head.html
<template name="head">
<h1>fixed header</h1>
</template>
main.html
<template name="main">
<p>context</p>
</template>
emmet- meta:vp
HEAD
• https://atmospherejs.com/twbs/bootstrap
• meteor add twbs:bootstrap
• 적용 후 변화를 관찰
• navbar 사용

http://bootstrapk.com/components/#navbar-brand-
image
HEAD - NAVBAR
<template name="head">
<nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Sogon2X</a>
</div>
</div>
</nav>
</template>
HEAD - NAVBAR
<template name="main">
<nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Sogon2X</a>
</div>
</div>
</nav>
</template> 상단 네비게이션 바
HEAD - NAVBAR
<template name="main">
<nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Sogon2X</a>
</div>
</div>
</nav>
</template>
기본 색상

navbar-inverse도 시도
HEAD - NAVBAR
<template name="main">
<nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Sogon2X</a>
</div>
</div>
</nav>
</template>
상단 고정 (optional)
HEAD - NAVBAR
<template name="main">
<nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Sogon2X</a>
</div>
</div>
</nav>
</template>
컨테이너
http://bootstrapk.com/css/#overview-container
HEAD - NAVBAR
<template name="main">
<nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Sogon2X</a>
</div>
</div>
</nav>
</template> Header 영역
HEAD - NAVBAR
<template name="main">
<nav class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">Sogon2X</a>
</div>
</div>
</nav>
</template> 로고 영역
MAINTEMPLATE
버튼 애드온을 사용하여 입력 창을 만듭니다.
http://bootstrapk.com/components/#input-groups-
buttons
MAINTEMPLATE
<div class="container">

<h2>Nobody's Page</h2>

<form>

<div class="input-group">

<input type="text" id="post" class="form-control" placeholder="Tell me something..."/>

<div class="input-group-btn">

<button class="btn btn-primary">

<i class="glyphicon glyphicon-pencil"></i>

Post

</button>

</div>

</div>

</form>

</div>
MAINTEMPLATE
<div class="container">

<h2>Nobody's Page</h2>

<form>

<div class="input-group">

<input type="text" id="post" class="form-control" placeholder="Tell me something..."/>

<div class="input-group-btn">

<button class="btn btn-primary">

<i class="glyphicon glyphicon-pencil"></i>

Post

</button>

</div>

</div>

</form>

</div>
입력 그룹

http://bootstrapk.com/components/#input-groups
MAINTEMPLATE
<div class="container">

<h2>Nobody's Page</h2>

<form>

<div class="input-group">

<input type="text" id="post" class="form-control" placeholder="Tell me something..."/>

<div class="input-group-btn">

<button class="btn btn-primary">

<i class="glyphicon glyphicon-pencil"></i>

Post

</button>

</div>

</div>

</form>

</div>
폼 요소
MAINTEMPLATE
<div class="container">

<h2>Nobody's Page</h2>

<form>

<div class="input-group">

<input type="text" id="post" class="form-control" placeholder="Tell me something..."/>

<div class="input-group-btn">

<button class="btn btn-primary">

<i class="glyphicon glyphicon-pencil"></i>

Post

</button>

</div>

</div>

</form>

</div>
버튼 애드온
http://bootstrapk.com/components/#input-groups-buttons
MAINTEMPLATE
<div class="container">

<h2>Nobody's Page</h2>

<form>

<div class="input-group">

<input type="text" id="post" class="form-control" placeholder="Tell me something..."/>

<div class="input-group-btn">

<button class="btn btn-primary">

<i class="glyphicon glyphicon-pencil"></i>

Post

</button>

</div>

</div>

</form>

</div>
버튼 옵션
http://bootstrapk.com/css/#buttons-options
MAINTEMPLATE
<div class="container">

<h2>Nobody's Page</h2>

<form>

<div class="input-group">

<input type="text" id="post" class="form-control" placeholder="Tell me something..."/>

<div class="input-group-btn">

<button class="btn btn-primary">

<i class="glyphicon glyphicon-pencil"></i>

Post

</button>

</div>

</div>

</form>

</div>
아이콘
http://bootstrapk.com/components/#glyphicons
POSTTEMPLATE
• Main 아래 Post들의 목록을 열거하는 화면구성
• media를 사용하여 UI를 먼저 만든다.
• http://bootstrapk.com/components/#media-default
POSTTEMPLATE
• main template 아래에 

{{> posts}} 를 추가하여
posts라는 템플릿을 붙여주
도록한다.
<template name="main">

<div class="container">
….
{{> posts}}
</div>
</template>
POSTTEMPLATE
• Main 아래 Post들의 목록을 열거하는 화면구성
• media를 사용하여 UI를 먼저 만든다.
• http://bootstrapk.com/components/#media-default
POSTTEMPLATE<template name="posts">

<div class="media">

<div class="media-left">

<a href="#">

<img class="media-object" src="http://lorempixel.com/64/64/cats/" alt="nobody">

</a>

</div>

<div class="media-body">

<h4 class="media-heading">Master</h4>

집사야 내 밥은 어디있냐?

</div>

</div>

<div class="media">

<div class="media-left">

<a href="#">

<img class="media-object" src="http://lorempixel.com/64/64/people/" alt="nobody">

</a>

</div>

<div class="media-body">

<h4 class="media-heading">Slave4U</h4>

배고파서 내가 먹었다.

</div>

</div>

</template>
POSTTEMPLATE<template name="posts">

<div class="media">

<div class="media-left">

<a href="#">

<img class="media-object" src="http://lorempixel.com/64/64/cats/" alt="nobody">

</a>

</div>

<div class="media-body">

<h4 class="media-heading">Master</h4>

집사야 내 밥은 어디있냐?

</div>

</div>

<div class="media">

<div class="media-left">

<a href="#">

<img class="media-object" src="http://lorempixel.com/64/64/people/" alt="nobody">

</a>

</div>

<div class="media-body">

<h4 class="media-heading">Slave4U</h4>

배고파서 내가 먹었다.

</div>

</div>

</template>
반복구간
POSTTEMPLATE
{{#each posts}}
….
{{/each}}
• 반복 구간 처리
이제 코딩을 합시다.
Let’s Do Some Coding!
일단 이사 먼저!
• client 폴더를 만듭니다.
• 지금까지 만든 모든 html파
일들을 client 아래로 이동합
니다.
• 같은 곳에 posts.js를 만들어
줍니다.
POSTTEMPLATE
가짜로 자료를 만듭니다.
posts.js 안에 반복 구간에 들어갈 값들을 JSON 형태
로 만들어봅시다.
POSTTEMPLATE
Template.posts.helpers({

"posts": function() {

return [

{

author: {

name: "Master",

profile_image: "http://lorempixel.com/64/64/cats/"

},

message: "집사야 내 밥은 어딨냐?"

},

{

author: {

name: "Slave4U",

profile_image: "http://lorempixel.com/64/64/people/"

},

message: "배고파서 내가 먹었다."

}

]

}

});
POSTTEMPLATE
• posts.html에 반복 구간을 정하고 값을 받을 helper
들로 교체합니다.
POSTTEMPLATE
<template name="posts">

<div class="media">

<div class="media-left">

<a href="#">

<img class="media-object" src="http://lorempixel.com/64/64/
cats/" alt="nobody">

</a>

</div>

<div class="media-body">

<h4 class="media-heading">Master</h4>

집사야 내 밥은 어디있냐?

</div>

</div>

</template>
POSTTEMPLATE<template name="posts">

{{#each posts}}

<div class="media">

<div class="media-left">

<a href="#">

<img class="media-object" src="http://lorempixel.com/64/64/cats/"
alt="nobody">

</a>

</div>

<div class="media-body">

<h4 class="media-heading">Master</h4>

집사야 내 밥은 어디있냐?

</div>

</div>

{{/each}}

</template>
POSTTEMPLATE<template name="posts">

{{#each posts}}

<div class="media">

<div class="media-left">

<a href="#">

<img class="media-object" src="{{author.profile_image}}"
alt="{{author.name}}">

</a>

</div>

<div class="media-body">

<h4 class=“media-heading”>{{author.name}}</h4>

{{message}}

</div>

</div>

{{/each}}

</template>
중간 결과물
Mobile과 Desktop
동일하게 나옵니까?
CONNECT DB
• lib/collection.js 에 추가

Posts = new Mongo.Collection('posts');
• client/server 양쪽에 적용
• 기존 posts.js 수정

Template.posts.helpers({

"posts": function() {

return Posts.find();

}

});
CONNECT DB
• Browser Console에서 테스트
• Posts.insert({

author: {

name: "Master",

profile_image: "http://lorempixel.com/64/64/cats/"

},

message: "집사야 내 밥은 어딨냐?"

});
• Posts.find().fetch();
• 화면과 결과값을 확인
SERVER
METHOD
보안이 필요한 시기
REMOVE INSECURE
• meteor remove insecure
• insert failed:Access denied
• 사용자가 임의로 데이터 조작을 할 수 없음
METHODS
• server/methods.js - 서버에서만 insert

Meteor.methods({

"addPosts": function(obj) {

Posts.insert({

author: {

name: obj.name,

profile_image: obj.profile_image

},

message: obj.message

});

}

});
44
CLIENT CALL
• Method.call 사용. 콘솔에서 테스트.

Meteor.call("addPosts", {

name: "Slave4U",

profile_image: "http://lorempixel.com/64/64/
people/",

message: "배고파서 내가 다 먹었다."

});
45
EVENT HANDLING
• Template.main.events({

"submit": function(event, template) {

Meteor.call("addPosts", {

name: "Slave4U",

profile_image: "http://lorempixel.com/64/64/people/",

message : template.find('#post').value

}, function(err, result) {

if (err) {

throw(error);

} else {

console.log(result);

template.find('#post').value = "";

}

});

event.preventDefault();

}

});
46
사용자 로그인과
연동 필요
EVENT HANDLING
• Template.main.events({

"submit": function(event, template) {

Meteor.call("addPosts", {

name: "Slave4U",

profile_image: "http://lorempixel.com/64/64/people/",

message : template.find('#post').value

}, function(err) {

if (err) {

throw(error);

} else {

console.log(result);

template.find('#post').value = "";

}

});

event.preventDefault();

}

});
47
템플릿 안에서 post라는
id를 가진 객체를 검색.
그 값을 가져온다.
EVENT HANDLING
• Template.main.events({

"submit": function(event, template) {

Meteor.call("addPosts", {

name: "Slave4U",

profile_image: "http://lorempixel.com/64/64/people/",

message : template.find('#post').value

}, function(err) {

if (err) {

throw(error);

} else {

template.find('#post').value = "";

}

});

event.preventDefault();

}

});
48
method call 후 오류처리
EVENT HANDLING
• Template.main.events({

"submit": function(event, template) {

Meteor.call("addPosts", {

name: "Slave4U",

profile_image: "http://lorempixel.com/64/64/people/",

message : template.find('#post').value

}, function(err) {

if (err) {

throw(error);

} else {

template.find('#post').value = "";

}

});

event.preventDefault();

}

});
49
처리 성공 후 입력창
내용 삭제
EVENT HANDLING
• Template.main.events({

"submit": function(event, template) {

Meteor.call("addPosts", {

name: "Slave4U",

profile_image: "http://lorempixel.com/64/64/people/",

message : template.find('#post').value

}, function(err) {

if (err) {

throw(error);

} else {

template.find('#post').value = "";

}

});

event.preventDefault();

}

});
50
기존 submit 이벤트를 금지
페이지 이동이 안되도록 제한
RESET DATABASE
• 서버 정지
• meteor reset
• 재기동
ADDPOSTS
• server/methods.js - 서버에서만 insert

Meteor.methods({

"addPosts": function(obj) {

Posts.insert({

author: {

name: obj.name,

profile_image: obj.profile_image

},

message: obj.message,

createdAt: new Date()

});

}

});
52
반드시 서버 시간!
SORT BYTIME DESC
• 시간 역순 정렬. Server 시간 기준
• http://docs.meteor.com/#/full/sortspecifiers
• Posts.find({}, {

sort: {

createdAt: -1

}

});
POSTS HELPER
• posts.js

Template.posts.helpers({

"posts": function() {

return Posts.find({}, {

sort: {

createdAt: -1

}

});

}

});
정렬순서

-1 : 내림차순
1 : 오름차순
SESSION
insecure처럼
편리하지만 버려야할 것
계륵(鷄肋)
…하지만 맛있다
SESSION
• Session의 장점

전역으로 사용할 수 있다.

브라우저 콘솔에서 사용이 자유롭다.

서버 재시작 이후에도 값을 유지한다.
• Session의 단점

전역으로 밖에 사용할 수 없다.

Deprecated 예정
SESSION 사용법
• Session의 읽기 

Session.get('pageId');
• Session의 쓰기

Session.set('pageId', 'catLover');
SESSION 적용
• main.js

Template.main.helpers({

'page': function() {

return Session.get('pageId');

}

});
• main.html

<template name="main">

<div class="container">

<h2>{{page}}'s Page</h2>

…
SESSION.SET
• 브라우저 콘솔에서

Session.set('pageId', 'catLover')
• 바로 화면이 갱신되는 것을 관찰
• 어째서 이렇게 될까?

Reactive Programming!

http://docs.meteor.com/#/full/reactivity
PUBLISH/SUBSCRIBE
• 보고싶은 것만 보고 싶어요.
• meteor remove autopublish
AUTOPUBLISH?
• insecure 처럼 기본 설치 Meteor package
• Collection의 모든 내용을 서버로부터 가져온다.
• 하지만 우리는 page별로 따로따로 보고 싶다.
BEFORE
default Autopublish
AFTER
with Publish/Subscribe
(https://
www.discovermeteor.com/
blog/understanding-meteor-
publications-and-
subscriptions/)
REMOVE AUTOPUBLISH
• meteor remove autopublish
• 어? 아무것도 안나와요?????
DON’T PANIC
• 원래대로 돌려놓아 봅시다.
• server/publish.js 추가

Meteor.publish('getPage', function() {

return Posts.find();

});
• 브라우저 콘솔에서 확인해보자

Meteor.subscribe('getPage');
MANUAL SUBSCRIPTION
• main.js에 subscribe 추가

Template.main.onCreated(function() {

this.subscribe('getPage');

});
• 원래대로 돌아왔다!
PUB/SUB BASIC
• Server에서 publish 한 데이터를...

Meteor.publish('publishName', function() {

return YourCollection.find();

});
• client에서 subscribe 에서 가져온다.

Template.yourTemplate.onCreated(function() {

this.subscribe('publishName');

});
• 간단하죠?
PUBLISH WITH PAGEID
• 조건을 주고 필요한 것들만 가져옵니다.

(http://docs.meteor.com/#/full/selectors)
• server/publish.js 수정

Meteor.publish('getPage', function(pageId) {

return Posts.find({pageId: pageId});

});
SUBSCRIBE WITH PAGEID
• client/main.js 수정

Template.main.helpers({

'page': function() {

return Session.get('pageId') || 'popular';

}

});
• client/posts.js 수정

Template.posts.onCreated(function() {

this.subscribe('getPage', Session.get('pageId'));

});
pageId가 없으면

popular를 기본으로
pageId로 가입
CALL WITH PAGEID
• client/main.js 수정

Template.main.events({

"submit": function(event, template) {

Meteor.call("addPosts", {

name: "Slave4U",

profile_image: "http://lorempixel.com/64/64/people/",

pageId: Session.get('pageId'),

message : template.find('#post').value

}, function(err) {

…
METHOD WITH PAGEID
• server/methods.js 수정

Meteor.methods({

"addPosts": function(obj) {

Posts.insert({

author: {

name: obj.name,

profile_image: obj.profile_image

},

pageId: obj.pageId,

message: obj.message,

createdAt: new Date()

});

}

})
ROUTER
어디로 가야하나요?
콘솔에서 Session.set은 그만
KEYWORD별 POSTS
• 같은 관심사를 가진 사람들끼리 이야기 할 수 있도록
POSTS를 분리
• 채널이나 대화방 같은 느낌
• Page라는 이름으로 분리
• URL로 구분

/page/keyword
ROUTING
• Routing용 package 설치
• meteor add kadira:flow-router
WARNING!
• Flow-router는 third-party package입니다.

작성자가 꼭 업데이트를 보증하지 않습니다.
• 어떤 Router를 사용할지는 선택할 수 있습니다.
• Single Page Application에서 Routing(URL 경로)가
꼭 필수이진 않습니다.
ROUTER 만들기
• https://kadira.io/academy/meteor-routing-guide/content/
introduction-to-flow-router
• client/router.js 생성 (원래 이렇게 쓰는 건 아니에요!)

FlowRouter.route('/page/:pageId', {

name: 'main',

action: function(params) {

Session.set('pageId', params.pageId);

}

});
인자를 받아서
Session에 기록한다.
ACCOUNTS
meteor add accounts-
password
사용자를 만들자
ACCOUNTS PACKAGE
• meteor add accounts-password
• http://docs.meteor.com/#/full/accounts_api
• Meteor.user() - 현재 접속중인 사용자
• Meteor.userId() - 접속 중인 사용자 ID
• Meteor.loginWithPassword(user, password, [callback]) 

로그인하기, 성공 시 callback function 실행
• Meteor.logout() - 로그아웃
• Accounts.createUser(option, [callback]) - 사용자 생성
ACCOUNTS PACKAGES
• meteor add accounts-
password



E-mail/password 인증
• meteor add ian:accounts-ui-
bootstrap-3



bootstrap3용 accounts UI
• Template에 {{> loginButtons}}
LOGINBUTTONS
<template name="head">

<nav class="navbar navbar-default navbar-static-top">

<div class="container">

<div class="navbar-header">

<a class="navbar-brand" href="#">Sogon2x</a>

<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"

data-target=".navbar-collapse">

<span class="icon-bar"></span>

<span class="icon-bar"></span>

<span class="icon-bar"></span>

</button>

</div>

<div class="navbar-collapse collapse">

<ul class="nav navbar-nav navbar-right">{{> loginButtons}}</ul>

</div>

</div>

</nav>

</template> https://github.com/ianmartorell/meteor-
accounts-ui-bootstrap-3/#how-to-use
LOGINBUTTONS
<template name="head">

<nav class="navbar navbar-default navbar-static-top">

<div class="container">

<div class="navbar-header">

<a class="navbar-brand" href="#">Sogon2x</a>

<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"

data-target=".navbar-collapse">

<span class="icon-bar"></span>

<span class="icon-bar"></span>

<span class="icon-bar"></span>

</button>

</div>

<div class="navbar-collapse collapse">

<ul class="nav navbar-nav navbar-right">{{> loginButtons}}</ul>

</div>

</div>

</nav>

</template>
모바일에서 접히는 영역
LOGINBUTTONS
<template name="head">

<nav class="navbar navbar-default navbar-static-top">

<div class="container">

<div class="navbar-header">

<a class="navbar-brand" href="#">Sogon2x</a>

<button type="button" class="navbar-toggle collapsed" data-toggle="collapse"

data-target=".navbar-collapse">

<span class="icon-bar"></span>

<span class="icon-bar"></span>

<span class="icon-bar"></span>

</button>

</div>

<div class="navbar-collapse collapse">

<ul class="nav navbar-nav navbar-right">{{> loginButtons}}</ul>

</div>

</div>

</nav>

</template>
loginButtons 삽입 (MAGIC!!)
USERNAME
• 사용자명 추가
• https://github.com/ianmartorell/meteor-accounts-ui-bootstrap-3/#custom-signup-
options
• client/config.js

Accounts.ui.config({

extraSignupFields: [{

fieldName: "username",

fieldLabel: "username",

inputType: 'text'

}]

});
추가 입력 필드
USER IN METHOD
• server/methods.js 에 사용자 정보 적용
• 로그인 여부 검사 위해 check 사용

meteor add check
• username은 Meteor.user().username
• profile_image는 gravatar를 사용하자

meteor add jparker:gravatar
USER IN METHOD
• client/main.js 에 Method.call 에 사용자 정보 제거

Template.main.events({

"submit": function(event, template) {

Meteor.call('addPosts', {

pageId: Session.get('pageId'),

message: template.find("#post").value

}, function(err, result) {

if (err) {

throw(err);

} else {

template.find('#post').value = '';

}

});

event.preventDefault();

}
사용자 정보는 서버에서
추가하고 pageId와
Message만 전송
USER IN METHOD
• server/methods.js 에 사용자 정보 적용

Meteor.methods({

"addPosts": function(obj) {

check(this.userId, String);

Posts.insert({

author: {

_id: this.userId,

name: Meteor.user().username,

profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address)+"?d=retro"

},

pageId: obj.pageId,

message: obj.message,

createdAt: new Date()

});

}

});
USER IN METHOD
• server/methods.js 에 사용자 정보 적용

Meteor.methods({

"addPosts": function(obj) {

check(this.userId, String);

Posts.insert({

author: {

_id: this.userId,

name: Meteor.user().username,

profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address, {d: "retro"})

},

pageId: obj.pageId,

message: obj.message,

createdAt: new Date()

});

}

});
로그인 여부 체크
http://docs.meteor.com/#/full/check
USER IN METHOD
• server/methods.js 에 사용자 정보 적용

Meteor.methods({

"addPosts": function(obj) {

check(this.userId, String);

Posts.insert({

author: {

_id: this.userId,

name: Meteor.user().username,

profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address, {d: "retro"})

},

pageId: obj.pageId,

message: obj.message,

createdAt: new Date()

});

}

});
사용자 ID
USER IN METHOD
• server/methods.js 에 사용자 정보 적용

Meteor.methods({

"addPosts": function(obj) {

check(this.userId, String);

Posts.insert({

author: {

_id: this.userId,

name: Meteor.user().username,

profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address, {d: "retro"})

},

pageId: obj.pageId,

message: obj.message,

createdAt: new Date()

});

}

});
Accounts.ui.config에서 받은
사용자 이름
USER IN METHOD
• server/methods.js 에 사용자 정보 적용

Meteor.methods({

"addPosts": function(obj) {

check(this.userId, String);

Posts.insert({

author: {

_id: this.userId,

name: Meteor.user().username,

profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address, {d: "retro"})

},

pageId: obj.pageId,

message: obj.message,

createdAt: new Date()

});

}

});
E-Mail 주소로 사용자
Image를 가져옴
USER IN METHOD
• server/methods.js 에 사용자 정보 적용

Meteor.methods({

"addPosts": function(obj) {

check(this.userId, String);

Posts.insert({

author: {

_id: this.userId,

name: Meteor.user().username,

profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address, {d: "retro"})

},

pageId: obj.pageId,

message: obj.message,

createdAt: new Date()

});

}

});
(선택사항) 등록된 이미지가 없을 때

retro 아이콘을 임의로 생성

https://en.gravatar.com/site/implement/images/
생성일 추가
• posts.html

<template name="posts">

{{#each posts}}

<div class="media">

<div class="media-left">

<a href="#">

<img class="media-object" src="{{author.profile_image}}" alt="{{author.name}}">

</a>

</div>

<div class="media-body">

<h5 class="media-heading">{{author.name}}

- <i>{{createdAt}}</i>

</h5>

<div>

{{message}}

</div>

</div>

</div>

{{/each}}

</template>
가독성이 떨어진다.
좀 더 친근한 방법으로 표현할 수 없을까?
MOMENT
• 글별 상대시간 표시
• meteor add momentjs:moment
MOMENT
• Moment의Time From을 사용한다.

http://momentjs.com/docs/#/displaying/from/
• Template helper로 적용한다.

http://docs.meteor.com/#/full/template_helpers
생성일 추가
• client/posts.js

Template.posts.helpers({

…

"timeFrom": function(time) {

return moment().from(time);

}

});
• posts.html 수정

…

<h5 class="media-heading">{{author.name}}

- <i>{{timeFrom createdAt}}</i>

</h5>

…
REACTIVE
살아있는 실시간 값
FACEBOOK/TWITTER
• 별다른 행동을 하지 않았는데 가만히 보고 있으면...
• 알아서 시간이 변한다.
• Reactive Programming 을 활용해서 구현해보자.
REACTIVE PROGRAMMING
Don’t imperate, Just delcare
https://en.wikipedia.org/wiki/Reactive_programming
REACTIVETIME
• meteor add random 패키지 추가
• posts.js

Template.posts.onCreated(function() {

…

this.interval = Meteor.setInterval(function() {

Session.set('live', Random.id());

}, 1000);

});
• Session.set('live', ....) 하는 순간

Session.get('live')가 helper 이나 autorun 같은 곳 안쪽에 있으면 전부 재실행한다.

http://docs.meteor.com/#/full/reactivity

1초마다 live라는 키로 고유값을 생성
REACTIVETIME
• posts.js

Template.posts.helpers({

…

"timeFrom": function(time) {

Session.get('live');

return moment().from(time);

}

});
• 이때 live의 값이 변경이 없으면 해당 구문을 실행하지 않는다!
live를 변경하면 

timeFrom helper를 재실행
REACTIVE COMPUTATION
변경이 있을 때만 실행하여 효율적
LOGIN 여부
• Client

Meteor.userId()
• Server

this.userId()
• Template

{{#if currentUser}}
CURRENTUSER 적용
• main.html - 로그인 사용자만 글을 쓸 수 있게

{{#if currentUser}}

<form>

<div class="input-group">

........

</div>

</form>

{{/if}}
FOLLOW/UNFOLLOW
관심사 추적
FOLLOW/UNFOLLOW
• main.html

<h2>{{page}}'s Page

{{#if currentUser}}

{{#if isFollowing}}

<button id="unfollow" class="btn btn-inverse">unfollow</button>

{{else}}

<button id="follow" class="btn btn-primary">follow</button>

{{/if}}

{{/if}}

</h2>
접속여부 확인
Follwing 여부
FOLLOW/UNFOLLOW
• main.js helper 구현

사용자가 해당 토픽에 follow하고 있는지 검사

client에서 기본 접근 가능한 profile 객체를 사용

'isFollowing': function() {

var followings = Meteor.user().profile.followings;

return followings &&
followings[Session.get('pageId')];

}
FOLLOW/UNFOLLOW
• main.js event 구현. follow/unfollow

Template.main.events({

…..

"click #follow": function() {

Meteor.call('follow', Session.get('pageId'));

},

"click #unfollow": function() {

Meteor.call('unfollow', Session.get('pageId'));

}

});
FOLLOW/UNFOLLOW
• server/methods.js - Follow

"follow": function(pageId) {

check(this.userId, String);

var obj={};

obj["profile.followings."+pageId]={

createdAt: new Date()

};

Meteor.users.update(this.userId, {

$set: obj

});

},
• server/methods.js - Unfollow.

"unfollow": function(pageId) {

check(this.userId, String);

var obj={};

obj["profile.followings."+pageId]=
"";

Meteor.users.update(this.userId, {

$unset: obj

});

}
사용자확인
DASHBOARD
• 현재 사용자의 Follow한 Page를 모아 보는 기능
• Feeling Lucky - 무작위 포스트 이동 기능
DASHBOARD
• 홈 디렉토리 이동 시 Dashboard로
• head.html

<a class="navbar-brand" href="/">Sogon2x</a>
• / 일때 pageId를 리셋
• client/router.js

FlowRouter.route('/', {

action: function() {

Session.set('pageId');

}

});
DASHBOARD
• main.html 수정
• 페이지가 있으면 현재
페이지 (/page:pageId)

없으면 Dashboard로
분기
• {{> post}} helper에
pageId 인자 추가
• <template name="main">

<div class="container">

{{#if page}}

<h2>{{page}}'s Page

……

{{> posts
pageId=page}}

{{else}}

{{> dashboard}}

{{/if}}
DASHBOARD
• main.js 수정
• {{> post}} helper에 pageId 인자
전달
• Template.main.helpers({

'page': function() {

return Session.get('pageId');

},
• default 제거
• main.html / main.js 수정
• 페이지가 있으면 현재 페이지 (/page:pageId)

없으면 Dashboard로 분기
• <template name="main">

<div class="container">

{{#if page}}

<div>

<h2>{{page}}'s Page

……

{{> posts pageId=page}}

{{else}}

{{> dashboard}}

{{/if}}
DASHBOARD
• Template helper에서 받은 인자를 js에 적용

Session 에서 this.data.pageId로 변경
• posts.js 수정

Template.posts.onCreated(function() {

var pageId = this.data.pageId;

pageId && this.subscribe('getPage', pageId);

…

Template.posts.helpers({

"posts": function () {

return Posts.find({

pageId: Template.instance().data.pageId

}, {

…
this.data 로부터 상위
템플릿의 인자를 받는다.
Template.instance는
this.data와 같다.
Scope 이유로 다르게 씀.
DASHBOARD
• dashboard 화면 구성
• 필요한 데이터들을 Publish
• Reactive를 이용한 사용자 정보 변경 감지
DASHBOARD
• 운좋은 예감 - 무작위 Posts 추출

전체 데이터 갯수-count()이용-를 기준으로 랜덤만큼 skip하고 limit
을 이용해 1개만 값을 find한다.
• Meteor.publish('feelingLucky', function() {

return Posts.find({}, {

skip: Math.random()*Posts.find().count(),

limit: 1

});

});
DASHBOARD
• dashboard 생성 시 feelingLucky 를 구독(subscribe)
한다.
• client/dashboard.js 생성 후

Template.dashboard.onCreated(function() {

this.subscribe('feelingLucky');

});
DASHBOARD
• helper 정보 - luckyPage / pages
• dashboard.js

Template.dashboard.helpers({

'luckyPage': function() {

var post = Posts.findOne()

return post && post.pageId;

},

'pages': function() {

var result = [];

for (var i in Meteor.user().profile.followings) {

result.push({ pageId: i });

}

return result;

}

});
posts가 없을 때 오류 방지
DASHBOARD
• helper 정보 - luckyPage / pages
• dashboard.js

Template.dashboard.helpers({

'luckyPage': function() {

var post = Posts.findOne()

return post && post.pageId;

},

'pages': function() {

var result = [];

for (var i in Meteor.user().profile.followings) {

result.push({ pageId: i });

}

return result;

}

});
following 정보를 가져온다.
DASHBOARD
• helper 정보 - luckyPage / pages
• dashboard.js

Template.dashboard.helpers({

'luckyPage': function() {

var post = Posts.findOne()

return post && post.pageId;

},

'pages': function() {

var result = [];

for (var i in Meteor.user().profile.followings) {

result.push({ pageId: i });

}

return result;

}

});
pageId로 배열로 밀어넣는다.
DASHBOARD
• 화면 구성 - 사용자 여부에 따라 Feeling lucky와 최근 Posts를 나눠서 보여준다.
• dashboard.html

<template name="dashboard">

<div class="well">

<h2>Welcome to Sogon</h2>

<p>What do you want to talk about?</p>

<a href="/page/{{luckyPage}}" class="btn btn-primary">Feeling lucky</a>

</div>

{{#if currentUser}}

<h2>Recent Posts</h2>

{{#each pages}}

<h3><a href="/page/{{pageId}}">{{pageId}}</a></h3>

{{> posts pageId=pageId}}

{{/each}}

{{/if}}

</template>
운좋은 예감(랜덤링크)
사용자 정보가 “있으면”
following 중인 page들 목록
더 생각해 볼 것들
더 좋은 서비스를 위해
• MongoDB Operator의 사용. (ex: $addToSet, $pull 등)
• OAuth를 사용한 외부 서비스(페이스북/네이버/카카오) 로그인 연동
• 수정/삭제 기능
• 외부 공유와 검색엔진 최적화
• iOS/Android Hybrid Apps 제작
• Deploy …
참고 사이트
• https://github.com/MeteorKorea/meteor2015codelab

본 문서의 소스 코드 github 저장소
• http://meteorjs.rk 

Meteor Korea
• http://www.meetup.com/Meteor-Seoul

Meteor Seoul Meetup 모임
• http://kr.discovermeteor.com/

Discover Meteor 한글
• https://www.facebook.com/groups/meteorschool/

Facebook Meteor School

Meteor2015 codelab

  • 1.
    MODERN WEB APPLICATION WITHMETEOR 이재호 Appsoulute 대표 jhlee@appsoulute.com http://github.com/acidsound http://spectrumdig.blogspot.kr
  • 2.
    INSTALL METEOR Linux/OS Xcurl https://install.meteor.com/ | sh Windows https://install.meteor.com/windows
  • 3.
    첫 METEOR APP •meteor로 시작하는 명령은 터미널이나 커맨드라인(시작 >실행>cmd)에서 입력합니 다. • meteor create sogon2x
  • 4.
  • 5.
    구현 목표 관심사 기반마이크로 블로깅 서비스 1. 화면생성 2. 포스트 입력 3. 이벤트 처리 4. 포스트 정렬 5. 사용자 계정 6. 구독/탈퇴 7. 대쉬보드
  • 6.
  • 7.
    TOOL • 어떤 걸로코드를 만드실 건 가요? • ATOM (무료 추천!) • Sublime text (인기!) • Webstorm (유료 최고!)
  • 8.
    JAVASCRIPT 구조 CLIENT if (Meteor.isClient){ } SERVER if (Meteor.isServer) { Meteor.startup(function() { // code to run on server at startup }); } 사용자 
 브라우저에서 실행합니다. 서버에서 실행합니다.
  • 9.
    HTMLTEMPLATE (mobile first!) index.html <head> <title>sogon2x</title> <meta name="viewport"content="width=device- width, initial-scale=1"> </head> <body> {{> head}} {{> main}} </body> head.html <template name="head"> <h1>fixed header</h1> </template> main.html <template name="main"> <p>context</p> </template> emmet- meta:vp
  • 10.
    HEAD • https://atmospherejs.com/twbs/bootstrap • meteoradd twbs:bootstrap • 적용 후 변화를 관찰 • navbar 사용
 http://bootstrapk.com/components/#navbar-brand- image
  • 11.
    HEAD - NAVBAR <templatename="head"> <nav class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">Sogon2X</a> </div> </div> </nav> </template>
  • 12.
    HEAD - NAVBAR <templatename="main"> <nav class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">Sogon2X</a> </div> </div> </nav> </template> 상단 네비게이션 바
  • 13.
    HEAD - NAVBAR <templatename="main"> <nav class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">Sogon2X</a> </div> </div> </nav> </template> 기본 색상
 navbar-inverse도 시도
  • 14.
    HEAD - NAVBAR <templatename="main"> <nav class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">Sogon2X</a> </div> </div> </nav> </template> 상단 고정 (optional)
  • 15.
    HEAD - NAVBAR <templatename="main"> <nav class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">Sogon2X</a> </div> </div> </nav> </template> 컨테이너 http://bootstrapk.com/css/#overview-container
  • 16.
    HEAD - NAVBAR <templatename="main"> <nav class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">Sogon2X</a> </div> </div> </nav> </template> Header 영역
  • 17.
    HEAD - NAVBAR <templatename="main"> <nav class="navbar navbar-default navbar-static-top"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">Sogon2X</a> </div> </div> </nav> </template> 로고 영역
  • 18.
    MAINTEMPLATE 버튼 애드온을 사용하여입력 창을 만듭니다. http://bootstrapk.com/components/#input-groups- buttons
  • 19.
    MAINTEMPLATE <div class="container">
 <h2>Nobody's Page</h2>
 <form>
 <divclass="input-group">
 <input type="text" id="post" class="form-control" placeholder="Tell me something..."/>
 <div class="input-group-btn">
 <button class="btn btn-primary">
 <i class="glyphicon glyphicon-pencil"></i>
 Post
 </button>
 </div>
 </div>
 </form>
 </div>
  • 20.
    MAINTEMPLATE <div class="container">
 <h2>Nobody's Page</h2>
 <form>
 <divclass="input-group">
 <input type="text" id="post" class="form-control" placeholder="Tell me something..."/>
 <div class="input-group-btn">
 <button class="btn btn-primary">
 <i class="glyphicon glyphicon-pencil"></i>
 Post
 </button>
 </div>
 </div>
 </form>
 </div> 입력 그룹
 http://bootstrapk.com/components/#input-groups
  • 21.
    MAINTEMPLATE <div class="container">
 <h2>Nobody's Page</h2>
 <form>
 <divclass="input-group">
 <input type="text" id="post" class="form-control" placeholder="Tell me something..."/>
 <div class="input-group-btn">
 <button class="btn btn-primary">
 <i class="glyphicon glyphicon-pencil"></i>
 Post
 </button>
 </div>
 </div>
 </form>
 </div> 폼 요소
  • 22.
    MAINTEMPLATE <div class="container">
 <h2>Nobody's Page</h2>
 <form>
 <divclass="input-group">
 <input type="text" id="post" class="form-control" placeholder="Tell me something..."/>
 <div class="input-group-btn">
 <button class="btn btn-primary">
 <i class="glyphicon glyphicon-pencil"></i>
 Post
 </button>
 </div>
 </div>
 </form>
 </div> 버튼 애드온 http://bootstrapk.com/components/#input-groups-buttons
  • 23.
    MAINTEMPLATE <div class="container">
 <h2>Nobody's Page</h2>
 <form>
 <divclass="input-group">
 <input type="text" id="post" class="form-control" placeholder="Tell me something..."/>
 <div class="input-group-btn">
 <button class="btn btn-primary">
 <i class="glyphicon glyphicon-pencil"></i>
 Post
 </button>
 </div>
 </div>
 </form>
 </div> 버튼 옵션 http://bootstrapk.com/css/#buttons-options
  • 24.
    MAINTEMPLATE <div class="container">
 <h2>Nobody's Page</h2>
 <form>
 <divclass="input-group">
 <input type="text" id="post" class="form-control" placeholder="Tell me something..."/>
 <div class="input-group-btn">
 <button class="btn btn-primary">
 <i class="glyphicon glyphicon-pencil"></i>
 Post
 </button>
 </div>
 </div>
 </form>
 </div> 아이콘 http://bootstrapk.com/components/#glyphicons
  • 25.
    POSTTEMPLATE • Main 아래Post들의 목록을 열거하는 화면구성 • media를 사용하여 UI를 먼저 만든다. • http://bootstrapk.com/components/#media-default
  • 26.
    POSTTEMPLATE • main template아래에 
 {{> posts}} 를 추가하여 posts라는 템플릿을 붙여주 도록한다. <template name="main">
 <div class="container"> …. {{> posts}} </div> </template>
  • 27.
    POSTTEMPLATE • Main 아래Post들의 목록을 열거하는 화면구성 • media를 사용하여 UI를 먼저 만든다. • http://bootstrapk.com/components/#media-default
  • 28.
    POSTTEMPLATE<template name="posts">
 <div class="media">
 <divclass="media-left">
 <a href="#">
 <img class="media-object" src="http://lorempixel.com/64/64/cats/" alt="nobody">
 </a>
 </div>
 <div class="media-body">
 <h4 class="media-heading">Master</h4>
 집사야 내 밥은 어디있냐?
 </div>
 </div>
 <div class="media">
 <div class="media-left">
 <a href="#">
 <img class="media-object" src="http://lorempixel.com/64/64/people/" alt="nobody">
 </a>
 </div>
 <div class="media-body">
 <h4 class="media-heading">Slave4U</h4>
 배고파서 내가 먹었다.
 </div>
 </div>
 </template>
  • 29.
    POSTTEMPLATE<template name="posts">
 <div class="media">
 <divclass="media-left">
 <a href="#">
 <img class="media-object" src="http://lorempixel.com/64/64/cats/" alt="nobody">
 </a>
 </div>
 <div class="media-body">
 <h4 class="media-heading">Master</h4>
 집사야 내 밥은 어디있냐?
 </div>
 </div>
 <div class="media">
 <div class="media-left">
 <a href="#">
 <img class="media-object" src="http://lorempixel.com/64/64/people/" alt="nobody">
 </a>
 </div>
 <div class="media-body">
 <h4 class="media-heading">Slave4U</h4>
 배고파서 내가 먹었다.
 </div>
 </div>
 </template> 반복구간
  • 30.
  • 31.
  • 32.
    일단 이사 먼저! •client 폴더를 만듭니다. • 지금까지 만든 모든 html파 일들을 client 아래로 이동합 니다. • 같은 곳에 posts.js를 만들어 줍니다.
  • 33.
    POSTTEMPLATE 가짜로 자료를 만듭니다. posts.js안에 반복 구간에 들어갈 값들을 JSON 형태 로 만들어봅시다.
  • 34.
    POSTTEMPLATE Template.posts.helpers({
 "posts": function() {
 return[
 {
 author: {
 name: "Master",
 profile_image: "http://lorempixel.com/64/64/cats/"
 },
 message: "집사야 내 밥은 어딨냐?"
 },
 {
 author: {
 name: "Slave4U",
 profile_image: "http://lorempixel.com/64/64/people/"
 },
 message: "배고파서 내가 먹었다."
 }
 ]
 }
 });
  • 35.
    POSTTEMPLATE • posts.html에 반복구간을 정하고 값을 받을 helper 들로 교체합니다.
  • 36.
    POSTTEMPLATE <template name="posts">
 <div class="media">
 <divclass="media-left">
 <a href="#">
 <img class="media-object" src="http://lorempixel.com/64/64/ cats/" alt="nobody">
 </a>
 </div>
 <div class="media-body">
 <h4 class="media-heading">Master</h4>
 집사야 내 밥은 어디있냐?
 </div>
 </div>
 </template>
  • 37.
    POSTTEMPLATE<template name="posts">
 {{#each posts}}
 <divclass="media">
 <div class="media-left">
 <a href="#">
 <img class="media-object" src="http://lorempixel.com/64/64/cats/" alt="nobody">
 </a>
 </div>
 <div class="media-body">
 <h4 class="media-heading">Master</h4>
 집사야 내 밥은 어디있냐?
 </div>
 </div>
 {{/each}}
 </template>
  • 38.
    POSTTEMPLATE<template name="posts">
 {{#each posts}}
 <divclass="media">
 <div class="media-left">
 <a href="#">
 <img class="media-object" src="{{author.profile_image}}" alt="{{author.name}}">
 </a>
 </div>
 <div class="media-body">
 <h4 class=“media-heading”>{{author.name}}</h4>
 {{message}}
 </div>
 </div>
 {{/each}}
 </template>
  • 39.
  • 40.
    CONNECT DB • lib/collection.js에 추가
 Posts = new Mongo.Collection('posts'); • client/server 양쪽에 적용 • 기존 posts.js 수정
 Template.posts.helpers({
 "posts": function() {
 return Posts.find();
 }
 });
  • 41.
    CONNECT DB • BrowserConsole에서 테스트 • Posts.insert({
 author: {
 name: "Master",
 profile_image: "http://lorempixel.com/64/64/cats/"
 },
 message: "집사야 내 밥은 어딨냐?"
 }); • Posts.find().fetch(); • 화면과 결과값을 확인
  • 42.
  • 43.
    REMOVE INSECURE • meteorremove insecure • insert failed:Access denied • 사용자가 임의로 데이터 조작을 할 수 없음
  • 44.
    METHODS • server/methods.js -서버에서만 insert
 Meteor.methods({
 "addPosts": function(obj) {
 Posts.insert({
 author: {
 name: obj.name,
 profile_image: obj.profile_image
 },
 message: obj.message
 });
 }
 }); 44
  • 45.
    CLIENT CALL • Method.call사용. 콘솔에서 테스트.
 Meteor.call("addPosts", {
 name: "Slave4U",
 profile_image: "http://lorempixel.com/64/64/ people/",
 message: "배고파서 내가 다 먹었다."
 }); 45
  • 46.
    EVENT HANDLING • Template.main.events({
 "submit":function(event, template) {
 Meteor.call("addPosts", {
 name: "Slave4U",
 profile_image: "http://lorempixel.com/64/64/people/",
 message : template.find('#post').value
 }, function(err, result) {
 if (err) {
 throw(error);
 } else {
 console.log(result);
 template.find('#post').value = "";
 }
 });
 event.preventDefault();
 }
 }); 46 사용자 로그인과 연동 필요
  • 47.
    EVENT HANDLING • Template.main.events({
 "submit":function(event, template) {
 Meteor.call("addPosts", {
 name: "Slave4U",
 profile_image: "http://lorempixel.com/64/64/people/",
 message : template.find('#post').value
 }, function(err) {
 if (err) {
 throw(error);
 } else {
 console.log(result);
 template.find('#post').value = "";
 }
 });
 event.preventDefault();
 }
 }); 47 템플릿 안에서 post라는 id를 가진 객체를 검색. 그 값을 가져온다.
  • 48.
    EVENT HANDLING • Template.main.events({
 "submit":function(event, template) {
 Meteor.call("addPosts", {
 name: "Slave4U",
 profile_image: "http://lorempixel.com/64/64/people/",
 message : template.find('#post').value
 }, function(err) {
 if (err) {
 throw(error);
 } else {
 template.find('#post').value = "";
 }
 });
 event.preventDefault();
 }
 }); 48 method call 후 오류처리
  • 49.
    EVENT HANDLING • Template.main.events({
 "submit":function(event, template) {
 Meteor.call("addPosts", {
 name: "Slave4U",
 profile_image: "http://lorempixel.com/64/64/people/",
 message : template.find('#post').value
 }, function(err) {
 if (err) {
 throw(error);
 } else {
 template.find('#post').value = "";
 }
 });
 event.preventDefault();
 }
 }); 49 처리 성공 후 입력창 내용 삭제
  • 50.
    EVENT HANDLING • Template.main.events({
 "submit":function(event, template) {
 Meteor.call("addPosts", {
 name: "Slave4U",
 profile_image: "http://lorempixel.com/64/64/people/",
 message : template.find('#post').value
 }, function(err) {
 if (err) {
 throw(error);
 } else {
 template.find('#post').value = "";
 }
 });
 event.preventDefault();
 }
 }); 50 기존 submit 이벤트를 금지 페이지 이동이 안되도록 제한
  • 51.
    RESET DATABASE • 서버정지 • meteor reset • 재기동
  • 52.
    ADDPOSTS • server/methods.js -서버에서만 insert
 Meteor.methods({
 "addPosts": function(obj) {
 Posts.insert({
 author: {
 name: obj.name,
 profile_image: obj.profile_image
 },
 message: obj.message,
 createdAt: new Date()
 });
 }
 }); 52 반드시 서버 시간!
  • 53.
    SORT BYTIME DESC •시간 역순 정렬. Server 시간 기준 • http://docs.meteor.com/#/full/sortspecifiers • Posts.find({}, {
 sort: {
 createdAt: -1
 }
 });
  • 54.
    POSTS HELPER • posts.js
 Template.posts.helpers({
 "posts":function() {
 return Posts.find({}, {
 sort: {
 createdAt: -1
 }
 });
 }
 }); 정렬순서
 -1 : 내림차순 1 : 오름차순
  • 55.
  • 56.
    SESSION • Session의 장점
 전역으로사용할 수 있다.
 브라우저 콘솔에서 사용이 자유롭다.
 서버 재시작 이후에도 값을 유지한다. • Session의 단점
 전역으로 밖에 사용할 수 없다.
 Deprecated 예정
  • 57.
    SESSION 사용법 • Session의읽기 
 Session.get('pageId'); • Session의 쓰기
 Session.set('pageId', 'catLover');
  • 58.
    SESSION 적용 • main.js
 Template.main.helpers({
 'page':function() {
 return Session.get('pageId');
 }
 }); • main.html
 <template name="main">
 <div class="container">
 <h2>{{page}}'s Page</h2>
 …
  • 59.
    SESSION.SET • 브라우저 콘솔에서
 Session.set('pageId','catLover') • 바로 화면이 갱신되는 것을 관찰 • 어째서 이렇게 될까?
 Reactive Programming!
 http://docs.meteor.com/#/full/reactivity
  • 60.
    PUBLISH/SUBSCRIBE • 보고싶은 것만보고 싶어요. • meteor remove autopublish
  • 61.
    AUTOPUBLISH? • insecure 처럼기본 설치 Meteor package • Collection의 모든 내용을 서버로부터 가져온다. • 하지만 우리는 page별로 따로따로 보고 싶다.
  • 62.
  • 63.
  • 64.
    REMOVE AUTOPUBLISH • meteorremove autopublish • 어? 아무것도 안나와요?????
  • 65.
    DON’T PANIC • 원래대로돌려놓아 봅시다. • server/publish.js 추가
 Meteor.publish('getPage', function() {
 return Posts.find();
 }); • 브라우저 콘솔에서 확인해보자
 Meteor.subscribe('getPage');
  • 66.
    MANUAL SUBSCRIPTION • main.js에subscribe 추가
 Template.main.onCreated(function() {
 this.subscribe('getPage');
 }); • 원래대로 돌아왔다!
  • 67.
    PUB/SUB BASIC • Server에서publish 한 데이터를...
 Meteor.publish('publishName', function() {
 return YourCollection.find();
 }); • client에서 subscribe 에서 가져온다.
 Template.yourTemplate.onCreated(function() {
 this.subscribe('publishName');
 }); • 간단하죠?
  • 68.
    PUBLISH WITH PAGEID •조건을 주고 필요한 것들만 가져옵니다.
 (http://docs.meteor.com/#/full/selectors) • server/publish.js 수정
 Meteor.publish('getPage', function(pageId) {
 return Posts.find({pageId: pageId});
 });
  • 69.
    SUBSCRIBE WITH PAGEID •client/main.js 수정
 Template.main.helpers({
 'page': function() {
 return Session.get('pageId') || 'popular';
 }
 }); • client/posts.js 수정
 Template.posts.onCreated(function() {
 this.subscribe('getPage', Session.get('pageId'));
 }); pageId가 없으면
 popular를 기본으로 pageId로 가입
  • 70.
    CALL WITH PAGEID •client/main.js 수정
 Template.main.events({
 "submit": function(event, template) {
 Meteor.call("addPosts", {
 name: "Slave4U",
 profile_image: "http://lorempixel.com/64/64/people/",
 pageId: Session.get('pageId'),
 message : template.find('#post').value
 }, function(err) {
 …
  • 71.
    METHOD WITH PAGEID •server/methods.js 수정
 Meteor.methods({
 "addPosts": function(obj) {
 Posts.insert({
 author: {
 name: obj.name,
 profile_image: obj.profile_image
 },
 pageId: obj.pageId,
 message: obj.message,
 createdAt: new Date()
 });
 }
 })
  • 72.
  • 73.
    KEYWORD별 POSTS • 같은관심사를 가진 사람들끼리 이야기 할 수 있도록 POSTS를 분리 • 채널이나 대화방 같은 느낌 • Page라는 이름으로 분리 • URL로 구분
 /page/keyword
  • 74.
    ROUTING • Routing용 package설치 • meteor add kadira:flow-router
  • 75.
    WARNING! • Flow-router는 third-partypackage입니다.
 작성자가 꼭 업데이트를 보증하지 않습니다. • 어떤 Router를 사용할지는 선택할 수 있습니다. • Single Page Application에서 Routing(URL 경로)가 꼭 필수이진 않습니다.
  • 76.
    ROUTER 만들기 • https://kadira.io/academy/meteor-routing-guide/content/ introduction-to-flow-router •client/router.js 생성 (원래 이렇게 쓰는 건 아니에요!)
 FlowRouter.route('/page/:pageId', {
 name: 'main',
 action: function(params) {
 Session.set('pageId', params.pageId);
 }
 }); 인자를 받아서 Session에 기록한다.
  • 77.
  • 78.
    ACCOUNTS PACKAGE • meteoradd accounts-password • http://docs.meteor.com/#/full/accounts_api • Meteor.user() - 현재 접속중인 사용자 • Meteor.userId() - 접속 중인 사용자 ID • Meteor.loginWithPassword(user, password, [callback]) 
 로그인하기, 성공 시 callback function 실행 • Meteor.logout() - 로그아웃 • Accounts.createUser(option, [callback]) - 사용자 생성
  • 79.
    ACCOUNTS PACKAGES • meteoradd accounts- password
 
 E-mail/password 인증 • meteor add ian:accounts-ui- bootstrap-3
 
 bootstrap3용 accounts UI • Template에 {{> loginButtons}}
  • 80.
    LOGINBUTTONS <template name="head">
 <nav class="navbarnavbar-default navbar-static-top">
 <div class="container">
 <div class="navbar-header">
 <a class="navbar-brand" href="#">Sogon2x</a>
 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
 data-target=".navbar-collapse">
 <span class="icon-bar"></span>
 <span class="icon-bar"></span>
 <span class="icon-bar"></span>
 </button>
 </div>
 <div class="navbar-collapse collapse">
 <ul class="nav navbar-nav navbar-right">{{> loginButtons}}</ul>
 </div>
 </div>
 </nav>
 </template> https://github.com/ianmartorell/meteor- accounts-ui-bootstrap-3/#how-to-use
  • 81.
    LOGINBUTTONS <template name="head">
 <nav class="navbarnavbar-default navbar-static-top">
 <div class="container">
 <div class="navbar-header">
 <a class="navbar-brand" href="#">Sogon2x</a>
 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
 data-target=".navbar-collapse">
 <span class="icon-bar"></span>
 <span class="icon-bar"></span>
 <span class="icon-bar"></span>
 </button>
 </div>
 <div class="navbar-collapse collapse">
 <ul class="nav navbar-nav navbar-right">{{> loginButtons}}</ul>
 </div>
 </div>
 </nav>
 </template> 모바일에서 접히는 영역
  • 82.
    LOGINBUTTONS <template name="head">
 <nav class="navbarnavbar-default navbar-static-top">
 <div class="container">
 <div class="navbar-header">
 <a class="navbar-brand" href="#">Sogon2x</a>
 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse"
 data-target=".navbar-collapse">
 <span class="icon-bar"></span>
 <span class="icon-bar"></span>
 <span class="icon-bar"></span>
 </button>
 </div>
 <div class="navbar-collapse collapse">
 <ul class="nav navbar-nav navbar-right">{{> loginButtons}}</ul>
 </div>
 </div>
 </nav>
 </template> loginButtons 삽입 (MAGIC!!)
  • 83.
    USERNAME • 사용자명 추가 •https://github.com/ianmartorell/meteor-accounts-ui-bootstrap-3/#custom-signup- options • client/config.js
 Accounts.ui.config({
 extraSignupFields: [{
 fieldName: "username",
 fieldLabel: "username",
 inputType: 'text'
 }]
 }); 추가 입력 필드
  • 84.
    USER IN METHOD •server/methods.js 에 사용자 정보 적용 • 로그인 여부 검사 위해 check 사용
 meteor add check • username은 Meteor.user().username • profile_image는 gravatar를 사용하자
 meteor add jparker:gravatar
  • 85.
    USER IN METHOD •client/main.js 에 Method.call 에 사용자 정보 제거
 Template.main.events({
 "submit": function(event, template) {
 Meteor.call('addPosts', {
 pageId: Session.get('pageId'),
 message: template.find("#post").value
 }, function(err, result) {
 if (err) {
 throw(err);
 } else {
 template.find('#post').value = '';
 }
 });
 event.preventDefault();
 } 사용자 정보는 서버에서 추가하고 pageId와 Message만 전송
  • 86.
    USER IN METHOD •server/methods.js 에 사용자 정보 적용
 Meteor.methods({
 "addPosts": function(obj) {
 check(this.userId, String);
 Posts.insert({
 author: {
 _id: this.userId,
 name: Meteor.user().username,
 profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address)+"?d=retro"
 },
 pageId: obj.pageId,
 message: obj.message,
 createdAt: new Date()
 });
 }
 });
  • 87.
    USER IN METHOD •server/methods.js 에 사용자 정보 적용
 Meteor.methods({
 "addPosts": function(obj) {
 check(this.userId, String);
 Posts.insert({
 author: {
 _id: this.userId,
 name: Meteor.user().username,
 profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address, {d: "retro"})
 },
 pageId: obj.pageId,
 message: obj.message,
 createdAt: new Date()
 });
 }
 }); 로그인 여부 체크 http://docs.meteor.com/#/full/check
  • 88.
    USER IN METHOD •server/methods.js 에 사용자 정보 적용
 Meteor.methods({
 "addPosts": function(obj) {
 check(this.userId, String);
 Posts.insert({
 author: {
 _id: this.userId,
 name: Meteor.user().username,
 profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address, {d: "retro"})
 },
 pageId: obj.pageId,
 message: obj.message,
 createdAt: new Date()
 });
 }
 }); 사용자 ID
  • 89.
    USER IN METHOD •server/methods.js 에 사용자 정보 적용
 Meteor.methods({
 "addPosts": function(obj) {
 check(this.userId, String);
 Posts.insert({
 author: {
 _id: this.userId,
 name: Meteor.user().username,
 profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address, {d: "retro"})
 },
 pageId: obj.pageId,
 message: obj.message,
 createdAt: new Date()
 });
 }
 }); Accounts.ui.config에서 받은 사용자 이름
  • 90.
    USER IN METHOD •server/methods.js 에 사용자 정보 적용
 Meteor.methods({
 "addPosts": function(obj) {
 check(this.userId, String);
 Posts.insert({
 author: {
 _id: this.userId,
 name: Meteor.user().username,
 profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address, {d: "retro"})
 },
 pageId: obj.pageId,
 message: obj.message,
 createdAt: new Date()
 });
 }
 }); E-Mail 주소로 사용자 Image를 가져옴
  • 91.
    USER IN METHOD •server/methods.js 에 사용자 정보 적용
 Meteor.methods({
 "addPosts": function(obj) {
 check(this.userId, String);
 Posts.insert({
 author: {
 _id: this.userId,
 name: Meteor.user().username,
 profile_image: Gravatar.imageUrl(Meteor.user().emails[0].address, {d: "retro"})
 },
 pageId: obj.pageId,
 message: obj.message,
 createdAt: new Date()
 });
 }
 }); (선택사항) 등록된 이미지가 없을 때
 retro 아이콘을 임의로 생성
 https://en.gravatar.com/site/implement/images/
  • 92.
    생성일 추가 • posts.html
 <templatename="posts">
 {{#each posts}}
 <div class="media">
 <div class="media-left">
 <a href="#">
 <img class="media-object" src="{{author.profile_image}}" alt="{{author.name}}">
 </a>
 </div>
 <div class="media-body">
 <h5 class="media-heading">{{author.name}}
 - <i>{{createdAt}}</i>
 </h5>
 <div>
 {{message}}
 </div>
 </div>
 </div>
 {{/each}}
 </template>
  • 93.
    가독성이 떨어진다. 좀 더친근한 방법으로 표현할 수 없을까?
  • 94.
    MOMENT • 글별 상대시간표시 • meteor add momentjs:moment
  • 95.
    MOMENT • Moment의Time From을사용한다.
 http://momentjs.com/docs/#/displaying/from/ • Template helper로 적용한다.
 http://docs.meteor.com/#/full/template_helpers
  • 96.
    생성일 추가 • client/posts.js
 Template.posts.helpers({
 …
 "timeFrom":function(time) {
 return moment().from(time);
 }
 }); • posts.html 수정
 …
 <h5 class="media-heading">{{author.name}}
 - <i>{{timeFrom createdAt}}</i>
 </h5>
 …
  • 97.
  • 98.
    FACEBOOK/TWITTER • 별다른 행동을하지 않았는데 가만히 보고 있으면... • 알아서 시간이 변한다. • Reactive Programming 을 활용해서 구현해보자.
  • 99.
    REACTIVE PROGRAMMING Don’t imperate,Just delcare https://en.wikipedia.org/wiki/Reactive_programming
  • 100.
    REACTIVETIME • meteor addrandom 패키지 추가 • posts.js
 Template.posts.onCreated(function() {
 …
 this.interval = Meteor.setInterval(function() {
 Session.set('live', Random.id());
 }, 1000);
 }); • Session.set('live', ....) 하는 순간
 Session.get('live')가 helper 이나 autorun 같은 곳 안쪽에 있으면 전부 재실행한다.
 http://docs.meteor.com/#/full/reactivity
 1초마다 live라는 키로 고유값을 생성
  • 101.
    REACTIVETIME • posts.js
 Template.posts.helpers({
 …
 "timeFrom": function(time){
 Session.get('live');
 return moment().from(time);
 }
 }); • 이때 live의 값이 변경이 없으면 해당 구문을 실행하지 않는다! live를 변경하면 
 timeFrom helper를 재실행
  • 102.
    REACTIVE COMPUTATION 변경이 있을때만 실행하여 효율적
  • 103.
    LOGIN 여부 • Client
 Meteor.userId() •Server
 this.userId() • Template
 {{#if currentUser}}
  • 104.
    CURRENTUSER 적용 • main.html- 로그인 사용자만 글을 쓸 수 있게
 {{#if currentUser}}
 <form>
 <div class="input-group">
 ........
 </div>
 </form>
 {{/if}}
  • 105.
  • 106.
    FOLLOW/UNFOLLOW • main.html
 <h2>{{page}}'s Page
 {{#ifcurrentUser}}
 {{#if isFollowing}}
 <button id="unfollow" class="btn btn-inverse">unfollow</button>
 {{else}}
 <button id="follow" class="btn btn-primary">follow</button>
 {{/if}}
 {{/if}}
 </h2> 접속여부 확인 Follwing 여부
  • 107.
    FOLLOW/UNFOLLOW • main.js helper구현
 사용자가 해당 토픽에 follow하고 있는지 검사
 client에서 기본 접근 가능한 profile 객체를 사용
 'isFollowing': function() {
 var followings = Meteor.user().profile.followings;
 return followings && followings[Session.get('pageId')];
 }
  • 108.
    FOLLOW/UNFOLLOW • main.js event구현. follow/unfollow
 Template.main.events({
 …..
 "click #follow": function() {
 Meteor.call('follow', Session.get('pageId'));
 },
 "click #unfollow": function() {
 Meteor.call('unfollow', Session.get('pageId'));
 }
 });
  • 109.
    FOLLOW/UNFOLLOW • server/methods.js -Follow
 "follow": function(pageId) {
 check(this.userId, String);
 var obj={};
 obj["profile.followings."+pageId]={
 createdAt: new Date()
 };
 Meteor.users.update(this.userId, {
 $set: obj
 });
 }, • server/methods.js - Unfollow.
 "unfollow": function(pageId) {
 check(this.userId, String);
 var obj={};
 obj["profile.followings."+pageId]= "";
 Meteor.users.update(this.userId, {
 $unset: obj
 });
 } 사용자확인
  • 110.
    DASHBOARD • 현재 사용자의Follow한 Page를 모아 보는 기능 • Feeling Lucky - 무작위 포스트 이동 기능
  • 111.
    DASHBOARD • 홈 디렉토리이동 시 Dashboard로 • head.html
 <a class="navbar-brand" href="/">Sogon2x</a> • / 일때 pageId를 리셋 • client/router.js
 FlowRouter.route('/', {
 action: function() {
 Session.set('pageId');
 }
 });
  • 112.
    DASHBOARD • main.html 수정 •페이지가 있으면 현재 페이지 (/page:pageId)
 없으면 Dashboard로 분기 • {{> post}} helper에 pageId 인자 추가 • <template name="main">
 <div class="container">
 {{#if page}}
 <h2>{{page}}'s Page
 ……
 {{> posts pageId=page}}
 {{else}}
 {{> dashboard}}
 {{/if}}
  • 113.
    DASHBOARD • main.js 수정 •{{> post}} helper에 pageId 인자 전달 • Template.main.helpers({
 'page': function() {
 return Session.get('pageId');
 }, • default 제거 • main.html / main.js 수정 • 페이지가 있으면 현재 페이지 (/page:pageId)
 없으면 Dashboard로 분기 • <template name="main">
 <div class="container">
 {{#if page}}
 <div>
 <h2>{{page}}'s Page
 ……
 {{> posts pageId=page}}
 {{else}}
 {{> dashboard}}
 {{/if}}
  • 114.
    DASHBOARD • Template helper에서받은 인자를 js에 적용
 Session 에서 this.data.pageId로 변경 • posts.js 수정
 Template.posts.onCreated(function() {
 var pageId = this.data.pageId;
 pageId && this.subscribe('getPage', pageId);
 …
 Template.posts.helpers({
 "posts": function () {
 return Posts.find({
 pageId: Template.instance().data.pageId
 }, {
 … this.data 로부터 상위 템플릿의 인자를 받는다. Template.instance는 this.data와 같다. Scope 이유로 다르게 씀.
  • 115.
    DASHBOARD • dashboard 화면구성 • 필요한 데이터들을 Publish • Reactive를 이용한 사용자 정보 변경 감지
  • 116.
    DASHBOARD • 운좋은 예감- 무작위 Posts 추출
 전체 데이터 갯수-count()이용-를 기준으로 랜덤만큼 skip하고 limit 을 이용해 1개만 값을 find한다. • Meteor.publish('feelingLucky', function() {
 return Posts.find({}, {
 skip: Math.random()*Posts.find().count(),
 limit: 1
 });
 });
  • 117.
    DASHBOARD • dashboard 생성시 feelingLucky 를 구독(subscribe) 한다. • client/dashboard.js 생성 후
 Template.dashboard.onCreated(function() {
 this.subscribe('feelingLucky');
 });
  • 118.
    DASHBOARD • helper 정보- luckyPage / pages • dashboard.js
 Template.dashboard.helpers({
 'luckyPage': function() {
 var post = Posts.findOne()
 return post && post.pageId;
 },
 'pages': function() {
 var result = [];
 for (var i in Meteor.user().profile.followings) {
 result.push({ pageId: i });
 }
 return result;
 }
 }); posts가 없을 때 오류 방지
  • 119.
    DASHBOARD • helper 정보- luckyPage / pages • dashboard.js
 Template.dashboard.helpers({
 'luckyPage': function() {
 var post = Posts.findOne()
 return post && post.pageId;
 },
 'pages': function() {
 var result = [];
 for (var i in Meteor.user().profile.followings) {
 result.push({ pageId: i });
 }
 return result;
 }
 }); following 정보를 가져온다.
  • 120.
    DASHBOARD • helper 정보- luckyPage / pages • dashboard.js
 Template.dashboard.helpers({
 'luckyPage': function() {
 var post = Posts.findOne()
 return post && post.pageId;
 },
 'pages': function() {
 var result = [];
 for (var i in Meteor.user().profile.followings) {
 result.push({ pageId: i });
 }
 return result;
 }
 }); pageId로 배열로 밀어넣는다.
  • 121.
    DASHBOARD • 화면 구성- 사용자 여부에 따라 Feeling lucky와 최근 Posts를 나눠서 보여준다. • dashboard.html
 <template name="dashboard">
 <div class="well">
 <h2>Welcome to Sogon</h2>
 <p>What do you want to talk about?</p>
 <a href="/page/{{luckyPage}}" class="btn btn-primary">Feeling lucky</a>
 </div>
 {{#if currentUser}}
 <h2>Recent Posts</h2>
 {{#each pages}}
 <h3><a href="/page/{{pageId}}">{{pageId}}</a></h3>
 {{> posts pageId=pageId}}
 {{/each}}
 {{/if}}
 </template> 운좋은 예감(랜덤링크) 사용자 정보가 “있으면” following 중인 page들 목록
  • 122.
  • 123.
    더 좋은 서비스를위해 • MongoDB Operator의 사용. (ex: $addToSet, $pull 등) • OAuth를 사용한 외부 서비스(페이스북/네이버/카카오) 로그인 연동 • 수정/삭제 기능 • 외부 공유와 검색엔진 최적화 • iOS/Android Hybrid Apps 제작 • Deploy …
  • 124.
    참고 사이트 • https://github.com/MeteorKorea/meteor2015codelab
 본문서의 소스 코드 github 저장소 • http://meteorjs.rk 
 Meteor Korea • http://www.meetup.com/Meteor-Seoul
 Meteor Seoul Meetup 모임 • http://kr.discovermeteor.com/
 Discover Meteor 한글 • https://www.facebook.com/groups/meteorschool/
 Facebook Meteor School