썸머노트에서 이미지 업로드 할때 흔히 발견되는 nodejs 에러들

썸머노트에서 이미지 업로드하기

썸머노트에서 이미지를 서버에 올리려면 일단 아래와 같이 별도의 onImageUpload 핸들러를 구현해야한다.

var $editor = $('#editor');
var editor = $editor.summernote({
 ... 옵션 생략 ... 
 onImageUpload: function(files) {
    sendFile(files[0]);
  }
});

위에서 사용한 sendFile 함수는 아래와 같이 구현한다. 이때 ajax 옵션에 주의하자. 별거아닌 옵션을 실수로 잘못주면 하루종일 이유도 모르고 헤맬수있다. 특히 contentType은 없어야한다. 파일 업로드라고 종종 contentType을 “multipart/form-data”로 설정하는 경우가 있는데, 본래 XMLHttpRequest 객체는 파일 업로드를 지원하지 않기 때문에 파일 업로드를 위해서는 XHR2와 FormData 객체를 이용해야한다. 참고로 XHR2는 거의 모든 최신 브라우저에서 사용할수있다.

function sendFile(file) {
  var data = new FormData();
  data.append("file", file);

  $.ajax({
    type: "POST",
    url: "/api/upload/image",
    type: 'POST',
    data: data,
    contentType: false,
    processData: false, // Don't process the files
    success: function(data) {
      $('#editor').summernote("insertImage", data.url);
    }
  });
}

간혹 no multipart boundary was found 라는 오류가 발생하면 바로 컨텐츠 타입을 의심해보면 된다.

업로드 서버 구현하기

이제 파일을 업로드를 해줄 서버를 구현해보자. 노드로 구현할때 나는 주로 쓰는 multer라는 모듈을 사용한다. 사용법 또한 무지 간단하다.

var router = express.Router();
var multer = require('multer');
var upload = multer({ 
  dest: 'uploads/'
});
router.post('/upload/image', upload.single('file'), function(req, res){
  console.log(req.file);
});

하지만 간단하다고 생각할때 아래와 같은 에러를 만나게 된다.

위 두 에러는 ajax로 파일을 업로드할때 파일내용을 request body에 써버 보내게 되는데 바로 그 바디의 크기 제한에 걸려나 나는 문제다. 이럴땐 당황하지 말고 아래와 같이 설정한다.

app.use(bodyParser.urlencoded({limit: '5mb', extended: false, parameterLimit: 10000}));

물론 node 앞딴에 Nginx 같은 스태틱 서버를 두는 경우엔 nginx 설정에도 업로드 사이즈를 설정해야한다.
관련글은 검색하면 쉽게 찾을수있다.

http {
#...
    client_max_body_size 100m;
#...
}

nodejs winston 모듈을 이용해 로컬타임 출력하기

노드에서 많이 쓰이는 Logger 모듈 3가지

일단 morgan 같은 경우엔 Express4에서 정식 로거 모듈로 사용되고 있고, winston과 bunyan 모듈은 둘다 나름의 특징이 있어서 뭐가 더 좋다라고 할수는 없다. 그냥 취향에 맞는 녀석을 선택해서 쓰면 될것 같다. 둘을 비교한 내용은 이미 검색하면 많이 나오니까 요 포스트로 대신한다.

winston 모듈을 이용해 자동으로 타임로그 찍기

사실 해당 문서를 잘 읽어보면 아래와 같은 단락이 나오는데 예제가 없다보니 어떻게하라는거지? 라고 생각할수도 있다.

timestamp: Boolean flag indicating if we should prepend output with timestamps (default false).
If function is specified, its return value will be used instead of timestamps.

검색해보니 요렇게 써보라는 예제를 찾았다.

var winston = require('winston');
var logger = new (winston.Logger)({ 
    transports: [ new (winston.transports.Console)({ timestamp: true}) ]
});

오예!! 간단하구만…. 이렇게 생각했는데 출력된 로그를 보니 아래와 같이 기준시간이

2014-10-13T02:00:00.231z - info: 어쩌구 저쩌구....

그래서 좀더 검색을 해봤더니 commom.js 모듈을 직접 수정하라는 글도 찾았다.
http://blog.whitelife.co.kr/154

하지만 결론은 그냥 요렇게 하면된다.

var winston = require('winston');
var moment = require('moment');

var logger = new (winston.Logger)({
    transports: [
      new (winston.transports.Console)({
        timestamp: function(){
          return moment().format("YYYY-MM-DD HH:mm:ss.SSS");
        }
      })
    ]
});

오늘 얻은 교훈

사실 문서에도 함수를 지정할수있게 써 있는데 예제가 없으니 대충보고 없네..하고 넘어가는 습관 -_-;;;; 영어는 꼼꼼히 읽자!

생각대로 되는 nodejs, 또 잊어먹기 전에 메모!

지난달부터 재미삼에 패치팡이란 애니팡 아류작을 하나 맹글어보고 있는데, 부득이(?)하게 랭킹서버까지 만들어야했다. 그리고 지난주였나? 페북과 연동해서 친구들과 경쟁구도를 만들기 위해 랭킹서버를 약간 수정했다. 물론 데이터 모델도 달라졌고, API도 약간의 수정이 있었다. 귀차니즘에 수정을 최소화하다보니 많은 기능을 넣지는 않았음에도 불구하고 삽질의 연속이었다. 그때 정리했어야하는데 일단 잊어먹기 전에 생각나는대로 정리해보자.

1. 청크(Chank)데이터 이어붙이기 일단

페이스북 Graph API를 이용해 친구목록을 가져와야했다. 페이스북의 모든 API는 https로 호출한다. (혹시 http로 호출하는 API가 있을찌도 모르겠으나,.. 일단 문서에는 없는듯..) 뭐 https도 암호화와 패킷 검증을 제외하면 프로토콜은 http와 거의 대동소이하다.  여튼 nodejs를 이용해 API 호출후 응답을 받으려면 다음과 같이 ‘data’ 이벤트를 통해 데이터를 수신받는다.

var https = require('https');
var url = 'https://graph.facebook.com/me/friends?fields=id&access_token='+ token;
https.get(url, function(response) {
    response.on('data', function(chunk) {
        console.log('receive.. chunk data...', chunk);
        var friends = JSON.parse(buffer.join(''));
    });
});

HTTP와 HTTPS 객체에 대한 보다 자세한 설명은 nodejs API 문서 를 참고하자. 위와 같이 코드를 작성하고 실행해보면 간간히 JSON.parse 중에 에러가 난다. 아! 왜 자꾸 에러나는거야? 짱내고 있었는데.. 생각해보니 data 이벤트로 받는 녀석은 chunk 단위다! 그럼 당연히 데이터가 짤릴수도 있는거지! 아하~~ 😀 물론 한번에 담겨서 넘어올때도 있지만ㅋㅋㅋ 여튼 콘솔에 값을 출력해보면,.. [Buffer 0x45…..] 버퍼 어쩌구저쩌구가 찍힌다 그래서 버퍼에 담았다가 이어붙이면 되겠다 싶었다. 그런데!!! 어떻게하지? 어떻게 붙이지? Buffer 객체를 만들어야하나? Stream 객체로 해야하나? 서버개발에 익숙치 않으니 Java에서 쓰던 방법들을 동원해서 별에별 방법을 다 써봣다. 너무 Java틱했나? 결론은 다 잘 안됐다. OTL… 도대체 왜 안되는거야? 라고 씩씩대면서 4시간을 날렸다. 그런데 해결책이 아~ 허무해~~! 너무나 단순했다. 그냥 배열에 담았다가 join만 하면 되는거였어!! 정말 javascirpt 다운 해결책!~ ㅜㅜ 아~ 내 아까운 시간~ 그래도 뭐 4시간동안 API문서보고 이거저거 해봤다는거에 위안을 삼으며 다음과 같이 수정!

var https = require('https');
var url = 'https://graph.facebook.com/me/friends?fields=id&access_token='+ token;
https.get(url, function(response) {
    var buffer = [];
    response.on('data', function(chunk) {
        console.log('receive.. chunk data...', chunk);
        buffer.push(chunk);
    });
    response.on('end', function() {
        var friends = JSON.parse(buffer.join(''));
    });
});

위와 같이 수정하면 데이터 짤림 없이 온전한 데이터를 파싱하는데 성공!!

2. Express 모듈에 미들웨어 장착하기

이번에도 다소간의 삽질이 있었지만, 이번엔 지극히 자바스크립트 다운 생각으로 임했더니 매우 쉽게 해결된 문제다. 먼저 nodejs 로 서버를 만들때 가장 많이 쓰는 모듈이 바로 Express 모듈이다. 이젠 거의 socket.io와 더불이 이바닥 표준이 아닌가 싶다. 여튼 GET 방식이든 POST 방식이든 요청 URL에 한글이 들어갈 경우 반드시 인코딩해서 넘겨야하고 서버에선 디코딩 과정이 필요하다.  아니면 서버에서 무조건 URL을 자동 인코딩해서 처리해야한다. 그런데 난 이 모든 것들을 Express가 알아서 해주는줄만 알았다. 그런데 그게 아니었다. ㅜㅜ 너무 맹신했나? 그래서 데이터를 넘길때 서버에서 인코딩하고 다시 응답을 만들때 디코딩하는 생쑈를 했다. 그런데 말이지.. 자꾸 이상한 생각이 든다,.. 이거 너무 귀찮은데,…-_-;;.. 왜 이걸 Express에서 안해주는거지?… API를 뒤졌다.. 역시!!.. 저 밑에 있네.. 바로

Middleware 라는 항목에.. 빙고~!!

app.use(express.bodyParser());

// is equivalent to:
app.use(express.json());
app.use(express.urlencoded());
app.use(express.multipart());

바디파서 한줄이면 인코딩은 알아서 척척척 해준다. -_-;… 역시 자바스크립트는 생각대로 하면된다. 느무 조아요!! 일단 여기까지 또 생각나면… 이어서..

node를 이용한 초간단 랭킹 서버 만들기

몇일전에 애니팡과 비슷한 팡류 게임을 하나 만들었다. 이름하야 패치팡! ㅋㅋ 패치팡은 자바스크립트만으로 만들었고, 게임을 만드는데 꼬박 3일이 걸렸다. 3일중 이틀은 구조잡고 기본 알고리즘 만드는데 썻고, 나머지 하루는 대충 콜리로  UI만들고 포샵질 하느라 시간을 보냈다. 여튼 만들고 보니 뭔가 허전하다 제대로 쓸려면 내가 얻은 점수를 서버에 저장하고 친구들끼리 경쟁을 붙여야한다. 그래서 또 대충 랭킹 서버를 만들기로 했다.  랭킹 서버는 랭킹 정보를 저장할 DB가 있어야한다. DB는 SQL로 쓰려다가 쿼리 짜기 귀찮아서 그냥 몽고DB로 결정!!

1. 몽고DB 설치하기

일단 서버에 몽고DB 서버를 설치하고 설정해야한다. 설치와 설정은 다음 링크를 참고~!! 참고로 내 서버는 페도라다! 그리고 테스트 개발은 윈도우 환경에서 진행했다!

http://docs.mongodb.org/manual/tutorial/install-mongodb-on-red-hat-centos-or-fedora-linux/ 서버에 DB를 설치했으니 실행하자!

$service mongod start

2. 몽고DB를 읽고 쓰기 위한 node 모듈 설치하기

$npm install mongoose

물론 노드가 미리 설치되어 있어야한다. 뭐 노드는 이미 깔려있으니까.. 패스! 다음으로 RESTfull API로 만들어서 랭킹을 저장할 것이므로.. 간단한 웹 인터페이스 제작을 위해 express 모듈도 일단 설치!!

$npm install express

그리고 요즘엔 이녀석 없으면 코딩할 맛이 안난다. 바로 underscore.js!!! 뭐 안쓸찌도 모르겠찌만.. 일단 설치부터~~  

$npm install underscore

3. 몽고 DB 스키마 만들기

초간단하게 만들 것이므로 스키마도  간단하다. 물론 나중에 소셜을 연동할 것이므로 나중에는 좀더 복잡하겠지만, 지금은 그냥 동작 확인만 하면 되니까 이름과 점수만 기록하자!

var mongoose = require('mongoose');
var db = mongoose.createConnection('mongodb://localhost/test', {
    server : {
        poolSize : 10
    }
});
var RankSchema = new mongoose.Schema({
    point : Number,
    name : String
});
var PatchPangRank = db.model('PatchPangRank', RankSchema);

요렇게 만들어진 PatchPangRank는 RankSchema를 모델로 하는 일종의 Collection 이 된다. 기억해두자! 콜렉션!!  Backbone.js의 콜렉션과 유사하다고 보면 된다! 보다 자세한 몽구스 사용법은

API 문서를 참고하자.

4. RESTful API 서버 만들기

이제 DB는 대충 만들었고, 다음으로 서버를 구축해야한다. 서버는 당근 node를 이용할꺼다. 아까 설치한 express 모듈을 이용해서 간단하게 만들어보자!

var express = require('express');
var app = express();
app.get('*', function(req, res) {

    //일단 들어온 경우
    res.send('Hello!! Get the Fuck OUT!');

});
app.listen(8888);
console.log('starting server~!');

80번 포트가 아닌 8888번 포트인걸 감안하자. 80 포트로 만들면 아주 쉽겠지만,..  내서버의 80포트는 이미 nginx님아 차지하고 있다! 그래서 계획은 이렇다! 어찌됐는 /api/xxxx 형태로 호출되는 API는 프록시를 이용해 8888번 포트로 넘겨버릴 생각이다. 그럼 이제 API를 만들어보자. 대충 이런식으로 호출하면 점수가 기록되게 할 생각이다!

/api/add/score/[이름]/[점수]

그리고, 랭킹을 얻어오는 API는 대충 이렇다!

/api/get/ranks

뭐 대충대충.. 어떻게 만들지를 정했으니..  또 대충 서버에 해당 API를 추가해보자~!!

var express = require('express');
var app = express();

// 이 녀석은 점수를 추가하는 API
app.post('/api/add/score/:name/:point', function(req, res) {  });

// 이 녀석은 랭킹 정보를 가져오는 API
app.get('/api/get/ranks', function(req, res) { });

app.get('*', function(req, res) {
    //일단 들어온 경우
    res.send('Hello!! Get the Fuck OUT!');
});
app.listen(8888);
console.log('starting server~!');

이제 본격적으로 코딩해야할 시간이다~!!

5. 서버 API 만들기 먼저 점수를 넣는 것부터 만들어보자!!

app.post('/api/add/score/:name/:point', function(req, res) {
    var name = req.params.name;
    var point = req.params.point;
    var rank = new PatchPangRank({
        name : name,
        point : point
    });
    rank.save();
    res.send('{result: INSERT OK}');
});

아까 만들어둔 PatchPangRank에다가 점수를 기록하고 save()만 해주면 끝~!! 정말 간단하다~!! 물론 중복데이터는 걸려줘야한다… 그럼 중복데이터도 걸러내볼까? 난 이미 만들었지만.. 설명하기 귀찮타..-_- 힌트를 준다면.. PatchPangRank 객체에서 기존에 같은 이름이 있는지 검색한다. 대충 쓰면 요런식이 되겠다!!

PatchPangRank
    .find({ name : name })
    .exec(function (err, rank){
        if(err){ return handlerError(err); }

        // ...  여기다가 rank가 중복됐는지 확인하면 된다..
});

여기서 주의해야하는건 rank가 콜렉션이다! 그래서 배열로 넘어온다!! 다음으로 랭킹을 가져오는 API도 만들어보자!

app.get('/api/get/ranks', function(req, res) {
    console.log("--------");
    PatchPangRank
    .find()
    .limit(10)
    .sort('-point')
    .select('name point')
    .exec(function(err, rank) {
       if (err)  return handleError(err);

       var data = [];
       _.each(rank, function(v){
           if(!v.name){
               v.remove();
           }

           data.push({
               name : v.name,
               point : v.point
           });
       });

       var body = JSON.stringify(data);
       res.setHeader('Content-Type', 'x-application/json');
       res.setHeader('Content-Length', body.length);
       res.end(body);
   });
});

요건 뭐 너무 쉬워서 설명할 꺼리도 없겠다!!

5. 마지막 프락시 설정하기

아까도 이야기했지만, 내 서버는 엔진엑스가 80포트를 쓰고 있다. 그래서 노드는 뒷딴으로 밀려버린 상태.. 서버 설정에 프락시를 추가해준다.

location /api {
    proxy_pass_header Server;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Scheme $scheme;
    proxy_pass http://127.0.0.1:8888;
 }

짜잔~~ 이젠 정말 끝~!! 이제 http://miconblog.com/api/get/ranks 로 접근하면 기록된 랭키정보가 나와야한다! 물론 실제 랭킹서버가 이렇게 허술하진 않겠지만… 뭐 어찌됐든 점수를 기록하고 랭킹을 메겨주는건 맞으니까.. 보안 코드는 나중에 또 기회가 되면 얘기하는걸로 하고 여기까지 정리끝~!!

노드에서 세션 및 에러 핸들링시 주의할점

몇 시간을 삽질햇다.  -_-;;

분명 API 문서를 보면서 가이드 따라 잘 했는데..

도대체 왜 세션이 안되는거야~!! 라고 버럭질햇는데..

API 문서를 꼼꼼히 보지 않으면,.. 다들 고생할듯싶다.

1. 세션 설명
http://expressjs.com/guide.html#session-support 

2. 에러 핸들링
http://expressjs.com/guide.html#error-handling 

자바스크립트가 스크립팅 언어라는 사실을 다시금 일깨워준다.
무엇을 먼저 실행하느냐에 따라,.. 결과가 달라진다.
명심하자!

1) 제대로 된 설정

/**
 * Express 서버 설정
 */
app.configure(function() {
app.use(express.cookieParser());
app.use(express.session({store: sessionStore, secret: ‘secret’, key: ‘express.sid’}));
app.use(express.errorHandler({dumpExceptions : false, showStack : true}));
app.use(app.router); // 반드시 라우터 설정 이전에 에러와 세션이 설정되어야한다.
app.register(‘.html’, require(‘ejs’));
app.set(‘views’, __dirname + ‘/views’);
app.set(‘view engine’, ‘html’);
});

2) 잘못 된 설정: 뭔가.. register, set, use 처럼 보기 좋으라고 각 설정을 묶어놨는데.. 잘 못된 설정이다.

/**
 * Express 서버 설정
 */
app.configure(function() {
app.register(‘.html’, require(‘ejs’));

app.set(‘views’, __dirname + ‘/views’);
app.set(‘view engine’, ‘html’);

app.use(express.cookieParser());
app.use(express.session({store: sessionStore, secret: ‘secret’, key: ‘express.sid’}));
app.use(app.router);
          app.use(express.errorHandler({dumpExceptions : false, showStack : true}));
});