ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 20장 Node.js express 를 이용한 CRUD 모듈
    MeanStack (deprecated) 2016. 7. 13. 21:47



    이제 Meanstack 으로 CRUD 모듈 (CREATE , READ , UPDATE , DELETE) 을 만들어 보기로 한다. 


    각 CRUD 모듈은 익스프레스와 angular.js 기능을 지원하는 두 MVC 구조를 포함한다. 익스프레스 쪽은 몽구스 


    모델, 익스프레스 컨트롤러, 익스프레스 라우트 파일로 구축된다. Angular.js 쪽은 조금 더 복잡하며, 뷰 집합, 


    angular.js 컨트롤러 , 서비스, 라우팅 구성을 포함한다.  


    이제는 이 들을 결합하여 Article CRUD 모듈을 구축 할 것 이다.  (한마디로 글을 입력, 수정, 삭제 하는 기능)



    먼저 익스프레스 부분부터 시작하자.  글을 저장하고 검증하기 위해 사용 될 몽구스 모델을 생성한다. 


    다음으로 모듈의 비즈니스 논리를 다루는 익스프레스 컨트롤러로 이동한다. 마지막으로 RESTful API를 컨트롤러 


    메소드에 제공하기 위해 익스프레스 라우트를 배선한다. 



    몽구스 모델에는 Article 엔티티를 표현 할 네가지 간단한 속성을 포함한다. 


    app/models 폴더로 가서 article.server.model.js 파일을 만들고 코드를 적어보자. 


    var mongoose = require('mongoose'),
        Schema = mongoose.Schema;
    
    var ArticleSchema = new Schema({
        created : {
            type : Date,
            default : Date.now
        },
        title : {
            type : String,
            default : '',
            trim : true ,
            required : 'Title cannot be blank'
        },
        content : {
            type : String,
            default : '',
            trim : true
        },
        creator : {
            type : Schema.ObjectId,
            ref : 'User'
        }
    });
    
    mongoose.model('Article', ArticleSchema);
    


    지난 User 모델을 만들때와 같다.  


    이번 ArticleSchema 모델은 4개의 필드를 가지고 있다. 




    created : 해당 글이 작성된 시간을 저장한다.  


    title : 해당 글의 제목을 저장한다. 


    content : 해당 글의 내용을 저장한다.


    creator : 해당 글의 작성자정보를 저장하는데 여기서 Schema.ObjectId , 즉 mongoDB의 유니크한 값인 ObjectId 


       를 저장 하고 User 모델을 참조하도록 하였다. 



    해당 필드를 가진 몽구스 모델을 구성하였고 , 마지막으로 Article 익스프레스 컨트롤러에서 사용하기 위해 몽구스 


    모델을 등록하였다.  이제 이 모델을 사용하기 위해 애플리케이션에서 이를 읽어 들일 필요 가 있다. 


    config 폴더로 가서 mongoose.js 파일을 열어서 수정하자. 


    var config = require('./config'),
         mongoose = require('mongoose');
    
    module.exports = function() {
       var db = mongoose.connect(config.db);
    
       require('../app/models/user.server.model.js');
       require('../app/models/article.server.model.js'); // 추가
       return db;
    }
    


    user 모델을 등록 했을 때 처럼 article 모델역시 등록하였다. 


    이제 이 article 모델을 컨트롤 할 컨트롤러를 생성해야 한다. 먼저 app/controllers 폴더로 가자.


    거기서 articles.server.controller.js 파일을 만들고 코드를 추가하자. 


    var mongoose = require('mongoose'),
        Article = mongoose.model('Article');
    
    var getErrorMessage = function(err) {
        if(err.errors) {
            for (var errName in err.errors) {
                if(err.errors[errName].message) {
                    return err.errors[errName].message;
                }
            }
        } else{
            return 'Unknown server error';
        }
    };
    
    



    Article 몽구스 모델을 포함하였고, 이어서 밑에 getErrorMessage 라는 오류처리 메소드를 추가 하였다. 


    이는 단순히 몽구스 오류 객체에서 errors 컬렉션을 순회하며 첫 메시지를 추출한다. 한번에 여러 오류 메시지를


    사용자에게 제공해 혼란을 주지 않도록 이렇게 구현했다. 



    이제 본격적으로 CRUD controller 를 만들어 보자.  가장먼저 create 메소드를 만들어보자. 


    app/controllers 폴더의 articles.server.controller.js 파일을 열어 다음 코드를 추가해보자. 


    exports.create = function(req,res) {
        var article = new Article(req.body);
        article.creator = req.user;
    
        article.save(function(err) {
            if(err) {
                return res.status(400).send({
                    message : getErrorMessage(err)
                });
            }else{
                res.json(article);
            }
        });
    };
    
    


    코드를 살펴보면 먼저 HTTP 요청 내용을 사용해 Article 모델 인스턴스를 생성했다. 다음으로 글의 creator 로 


    인증된 패스포트 사용자를 추가 했다. 


    # (이전 장에서 이미 사용자 인증을 구현하였으니 이는 인증된 사용자이다.)

    # (아까 몽구스 모델에서 creator 에 ref 로 user 를 참조하는 것을 봤을 것이다.  user 객체를 주입하였다.)


    마지막으로 몽구스 인스턴스의 save 메소드를 사용해 다큐먼트를 저장했다. save 메소드는 오류가 났을 시 


    HTTP 오류코드와 error 메시지를 반환하고, 성공한다면 JSON 응답으로 article 객체를 반환한다. 



    create 메소드를 만들었으니 이번엔 글의 목록을 보는 list() 메소드를 만들어 보자. 


    이 메소드는 find() 메소드를 사용해 articles 컬렉션에 속한 모든 다큐먼트를 인출하고 JSON 으로 표현한 목록


    결과를 출력 할 것이다. 


    다시 app/controllers 폴더로 가서 articles.server.controller.js 파일에 코드를 추가하자. 


    exports.list = function(req,res) {
        Article.find().sort('-created').populate('creator', 'username')
        .exec(function(err,articles) {
            if(err) {
                return res.status(400).send({
                    message : getErrorMessage(err)
                });
            }else{
                res.json(articles);
            }
        });
    };
    


    코드를 살펴봄면 find() 메소드를 이용해서 article 다큐먼트의 컬렉션을 얻어온다. 


    그리고 sort() 메소드로 created 필드를 사용해 순서를 정렬하고 , populate() 메소드를 사용해 articles 객체의 


    creator 속성에 username 이라는 사용자 필드를 추가했다.  


    (creator 필드는 user 모델을 참조하도록 되어있는 걸 기억하자. 결국 user 모델의 username 속성을 가져온다.)


    CRUD 연산의 나머지는 이미 존재하는 단일 article 다큐먼트 조작과 관련이 있다. 당연히 개별 메소드마다 


    기본적으로 이 논리를 반복하는 방법을 사용해 직접 article 다큐먼트를 인출하는 내용을 구현 할 수도 있다.


    하지만 익스프레스 라우터에는 아우트 매개변수를 처리하기 위한 산뜻한(?) 기능이 제공됨으로, 나머지 CRUD 


    기능을 구현하기 앞서, 먼저 라우트 매개변수 미들웨어를 지렛대로 삼아 시간을 절역하고 코드 중첩을 피하는


    방법을 알아보자. 



    read() 메소드는 데이터베이스에 이미 존재하는 article 다큐먼트를 읽기 위한 기본 연산을 제공한다. 일종의 


    RESTful API를 작성하고 있으므로, 이 메소드의 일반적인 용법은 라우트 매개변수로 글의 ID 필드를 전달하는 


    방법을 따른다. 즉 , 서버에 대한 요청은 경로에 articleId 매개변수를 포함해야 한다. 



    익스프레스에서는 app.param() 메소드를 사용하여 라우트 매개변수를 다룬다. 이 메소드를 사용하여 articleId 


    라우트 매개변수를 포함하는 모든 요청에 대해 미들웨어를 붙이게 허용할 것 이다. 


    설명은 이쯤하고 일단 만들어보자.  먼저 app/controllers/ 폴더의 articles.server.controller.js 파일을 열어서 


    코드를 추가하자. 


    exports.articleByID = function(req,res, next, id) {
        Article.findById(id).populate('creator', 'username')
        .exec(function(err, article) {
            if(err) {
                return next(err);
            }
            if(!article) {
                return (new Error('Failed to load article' + id));
            }
    
            req.article = article;
            next();
        });
    };
    



    위 코드를 보면 미들웨어 함수 서식은 모든 익스프레스 미들웨어 인수와 id 인수를 포함한다. 


    그리고 나서 이 함수는 id 인수를 사용해 글을 찾고 req.article 속성을 사용해 이를 참조한다. 그리고 


    몽구스 모델의 populate 메소드를 사용해 username 필드를 추가 하였다. 



    이제 익스프레스 라우트를 연결 할 때 , 다양한 라우터에 articleByID() 미들웨어를 추가하는 방법을 알아 볼 것 


    이다. 하지만 지금 당장은 articles 객체를 반환하는 익스프레스 컨트롤러의 read() 메소드를 추가해보자. 


    read() 메소드를 추가 하기 위해 다시 app/controllers/ 폴더의 articles.server.controller.js 파일에 코드를 


    추가 하자. 


    exports.read = function(req,res) {
        res.json(req.article);
    };
    


    상당히 단순하다. 이는 이미 위에서 articleByID 미들웨어를 사용해 article 객체를 얻었기 때문에 단순히 JSON 표현


    으로 article 객체를 출력하기만 하면 끝나기 때문이다. 


    이제 이어서 update() 메소드를 구현해보자. 이 메소드는 article 객체를 읽어온 뒤 title 과 content 를 수정하는 


    역활을 할 것 이다. app/controllers/ 폴더의 articles.server.controller.js 파일에 코드를  추가 하자. 


    exports.update = function(req,res) {
        var article = req.article;
    
        article.title = req.body.title;
        article.content = req.body.content;
    
        article.save(function(err){
            if(err) {
                return res.status(400).send({
                    message : getErrorMessage(err)
                });
            } else {
                res.json(article);
            }
        });
    };
    


    보다시피 update() 메소드는 articleByID() 미들웨어를 사용해서 article 객체를 이미 얻었다고 가정하고 req.article 


    로 article 객체를 얻은 뒤 title 과 content 를 바꾼다. 그리고 save() 메소드를 사용해 저장을 하는 방식이다. 


    마지막으로 delete() 메소드를 만들어 보자. 


    app/controllers/ 폴더의 articles.server.controller.js 파일에 코드를  추가 하자. 


    exports.delete = function(req,res) {
        var article = req.article;
    
        article.remove(function(err){
            if(err) {
                return res.status(400).send({
                    message : getErrorMessage(err)
                });
            } else {
                res.json(article);
            }
        });
    };
    


    update() 메소드와 마찬가지로 delete() 메소드 역시 articleByID() 미들웨어를 사용해서 article 객체를 이미 


    얻었다고 가정하고 remove() 메소드로 article을 삭제한다. 


    자 이제 CRUD 모듈은 완성하였다.  여기에 추가로 사용자 인증을 위한 미들웨어를 추가 해보자. 



    CRUD 모듈에서 사용자의 인증유무를 확인하여 인증받지 않았다면 컨트롤러 메소드를 수행할 수 없도록 차단을 


    해야 한다. user 의 인증관련 메소드 이므로 user 컨트롤러에서 이를 구현할 것 이다. 


    이를 위해 app/controllers 폴더의 users.server.controller.js 파일을 열어서 다음 코드를 추가 하자. 


    exports.requiresLogin = function(req,res,next) {
        if (!req.isAuthenticated()) {
            return res.status(401).send({
                message : 'User is not logged in'
            });
        }
        next();
    };
    
    



    requiresLogin() 미들웨어는 사용자가 인증되었는지를 점검하기 위해 패스포트가 초기화한 req.isAuthenticated() 


    메소드를 사용한다. 사용자가 로그인을 했다면 연쇄에 몰려있는 다음 메소드를 호출하고, 그렇지 않으면 인증


    오류와 HTTP 오류코드로 응답 할 것 이다. 여기서 특정사용자가 특정 행동을 수행하게 인가되었는지를 점검


    하기 위한 인가 미들웨어 역시 구현해야 한다. (글 수정과 삭제 를 글 작성자만 수행 할 수 있도록 해야 한다.)



    이를 위해 app/controllers/ 폴더의 articles.server.controller.js 파일에 코드를  추가 하자. 


    exports.hasAuthorization = function(req,res,next) {
        if(req.article.creator.id !=== req.user.id) {
            return res.status(403).send({
                message : 'User is not authorized'
            });
        }
        next();
    };
    



    이 코드를 살펴보면 hasAuthorization 미들웨어는 사용자가 현재 글의 작성자인지 검증하기 위해 req.article 과


    req.user 객체를 사용한다. 이 미들웨어는 또한 articleId 라우트 매개변수를 포함한 요청만 수행 가능하다고 가정


    한다. 모든 메소드와 미들웨어를 배치 했으므로 , 이제 CRUD 모듈 수행을 위해 라우트 배선을 할 시점이 왔다. 


    이는 다음 장에서 다루도록 하겠다. -_-


    마지막으로 오늘 만든 articles.server.controller.js 파일의 전체 코드를 확인하자. 


    var mongoose = require('mongoose'),
        Article = mongoose.model('Article');
    
    var getErrorMessage = function(err) {
        if(err.errors) {
            for (var errName in err.errors) {
                if(err.errors[errName].message) {
                    return err.errors[errName].message;
                }
            }
        } else{
            return 'Unknown server error';
        }
    };
    
    exports.create = function(req,res) {
        var article = new Article(req.body);
        article.creator = req.user;
    
        article.save(function(err) {
            if(err) {
                return res.status(400).send({
                    message : getErrorMessage(err)
                });
            }else{
                res.json(article);
            }
        })
    };
    
    exports.list = function(req,res) {
        Article.find().sort('-created').populate('creator', 'username')
        .exec(function(err,articles) {
            if(err) {
                return res.status(400).send({
                    message : getErrorMessage(err)
                });
            }else{
                res.json(articles);
            }
        });
    };
    
    
    exports.articleByID = function(req,res, next, id) {
        Article.findById(id).populate('creator', 'username')
        .exec(function(err, article) {
            if(err) {
                return next(err);
            }
            if(!article) {
                return (new Error('Failed to load article' + id));
            }
    
            req.article = article;
            next();
        });
    };
    
    exports.read = function(req,res) {
        res.json(req.article);
    };
    
    exports.update = function(req,res) {
        var article = req.article;
    
        article.title = req.body.title;
        article.content = req.body.content;
    
        article.save(function(err){
            if(err) {
                return res.status(400).send({
                    message : getErrorMessage(err)
                });
            } else {
                res.json(article);
            }
        });
    };
    
    exports.delete = function(req,res) {
        var article = req.article;
    
        article.remove(function(err){
            if(err) {
                return res.status(400).send({
                    message : getErrorMessage(err)
                });
            } else {
                res.json(article);
            }
        });
    };
    
    exports.hasAuthorization = function(req,res,next) {
        if(req.article.creator.id !== req.user.id) {
            return res.status(403).send({
                message : 'User is not authorized'
            });
        }
        next();
    };
    
    


    (끝)

    댓글