meteor2015 codelab
TRANSCRIPT
MODERN WEB APPLICATIONWITH METEOR
이재호Appsoulute 대표
[email protected]://github.com/acidsound
http://spectrumdig.blogspot.kr
INSTALL METEORLinux/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
});
}
사용자 브라우저에서실행합니다.
서버에서실행합니다.
HTML TEMPLATE(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> 로고 영역
MAIN TEMPLATE
버튼 애드온을 사용하여 입력 창을 만듭니다.
http://bootstrapk.com/components/#input-groups-buttons
MAIN TEMPLATE <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>
MAIN TEMPLATE <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
MAIN TEMPLATE <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> 폼 요소
MAIN TEMPLATE <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
MAIN TEMPLATE <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
MAIN TEMPLATE <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
POST TEMPLATE
• Main 아래 Post들의 목록을 열거하는 화면구성
• media를 사용하여 UI를 먼저 만든다.
• http://bootstrapk.com/components/#media-default
POST TEMPLATE
• main template 아래에 {{> posts}} 를 추가하여 posts라는 템플릿을 붙여주도록한다.
<template name="main"> <div class="container">
….
{{> posts}}
</div>
</template>
POST TEMPLATE
• Main 아래 Post들의 목록을 열거하는 화면구성
• media를 사용하여 UI를 먼저 만든다.
• http://bootstrapk.com/components/#media-default
POST TEMPLATE<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>
POST TEMPLATE<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>
반복구간
POST TEMPLATE{{#each posts}}
….
{{/each}}
• 반복 구간 처리
이제 코딩을 합시다.Let’s Do Some Coding!
일단 이사 먼저!
• client 폴더를 만듭니다.
• 지금까지 만든 모든 html파일들을 client 아래로 이동합니다.
• 같은 곳에 posts.js를 만들어줍니다.
POST TEMPLATE
가짜로 자료를 만듭니다.
posts.js 안에 반복 구간에 들어갈 값들을 JSON 형태로 만들어봅시다.
POST TEMPLATETemplate.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: "배고파서 내가 먹었다." } ] } });
POST TEMPLATE
• posts.html에 반복 구간을 정하고 값을 받을 helper들로 교체합니다.
POST TEMPLATE<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>
POST TEMPLATE<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>
POST TEMPLATE<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 BY TIME 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 : 오름차순
SESSIONinsecure처럼
편리하지만 버려야할 것
계륵(鷄肋)…하지만 맛있다
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별로 따로따로 보고 싶다.
BEFOREdefault Autopublish
AFTERwith 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에 기록한다.
ACCOUNTSmeteor 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-passwordE-mail/password 인증
• meteor add ian:accounts-ui-bootstrap-3bootstrap3용 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.jsAccounts.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 PROGRAMMINGDon’t imperate, Just delcare
https://en.wikipedia.org/wiki/Reactive_programming
REACTIVE TIME• meteor add random 패키지 추가
• posts.jsTemplate.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라는 키로 고유값을 생성
REACTIVE TIME• posts.js
Template.posts.helpers({ … "timeFrom": function(time) { Session.get('live'); return moment().from(time); } });
• 이때 live의 값이 변경이 없으면 해당 구문을 실행하지 않는다!
live를 변경하면 timeFrom helper를 재실행
REACTIVE COMPUTATION변경이 있을 때만 실행하여 효율적
LOGIN 여부
• ClientMeteor.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.jsFlowRouter.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.jsTemplate.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.jsTemplate.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.jsTemplate.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-SeoulMeteor Seoul Meetup 모임
• http://kr.discovermeteor.com/Discover Meteor 한글
• https://www.facebook.com/groups/meteorschool/Facebook Meteor School