2012년 8월 27일 월요일

Meteor 에서 REST 사용

Meteor 에서 file-upload 라던가 REST API 같은 걸 제공하려면
request 를 받아서 처리할 수 있어야하는데 Meteor 에선 지원하지 않아 좀 답답한 면이 있다.

하지만 그래봤자. Node.js 고 connect 프레임워크를 사용하고 있기 때문에
저번에 언급한 적이 있던 __meteor_bootstrap__ 를 이용해보자.

./.meteor/local/build/server/server.js 를 보면 서버쪽 구동부를 볼 수 있는데
run function 쪽을 보면


  __meteor_bootstrap__ = {require: require, startup_hooks: [], app: app};

이런 코드가 있다.
여기서 app은?

그 위쪽에 

  var app = connect.createServer();

어이쿠 connect 네.
게임 끝나셨다.

서버쪽에 아래와 같이 stack array 에 route 를 추가하면 된다.

Meteor.startup ->
  app=__meteor_bootstrap__.app
  app.stack.unshift
    route: "/api",
    handle: (req,res)->
      res.statusCode = 200
      res.write "OK"
      res.end()

다행이야 다행이군!

2012년 8월 26일 일요일

meteor 0.3.9 에서 사용자 인증을 써보자.

예전에 everyAuth 에 대한 글을 쓴적이 있는데
https://github.com/meteor/meteor/wiki/Getting-Started-with-Auth
meteor wiki에 위와 같은 글이 있더라.
발빠르다 meteor. 투자받더니 탄력이 붙었는지 쭉쭉 잘나가는군.
일단 meteor 설치부터 다시하자.
이전에 Quick start 가이드대로

curl https://install.meteor.com | /bin/sh

이렇게 설치했지만.
이번엔 git 에서 auth Branch를 따서 갈 것이므로 수동 설치하자.

git clone git://github.com/meteor/meteor.git
cd meteor
수동설치래봤자 적절한 곳에 clone 하고
인스톨 하는게 전부인데.

auth는 다른 branch에 있기 때문에 일단 clone 하고 해당 경로에 진입만한 상태에서
git branch -r 로 리모트 브랜치를 확인해보자

  origin/HEAD -> origin/master
  origin/auth
  origin/auth-email
  origin/auth-test-isolation
  origin/auth-twitter
  origin/avital
  origin/avital-remove
  origin/avital-watch
  origin/david-handlebars
  origin/dev-bundle-bump
  origin/devel
  origin/email
  origin/forms
  origin/jade
  origin/master
  origin/release-0.1
  origin/release-0.1-templates
  origin/spark
  origin/spiderable
  origin/test-isolation
  origin/version-bump
  origin/webgl
  origin/wrappedjs
  origin/{ref}squashed-auth

이렇게 주욱 나올텐데 우리가 필요한 건

  origin/auth

요놈이다.
git checkout -t origin/auth
(0.4.2 기준 git checkout -t origin/devel 로 변경)
해서 브랜치를 변경하고

$ git branch
* auth
  master

이렇게 확인해서 auth로 옮겨간 것을 확인하고 install.sh 를 실행해주자.
./install.sh
그 다음 내용을 확인해보면


$ meteor list
absolute-url            Generate absolute URLs pointing to the application
accounts                A user account system
accounts-facebook       Login service for Facebook accounts
accounts-google         Login service for Google accounts
accounts-passwords      Password support for accounts.
accounts-twitter        Login service for Twitter accounts
accounts-ui             Simple templates to add login widgets to an app.
accounts-weibo          Login service for Sina Weibo accounts
amplify                 Cross browser API for Persistant Storage, PubSub and Req
autopublish             Automatically publish all data in the database to every
backbone                A minimalist client-side MVC framework
bootstrap               UX/UI framework from Twitter
code-prettify           Syntax highlighting of code, from Google
coffeescript            Javascript dialect with fewer braces and semicolons
email                   Send email messages
force-ssl               Require this application always use transport layer encr
handlebars              Simple semantic templating language
htmljs                  Easy macros for generating DOM elements in Javascript
http                    Make HTTP calls to remote servers
insecure                Allow all database writes by default
jquery                  Manipulate the DOM using CSS selectors
jquery-history          pushState module from the jQuery project
jquery-layout           Easily create arbitrary multicolumn layouts
jquery-waypoints        Execute a function when the user scrolls past an element
less                    The dynamic stylesheet language.
localstorage-polyfill   Simulates the localStorage API on IE 6,7 using userData
madewith                Made With Meteor badge
sass                    Sassy CSS pre-processor.
showdown                Markdown-to-HTML processor
spiderable              Makes the application crawlable to web spiders.
stylus                  Expressive, dynamic, robust CSS.
underscore              Collection of small helper functions (map, each, bind, .

못보던 package들이 많이 생겼는데 accounts 라는 것들이 보인다. 심지어 weibo 도 있네;;

여기까지 왔으면 이제 사용하면 된다.


meteor create auth_example
cd auth_example
해서 만들고
meteor add accounts-google accounts-facebook accounts-twitter accounts-passwords accounts-ui
google, facebook, twitter, 일반인증까지 전부 넣자.

auth_example.html 을 열어서

<head>
  <title> auth_example </title>
</head>

<body>
  {{> loginButtons}}
  {{> hello}}
</body>

<template name="hello">
  <h1>Hello World!</h1>
  {{greeting}}
  <input type="button" value="Click" />
</template>


loginButtons 라는 template 을 추가해주자.
아마 브라우저에 Sign in 이라는 링크가 보일테고
Sign in with Facebook 같은 걸 누르면

Facebook API key not set. Configure app details with Meteor.accounts.facebook.config() and Meteor.accounts.facebook.setSecret()

이렇게 나올거다.
설정해주자.
https://developers.facebook.com/apps/<APP_ID>/summary?save=1 에서 만들어놓았던

App ID, App Secret, Website with Facebook Login 의 URL

이렇게 세가지를 가져와서
client/server 양쪽에 들어가는 공통 js 에
Meteor.accounts.facebook.config(APP_ID, APP_URL);

App ID, URL 을 각각 넣어주고
server 쪽 js 에
Meteor.accounts.facebook.setSecret(APP_SECRET);

App Secret 을 넣어준다. 당연히 Secret 은 노출되면 안되니까.

이번 업데이트로 필요없어졌다. app정보는 Collection 안에 들어간다. http://spectrumdig.blogspot.kr/2012/09/meteor-auth-branch-922.html
내용에 다시 정리하였다.

나머지 내용은
https://github.com/meteor/meteor/wiki/Getting-Started-with-Auth 를 참조하자.

(0.4.2 관련 최신 내용)
http://spectrumdig.blogspot.kr/2012/10/meteor-auth-branch-rc-release-candidated.html

2012년 8월 22일 수요일

angular.js + node.js + express 로 JSONP 구현을 해보았다.

크로스도메인 문제(다른 사이트의 ajax를 호출할 수 없는 브라우저의 보안정책)를 피하기 위해 JSONP라는 대안이 있는데
angular를 이용해 간단하게 목록을 가져오는 외부 호출을 만들어보자.
http://docs.angularjs.org/api/ng.$http#jsonp
내용을 보고 참조하였다.

index.html
<!DOCTYPE html>
<html ng-app>
<head>
    <title></title>
    <script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.1/angular.min.js"></script>
    <script type="text/javascript" src="todo.js"></script>
</head>
<body>
<div>
    <div ng-controller="listController">
        <ul>
            <li ng-repeat="list in lists">
                {{list.author}}
            </li>
        </ul>
    </div>
</div>
</body>
</html>

ng-app 을 설정하고 listController 에 lists 객체로부터 author 목록을 가져오게 하자.

todo.js

listController = function($scope,$http) {
    angular.extend($scope,{
        query:function() {
            $http.jsonp('http://jsbin.com/uqamef/1?callback=JSON_CALLBACK&a=bbb').success(function(data){
                $scope.lists=data;
            })
        }
    });
    $scope.query();
}

listController 에 데이터 바인딩을 위한 $scope와 ajax 호출을 위한 $http를 각각 인자로 두고
jsonp 호출 후 성공시 $scope.lists 에 결과값을 넣도록 한다.
URL은 해당 JSONP를 구현한 API URL과 "?callback=JSON_CALLBACK" 부분(중요!)
그리고 파라메터 값인 "&key=value..." 형태로 구성하여 $http.jsonp 메서드로 호출한다.

서버쪽은 오히려 간단한데
jsbin.com 같은 걸 이용해서 구현하면
html 쪽은 지우고 javascript 쪽에만 jsonp 형태의 데이터를 기술하여
http://jsbin.com/uqamef/1 처럼 테스트 데이터를 만들어 볼 수 있으며

실제 구현은 단지 res.json으로 결과를 던지면 된다.

... 전략 ...
app.get("/jsonp", function(req,res) {
  console.log(req.query.callback);
  console.dir(req.param("parameter name"));
  var result = [
    {"author":"spectrum"},
    {"author":"singajong"}
  ];
  res.json(result);
});
... 후략 ...

근데 요즘은 JSONP 잘 쓰나? 크록포드옹이 'Bad thing' 이라고도 말하기도 했고
실제로 펑션이 와리가리 하는 형태라서 보안문제도 있는데 말이지.

* 사족: 만일 express 3.0 이전버전을 사용한다면 JSONP형식이 아니라 JSON형식으로 나올 수 있다. 아래와 같이 app.configure 블록에 jsonp callback 을 사용한다고 명시하자


app.configure(function(){
  app.set("jsonp callback", true);
 ...
});


2012년 8월 13일 월요일

javascript에서 forEach의 성능 문제


http://jsperf.com/for-vs-foreach/9 를 보고 forEach가 정말 그렇게 느려?
라고 생각이 들어서 node 커맨드라인을 열고 아래와 같이 처 넣고 실행.
어짜피 IE에서도 안되는거 Array를 prototype 확장하여 forEach 를 만들어서 쓰곤 했다.

Array.prototype.forEach=function(callback) {
  for (var i=0,len=this.length; i<len; i++) {
    callback(this[i], i);
  }
}
그래서 이것도 넣어서 같이 돌려봤다.

--------------------------

var i, values = [], sum = 0;
for (i = 0; i < 10000; i++) {
 values[i] = i;
}

function add(val) {
  sum += val;
}

var itercount =2000;

sum=0;
time = +new Date();
for (var _i=itercount; _i>=0; _i--) {
  values.forEach(add);
}
console.log("forEach legacy:"+ (+new Date()-time));

sum=0;
time = +new Date();
for (var _i=itercount; _i>=0; _i--) {
  for (i = 0; i < values.length; i++) {
    add(values[i]);
  }
}
console.log("simple for:"+ (+new Date()-time));

Array.prototype.forEach2=function(callback) {
  for (var i=0,len=this.length; i<len; i++) {
    callback(this[i], i);
  }
}

sum=0;
time = +new Date();
for (var _i=itercount; _i>=0; _i--) {
  values.forEach2(add);
}
console.log("forEach custom:"+ (+new Date()-time));

---------------
결과는 아래와 같다.

forEach legacy:1041
simple for:295
forEach custom:215

오오 무려 prototype으로 만든 forEach 가 더 빠르다!
forEach, map, reduce, every, filter, join, 다 만들어야하나 싶네.

2012년 8월 12일 일요일

XCode OS X Application : Drag & Drop 후 파일 처리


개인적으로 필요한 유틸이 있어서 appleScript 로 만들까하다가
XCode 4.4 로 OS X Application 을 만들어 본 적이 없어서
Drag & Drop, Pipe, File 처리 같은 걸 해봤다.

xib 에 드래그할 대상인 NSImageView 를 놓고
그 NSImageView 를 Customize 한 NSCImageView 를 만들어서 구현했다.
performDragOperation 이벤트에서 

NSPasteboard  *paste = [sender draggingPasteboard];
로 NSPasteboard 객체에 드래그 한 것들을 가지고
NSFilenamesPboardType 인것들을 타입으로 추출하여 NSData로 받았다.


NSArray *fileArray = [paste propertyListForType:@"NSFilenamesPboardType"];
파일 목록은 propertyListForType으로 string array 를 받을 수 있다.


shell 실행하고 결과 stdout 을 받는 것 처리하는데 위의 링크를 참조했다.

  NSTask *task = [[NSTask allocinit];
  [task setLaunchPath:@"<SHELL COMMAND>"];

NSTask 를 setLaunchPath 메서드를 사용하여 콜할 커맨드를 지정하고

  [task setArguments:[NSArray arrayWithObjects:<@args>, file, nil]];

N개의 argument 를 지정

  NSPipe *pipe=[NSPipe pipe];
  [task setStandardOutput:pipe];


실행 후 stdout 을 출력할 대상을 pipe 로 돌려놓고

  [task launch];

로 실행.

  NSFileHandle *fileHandle = [pipe fileHandleForReading];

파일 핸들은 아까 설정한 pipe 로부터 받았다.

  [fileHandle readDataToEndOfFile];

로 받으면 된다.
근데 기껏 만들고 나니 배포하는 법을 모르겠네;



#import "NSCImageView.h"

@implementation NSCImageView

- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
  if ((NSDragOperationGeneric & [sender draggingSourceOperationMask])==NSDragOperationGeneric) {
    return NSDragOperationGeneric;
  } else {
    return NSDragOperationNone;
  }
}

- (void)draggingExited:(id<NSDraggingInfo>)sender {
}

- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
  NSPasteboard *paste = [sender draggingPasteboard];
  NSString *desiredType = [paste availableTypeFromArray:[NSArray arrayWithObjects:NSFilenamesPboardType, nil]];
  NSData *carriedData = [paste dataForType:desiredType];
  
  if (nil==carriedData) {
    return NO;
  } else {
    if ([desiredType isEqualToString:NSFilenamesPboardType]) {
      NSArray *fileArray = [paste propertyListForType:@"NSFilenamesPboardType"];
      NSTask *task = [[NSTask alloc] init];
      [task setLaunchPath:@"<SHELL COMMAND>"];
      for (NSString *file in fileArray) {
        [task setArguments:[NSArray arrayWithObjects:<@args>, file, nil]];
        NSPipe *pipe=[NSPipe pipe];
        [task setStandardOutput:pipe];
        NSFileHandle *fileHandle = [pipe fileHandleForReading];
        [task launch];
        NSData *data;
        data = [fileHandle readDataToEndOfFile];
        NSString *string;
        string=[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"filename : %@", file);
        [fileHandle closeFile];
        
        [[NSFileManager defaultManager] createFileAtPath:file contents:[string dataUsingEncoding:NSUTF8StringEncoding] attributes:nil];
      }
    } else {
      NSLog(@"Nothing happened");
    }
  }
  [self setNeedsDisplay:YES];
  return YES;
}
@end

2012년 8월 8일 수요일

express 2.0 에서 3.0으로 바꿀때 주의할 점.

하드 파티션이 날아가서 새로 셋업하고
작업하던 소스를 받아와서 아무생각 없이 npm install 했더니
express 3.0으로 되어있더라.

기존의 소스는

express = require 'express'
gzippo = require 'gzippo'
fs = require 'fs'
http = require 'http'
app = express.createServer()
path = require 'path'

tmpl =
  compile: (source, options)->
    (locals)->
      # 여기에 템플릿 뽁찡
      source
 
port = process.env.VMC_APP_PORT || 3000
app.configure ->
  app.set 'views', "#{__dirname}/../public"
  app.use app.router
  app.set 'view options', layout: false
  app.set 'view engine', 'html'
  app.register '.html', tmpl
  app.use gzippo.staticGzip "#{__dirname}/../public"

app.get '/', (req,res) ->
  res.render "./html/index.html"

app.listen port, ->
  console.log "Listening on #{port}"

이런 식으로 썼다면



express = require 'express'
fs = require 'fs'
http = require 'http'
app = express()

tmpl = (path, options, fn) ->
  fs.readFile path, 'utf8', (err,str) ->
    # 오류처리하고 템플릿 뽁찡
    if err then fn err else fn null, str.toString()
 
app.configure ->
  app.set 'port', process.env.VMC_APP_PORT || 3000
  app.set 'views', "#{__dirname}/../public"
  app.use app.router
  app.set 'view options', layout: false
  app.set 'view engine', 'html'
  app.engine '.html', tmpl
  app.use require('connect').compress()
  app.use express.static("#{__dirname}/../public")

app.get '/', (req,res) ->
  res.render "./html/index.html"

http.createServer(app).listen app.get('port'), ->
  console.log "Listening on #{app.get('port')}"



이런 패턴으로 바뀌었다.
정리해보면

  1. express.createServer() 가 deprecated 되었다. 그냥 express() 사용
  2. 커스텀 템플릿 엔진을 사용하기 위해 register 대신 engine 을 사용
  3. 압축 전송을 위해 gzippo 를 썼는데 connect 버전업과 함께 그냥 connect.compress() 사용
  4. 템플릿 인자가 바뀜. 전엔 해당 파일의 문자값이 넘어왔는데 지금은 path를 반환.
  5. app.get 으로 key, value 형태로 값을 읽어올 수 있음.
자세한 내용은 아래 링크를 참조

facebook, Google+, Diaspora* 3종 SNS AJAX 메시지 비교

일 관계로 SNS 비슷한 걸 만들고 있는데
데이터 모델을 하기 귀찮은 관계로 남이 한걸 베낄려고 facebook, google+, diaspora 의 XHR을 분석해보고 있다.

1. facebook

역시 예상했던 대로 지저분. entries 라는 객체 밑에 app, group, page, friendlist 등이 마구 혼재
본문 내용은 초기로드시에 
<code class="hidden_elem" id="u33ari_19"><!-- <li>.... --> 이런 형태로 뿌리고 MoreStoriesPagelet 을 통해 Document 요청.
app, group, page, friendlist 같은 건 본문과는 다르게 처리했는데 뭐 닭짓 아닌가? 하긴 원래 글 한번 쓰면 못바꾸는 구조로 처음에 만들어서 별도로 처리한득.

주기적으로 pull 이라는 heartbeat(클라이언트에서 서버찌르기)을 통해 json을 받아옴.

for (;;); {"t":"heartbeat"}
{"t":"heartbeat"}
{"t":"continue","seq":2}

결과값은 이런 형태. 전세계 클라이언트로부터 DDOS 공격을 잘도 버티고 있네 ㅎㅎ

2. Google+

얘들은 성능을 위해 json 데이터 구조를 사용하지 않는다.
그럼 어떻게 하냐고?
)]}'
[[["f.ri","12015082395849449"]
,["ghr",[]
]
,["di",136,,,,,[]
,[]
,,,[]
,[]
,[]
]
,["e",4,,,125]
]]
이런식의 중첩 배열을 사용한다.
마치 C Struct로 만든 전문통신을 보는 느낌인데 데이터를 본다고 알수 있는 구조가 아니다.
물론 주고 받는 쪽에서야 몇번째는 무엇이라고 약속을 해놓아서 알고 있겠지만서도.
오버헤드가 작으니 빠르겠지.
가끔 bind 라는 요청을 보내긴 하는데 talkgadget 즉 Google talk 용이라서 실제 갱신은 facebook 처럼 heartbeat을 쏘는 구조는 아닌 듯. 아니면 일타쌍피로 같이 해결하는 건가?

3. Diaspora*

가장 알기 쉬운 구조. 실은 다 이걸 쓰기 위해 던진 떡밥임.
간단하다. stream이란 이름의 request에 인자를 사용자 id를 줘서 요청했고
https://joindiaspora.com/stream?_=<숫자형태사용자ID>

response 를 보면 정직하게 Array + JSON 형태의 게시글을 다루고 있다.
그냥 데이터만 보면 딱 답이 나옴. 아예 복붙해본다.

[{
    "id": 1850060,
    "guid": "1660cffb68109543",
    "text": "#Sculpture #Scary #Skeleton #Spider <br><br> <br> [ ![Image](http://25.media.tumblr.com/tumblr_m82nycuJDe1qehyxro1_500.jpg) ](http://ur1.ca/9wq7m) <br>Shen Shaomin - Unknown Creature No.12 <br>",
    "public": true,
    "created_at": "2012-08-07T14:00:43Z",
    "interacted_at": "2012-08-07T14:21:57Z",
    "provider_display_name": null,
    "post_type": "Reshare",
    "image_url": null,
    "object_url": null,
    "favorite": false,
    "nsfw": false,
    "author": {
        "id": 25477,
        "guid": "4d1168bc2c1743390f000930",
        "name": "\u4e97 Dr. Emporio Efikz \u4e97",
        "diaspora_id": "emporioefikz@joindiaspora.com",
        "avatar": {
            "small": "https://joindiaspora.s3.amazonaws.com/uploads/images/thumb_small_20a5e6ffea75e8195608.gif",
            "medium": "https://joindiaspora.s3.amazonaws.com/uploads/images/thumb_medium_20a5e6ffea75e8195608.gif",
            "large": "https://joindiaspora.s3.amazonaws.com/uploads/images/thumb_large_20a5e6ffea75e8195608.gif"
        }
    },
    "o_embed_cache": null,
    "mentioned_people": [],
    "photos": [],
    "frame_name": "status",
    "root": {
        "id": 1850022,
        "guid": "724c602e8ca31f97",
        "text": "#Sculpture #Scary #Skeleton #Spider <br><br> <br> [ ![Image](http://25.media.tumblr.com/tumblr_m82nycuJDe1qehyxro1_500.jpg) ](http://ur1.ca/9wq7m) <br>Shen Shaomin - Unknown Creature No.12 <br>",
        "public": true,
        "created_at": "2012-08-07T13:37:31Z",
        "interacted_at": "2012-08-07T14:18:11Z",
        "provider_display_name": null,
        "post_type": "StatusMessage",
        "image_url": null,
        "object_url": null,
        "favorite": false,
        "nsfw": false,
        "author": {
            "id": 25741,
            "guid": "4d11bd252c174338f2002a4c",
            "name": "\u24b6\u24c5\u24c4\u24c1\u24c4\u24c3\u24be\u24c8 \u2301 \u24b6\u24c5\u24bd\u24c7\u24c4\u24b9\u24be\u24c8\u24be\u24b6",
            "diaspora_id": "apolonisaphrodisia@joindiaspora.com",
            "avatar": {
                "small": "https://joindiaspora.s3.amazonaws.com/uploads/images/thumb_small_8105711c49253702d494.jpg",
                "medium": "https://joindiaspora.s3.amazonaws.com/uploads/images/thumb_medium_8105711c49253702d494.jpg",
                "large": "https://joindiaspora.s3.amazonaws.com/uploads/images/thumb_large_8105711c49253702d494.jpg"
            }
        },
        "o_embed_cache": null,
        "mentioned_people": [],
        "photos": [],
        "frame_name": "status",
        "root": null,
        "title": "#Sculpture #Scary #Skeleton #Spider <br><br> <br> [ ![Image](http://25.media.tumblr.com/tumblr_m82nycuJDe1qehyxro1_500.jpg) ](http://ur1.ca/9wq7m) <br>Shen Shaomin - Unknown Creature No.12 <br>",
        "next_post": "/posts/1850022/next",
        "previous_post": "/posts/1850022/previous",
        "interactions": {
            "likes": [],
            "reshares": [],
            "comments_count": 1,
            "likes_count": 4,
            "reshares_count": 2
        }
    },
    "title": "\u4e97 Dr. Emporio Efikz \u4e97\ub2d8\uc758 \uac8c\uc2dc\ubb3c",
    "next_post": "/posts/1850060/next",
    "previous_post": "/posts/1850060/previous",
    "interactions": {
        "likes": [],
        "reshares": [],
        "comments_count": 0,
        "likes_count": 3,
        "reshares_count": 1,
        "comments": []
    }
},
....

길지만 알기쉬운 구조. 공부가 되네.
좀 독특한 건 ping 이라는 request 를 보내는데 response 가 없다.
facebook 처럼 document도 google+처럼 다중배열도 아니다.
게다가 심지어 같은 도메인도 아니다;;
내용을 보니 https://ping.chartbeat.net 쪽으로 찌르는데 http://chartbeat.com/ 이라는 서비스가 있나보다.
실시간 트래픽 분석/통계 관리를 해주는 곳인데 상당히 깔끔. 오호 google analytics 의 실시간 버전 느낌.

자 3줄 요약
1. facebook 은 게시글을 document 로 요청. 나머지는 죄다 JSON 형태의 ajax 요청
2. Google+ 는 게시글을 ajax로 요청. 단 다중배열 [[,,],,,] 형태로 보내줌
3. Diaspora* 는 ajax 에 JSON 형태로 요청. 셋중 가장 읽을 수 있는 형태의 자료 구조를 가지고 있음.

2012년 8월 2일 목요일

node.js(+express) everyAuth 적용기

node.js 용 package 중 everyAuth 라고 상당히 많은 서비스 API를 지원하는 훌륭한 라이브러리가 있다.
facebook, google, twitter 는 물론이고 github 에서 instagram 까지도 지원한다.
맨땅에 하기엔 사실 사용자 로그인/인증이라는 과정이 간단하지 않다.
설명을 쭉 읽어보니 그냥 커스텀 인증을 한다고 해도 구조가 잘 잡혀있어서 써야겠다고 결심.
express 프로젝트를 만들었다.

일단 facebook 부터 시작해보기로 했다.
먼저 OAuth 인증을 하려면 facebook 으로부터 appId 와 appSecret 두개 한쌍을 페어로 받아와야한다.

https://developers.facebook.com/apps 에서 "새 앱 만들기"를 한다음
App Name을 정해주자. Namespace랑 Web Hosting은 사용하지 않으므로 그대로 내버려둠.

그러고 난 뒤 보안 텍스트 확인하면
앱 > 앱이름 > Basic 으로 이동한다.

보면 앱이름과 App ID, App Secret, 아이콘을 확인할 수 있다.
아이콘은 적당히 수정하면 되고

"기본정보"와 "어떻게 앱을 Facebook과 통합시킬지 선택하세요"
두가지 설정 항목이 있음을 확인하고 "어떻게 앱을 Facebook과 통합시킬지 선택하세요" 에서 Website with Facebook Login 를 체크하면 사이트 URL 을 넣을 수 있다.
리디렉트 할 사이트 URL 을 넣고 변경 내용 저장.

일단 이걸로 facebook 준비는 끝났다.
별도의 facebook 앱 페이지를 만들거나 할 필요는 없다.

server.js 에 대략 구현하기를 (coffeescript)


everyauth = require 'everyauth'


# mock User list
user = {}

# set up for everyAuth
everyauth.facebook
  .appId(config.facebook.appId)
  .appSecret(config.facebook.appSecret)
  .handleAuthCallbackError (req, res) ->
    console.log "callback error"
    console.dir req
  .findOrCreateUser (session, accessToken, accessTokExtra, fbUserMetadata) ->
    console.log "find or Create User"
    # facebook 인증 직후 이쪽으로 온다. 사용자 등록을 할 타이밍
    user[fbUserMetadata.id] || user[fbUserMetadata.id]=fbUserMetadata
  .redirectPath('/');



이런 식으로 일단 했다. 별 내용은 없고 config.facebook.appId 과 config.facebook.appSecret 대신 facebook 에서 보았던 App ID, App Secret 내용을 복사해서 수정한다.

configure 쪽은 내 경우 아래와 같이 했다.
session 과 bodyParser, middleWare, methodOverride, router, 등등 use 를 해주고
마지막에 everyauth.helpExpress 를 추가한다. 순서가 잘못되면 오류가 날 수 있으니 주의하자.
역시 coffeescript 로


app.configure ()->
  ....
  app.use express.cookieParser()
  # enable session
  app.use express.session secret: config.sessionSecret, store: store
  app.use express.bodyParser() # for post
  app.use everyauth.middleware()
  app.use express.methodOverride()
  app.use app.router

  ....
  everyauth.helpExpress app


여기까지 한 다음
<domain_name 혹은 localhost>/auth/facebook 으로 가보자
everyauth 가 middleware 로 들어가면서 /auth/facebook 이라는 router 를 만드는 듯.
이 부분에 대한 언급이 없어서 도대체 어떻게 해야 페이스북으로 연결하는 건지 한참 찾았다.

app.get '/api/testAuth', (request, response) ->

    console.dir request.session
    response.writeHead 200, "Content-Type": "application/json"
    response.write JSON.stringify if checkAuth(request)
      "userId" : "#{JSON.stringify request.session.auth.facebook}"
      "result" : "authorized login"

session 에 제대로 들어왔는지 확인해보자.
facebook.user 에 JSON으로 id, name, first_name, last_name, link, username, hometown, location, work, description 등등이 들어와있는 걸 확인할 수 있다.

logout 도 간단하다.
그냥 단순히 <domain_name 혹은 localhost>/logout 링크를 달아주면 끝이다.
만일 /logout 이라는 경로가 싫으면
everyauth.everymodule.logoutPath('/bye');
이런식으로 재설정 가능하다.

logout 후 리디렉션이 필요하다면
everyauth.everymodule.logoutRedirectPath('/navigate/to/after/logout');
경로를 임의로 설정하여 특정 위치로 이동하게 할 수 있다.

참조 URL