04-16 15:08
Notice
Recent Posts
Recent Comments
관리 메뉴

Scientific Computing & Data Science

[MongoDB] Applications / Node.js-MongoDB를 이용한 회원 관리 페이지 만들기 본문

Data Science/MongoDB

[MongoDB] Applications / Node.js-MongoDB를 이용한 회원 관리 페이지 만들기

cinema4dr12 2014. 3. 12. 10:50

by Geol Choi | 

프로젝트 파일 다운로드

이번 글에서는 Node.js를 이용한 서버 스크립팅과 Node.js 상에서 구동되는 MongoDB 모듈인 Mongoose를 이용하여 회원 관리 및 블로그 포스팅 기능이 포함된 웹 페이지를 만들어 보도록 하겠다.

서버 응답에 대하 클라이언트-사이트 HTML 렌더링 방식은 HTML5의 JavaScript 형식을 지원하는 "ejs(Embedded JavaScript)"가 활용되었다. 이를 위해, Node.js의 ejs 엔진 모듈이 사용되었다.



차례

  1. 사전 이해 요구사항
  2. 사전 준비사항
  3. MongoDB 설치 및 실행
  4. Node.js 설치 및 실행
  5. 웹 페이지 전체 구조
  6. 프로젝트 폴더 전체 구조
  7. Node.js 패키지 모듈 설치
  8. 전체 논리 흐름
  9. app.js
  10. index.js
  11. memberdb.ejs
  12. dataview.ejs
  13. sign_up.ejs
  14. 맺음말



사전 이해 요구사항

    본 프로젝트를 이해하기 위해 사전에 어느 정도 이해(물론 각각에 대한 깊이있는 이해는 필요없다)가 필요한 항목들이다. 즉, 본 프로젝트에서 사용된 프레임웍들이라고 생각하면 된다.

    • HTML5 기본 태그

    • CSS3 클래스 개념

    • Twitter Bootstrap CSS Foundation 기본

    • Node.js JavaScript Framework

    • MongoDB 기초

    • Mongoose : Node.js와 MongoDB 연동을 위한 Node.js 패키지 모듈

    • EJS (Embedded JavaScript) Framework 기본

    • Node.js express JavaScript Framework 기본

    • Naver SmartEditor 기본

    • PHP 기본



    사전 준비사항


    • 웹페이지 서버에 MongoDB 설치 및 MongoDB 서버 실행
    • 웹페이지 서버에 Node.js 설치

    상기 사전 준비사항 각각에 대해서는 아래에 자세하게 설명하도록 하겠다.



    MongoDB 설치 및 실행


    MongoDB는 MongoDB 공식사이트에서 다운로드 받을 수 있다. MongoDB 실행을 쉘(또는 콘솔) 내에서 글로벌로 인식하도록 하려면 환경변수 설정이 필요한데, 이에 대한 자세한 내용은 [MongoDB-Node.js 연동하기 / 2. MongoDB 환경변수 설정]을 참고하기 바란다. 만약 설치 시 자동으로 환경변수가 설정된다면, 이 부분은 넘아가도록 한다.

    MongoDB 서버를 실행하기 전에 체계적인 DB 관리를 위해 DB 폴더를 따로 만드는 것이 바람직하다. 서버 실행 시, DB 폴더 위치를 따로 지정할 수 있으며, 향후 DB 이전 등 관련 이슈가 있을 시 보다 손쉽게 관리할 수 있을 것이라 생각하기 때문이다.

    설명을 위해 DB 폴더를 다음 위치에 생성한다:

    [MongoDB 설치 경로]/db/data

    이제 MongoDB 서버를 실행하는 방법에 대해 알아보자. 서버 실행파일은 "mongod"이며, OS에 따라 Unix 실행파일 또는 .exe의 확장자를 갖는다.

    명령 쉘(Mac의 경우 Terminal, Windows의 경우 Console)을 실행하고, 쉘에서 다음 명령을 입력한다:

    $ mongod -dbpath [MONGODB_DATA_PATH]

    만약 환경변수가 설정되지 않은 경우, 반드시 MongoDB의 설치 폴더 내 "bin" 폴더 내에서 위의 명령어를 실행해야 한다.

          [그림 1.] MongoDB 서버 실행 예



    Node.js 설치 및 실행


    Node.js는 Node.js의 공식사이트에 다운로드 할 수 있다. Node.js의 환경변수 설정은 [MongoDB-Node.js 연동하기 / 3. Node.js 환경변수 설정]을 참고하기 바란다.

    Node.js가 제대로 설치되었다면 다음과 같이 테스트 해 보자:

    $ node > console.log('Hello, world!') Hello, world! undefined

    위와 같이 실행된다면 Node.js가 제대로 설치된 것이다.



    웹 페이지 전체 구조


    이제 본격적으로 웹 페이지 제작 및 서버 스크립팅을 설명하도록 하겠다.

    가장 먼저 해야할 일은, 만들고자 하는 웹 페이지의 전체 구조를 파악하는 것이다. 웹 페이지 구성과 각각에 대한 설명은 다음과 같다:

    • Main : 메인 페이지
    • Members : 회원 관리 페이지 (MemberName / Password / Email 등)
    • Data View : 콘텐츠 표시, 작성된 포스팅을 볼 수 있음
    • Login : 로그인 버튼 (로그아웃 상태에서만 볼 수 있음)
      • Logout : 로그아웃 버튼 (로그인 상태에서만 볼 수 있음)
      • Insert Data : 콘텐츠 포스팅 버튼 (로그인 상태에서만 볼 수 있음)
    • Sign up : 회원가입 버튼 (로그아웃 상태에서만 볼 수 있음)


    각 페이지 별 웹 페이지의 모양은 [그림 2 - 7]과 같다:

    [그림 2.] Main 페이지: 본 웹 페이지에 접속하면 가장 먼저 보이는 페이지이다.


    [그림 3.] Members 페이지 : 가입된 회원의 ID/Password/Email을 확인할 수 있다.


    [그림 4.] Data View 페이지 : 회원이 작성한 포스팅의 내용을 확인할 수 있다.


    [그림 5.] Login 페이지 : 모달(Modal)로 창이 뜨며 가입된 회원이 대해 ID/Password를 입력하면 로그인 할 수 있다. 로그인을 해야만 글을 작성할 수 있다.


    [그림 6.] Sign up 페이지 : 회원 가입을 할 수 있다.


    [그림 7.] Logout 버튼 : 로그인이 되면 자동으로 Logout 버튼이 활성화 된다.



    프로젝트 폴더 구조


    Node.js의 서버-사이드 스크립팅을 지원하는 기본적인 패키지 모듈은 "connect"와 "express"가 있다. "connect"는 비교적 간단한 처리용으로는 적합하지만 복잡한 처리를 하기에는 다소 무리가 있기 때문에 복잡한 처리에 보다 적합한 "express" 패키지 모듈이 사용되었다.

    사실, express 패키지 모듈을 설치하고 "express authentication" 명령을 통해 기본 프로젝트 폴더 구조를 자동으로 생성할 수가 있는데, 이는 프로젝트를 처음부터 생성할 때 필요한 것이며, 본 설명에서는 필자가 이미 기본 구조를 만들어 놓았으므로 이 구조를 가지고 설명하도록 하겠다.

    만약 "express authentication" 명령을 통해 기본 프로젝트 폴더 구조를 자동으로 생성할 경우 express는 디폴트로 "EJS" 엔진 대신 "또다른 임베디드 HTML 형식인 "Jade" 엔진을 사용한다. 따라서, "EJS" 엔진으로 변경하려면 추가적인 스크립팅이 필요한데 차후 자세히 설명하도록 하겠다.

    전체 프로젝트 폴더 구조와 각각의 설명은 다음과 같다:

    • Blog : 프로젝트 루트 폴더
      • node_modules : Node.js의 패키지 모듈 폴더
        • ejs : Embedded JavaScript 패키지 모듈 폴더
        • ejs-locals : ejs-locals 패키지 모듈 폴더 / EJS를 레이아웃으로 사용하기 위함
        • express : Express3 패키지 모듈 폴더
        • mongoose : mongoose 패키지 모듈 폴더
      • public : CSS, 폰트, 이미지, 스마트에디터 등의 리소스 폴더
      • routes : 라우팅 처리를 위한 JavaScript 폴더
      • views : 렌더링용 ejs 파일들

    반드시 유념해야 할 사항이 있는데, express를 활용하여 멀티페이지 웹 어플리케이션을 개발하려면 위와 같은 폴더 구조를 지켜야 한다는 것이다. (이것은 규칙이자 약속이다.)

    또한 각 폴더 내 파일들은 다음과 같다(public 폴더는 리소스를 포함하는 것이므로 설명하지 않았다):

    • Blog
      • app.js : Node.js의 서버 실행용 JavaScript
      • package.json : 모듈 의존성(dependency) 등에 대한 정보 포함
    • node_modules
      • ejs : Embedded JavaScript 패키지 모듈 관련 파일들
      • ejs-locals : ejs-locals 패키지 모듈 관련 파일들
      • express : Express3 패키지 모듈 관련 파일들
      • mongoose : mongoose 패키지 모듈 관련 파일들
    • routes
      • index.js : app.js의 라우팅 처리를 위한 JavaScript 파일
      • user.js : express authentication 명령에 의해 자동 생성되는 JavaScript 파일
    • views
      • index.ejs : Main 페이지 렌더링용 ejs 파일
      • memberdb.ejs : Members 페이지 렌더링용 ejs 파일
      • dataview.ejs : Data View 페이지 렌더링용 ejs 파일
      • sign_up.ejs : Sign up 페이지 렌더링용 ejs 파일
      • layout.ejs : Layout 페이지 렌더링용 ejs 파일

    "views" 폴더의 파일들 중 뜬금없이 "layout.ejs" 파일이 무엇인지 궁금할 수 있다. 이름이 의미하는 바와 같이, 전체적인 레이아웃을 구성하는 역할을 하는데, 예를 들어 사용자가 "Main", "Members", "Data View" 등을 클릭하여 페이지 이동 시 이들 페이지가 공통적으로 포함하는 부분이 있다. 이 부분을 "layout.ejs"가 담당하게 함으로써 페이지 간에 공통적인 부분을 처리할 수 있도록 하는 것이다.

    다음 그림을 보면 좀 더 쉽게 이해될 것이다.

    [그림 8.] "Layout"과 "Main"의 페이지 분리. "Main", "Members", "Data View"로 페이지가 이동하여도 "Layout" 부분은 공통적으로 유지된다.


    "Main", "Members", "Data View" 등 서로 다른 페이지들이 "layout.ejs"의 내용을 포함하는 방법에 대해서는 차차 설명하도록 하겠다.



    Node.js 패키지 모듈 설치


    현재 프로젝트 폴더 내 "node_modules" 폴더의 각 폴더를 보면 내용이 비어있을 것이다. 이것은 Node.js 서버를 구동하는 서버의 OS에 따라 설치될 파일들이 약간 다를 수 있기 때문에, 이 글을 읽으면서 따라하는 독자가 Node.js 패키지 모듈을 직접 설치하는 연습을 할 수 있도록 하는 필자의 배려이기도 하고 근본적으로는 OS에 따라 패키지 내 파일이 다르기도 하기 때문이다.

    패키지 설치를 위해 쉘(또는 콘솔) 상에서 프로젝트 폴더의 루트 폴더("Blog")로 이동한다.

    이제 "npm install - " 명령을 통해 패키지를 하나씩 설치하도록 하자.

    (잠깐! 설치 전 Node.js의 명령이 글로벌로 실행될 수 있도록 환경변수가 설정되어 있는지 확인해 보도록 한다. 글로벌로 실행되지 않는 경우 해당 프로젝트 폴더에서 "npm" 명령이 실행되지 않는다.)

    [ejs 패키지 모듈 설치]

    $sudo npm install ejs

    [ejs-locals 패키지 모듈 설치]

    $sudo npm install ejs-locals

    [express 패키지 모듈 설치]

    $sudo npm install express

    [mongoose 패키지 모듈 설치]

    $sudo npm install mongoose

    참고로 "sudo"는 Unix 명령어로써 Windows의  "관리자 권한으로 실행"과 유사한 개념이다. 아마도 실행 시 root password를 입력해야 할 것이다. 만약 Windows 사용자라면 sudo를 제외하고 명령어를 입력하면 된다. 권리자 권한의 실행이 필요없다면 Unix 계열에서도 sudo를 제외하고 명령어를 입력한다.

    이로써 본 프로젝트에 필요한 Node.js 패키지 모듈 설치를 모두 완료하였다.



    전체 논리 흐름(Entire Logical Flow)


    지금까지 프로젝트 폴더 구조, 폴더 내 파일의 의미, 필요 패키지 모듈 설치하는 방법 등에 대하여 알아보았다. 이번 섹션에서는 각 JavaScript 파일들을 세밀하게 펼쳐보기 전에 "app.js", "index.js", 그리고 관련 ejs 파일들이 어떻게 연동되는지 살펴보자.

    간과하지 말아야 할 사항은, 전체 논리 흐름은 한번에 이해되기 쉽지 않을 것이다. 그저 "이런 식으로 파일들 간에 논리 흐름이 있구나" 정도의 감을 갖는 것이 중요하다. 대략적인 논리 흐름 정도만 파악하면 다음 섹션에서부터 심도있게 다룰 JavaScript 함수의 내용을 이해하는데 큰 도움이 될 것이다. 세밀한 내용을 파악한 후, 다시 전체 논리 흐름 보게 되면 100% 이해를 할 수 있게 되리라 믿는다.

    이제부터 나오는 작은 따옴표 내의 /...는 웹 브라우저의 주소창에서 서버 호스트 아이피 뒤에 붙는 경로이다. 예를 들어, 로컬 호스트('localhost:[port_num[' 또는 '127.0.0.1:[port_num]')인 경우 '/MEMBERDB'는 "localhost:[port_num]/MEMBERDB" 또는 "127.0.0.1:[port_num]/MEMBERDB"를 의미한다. 본 프로젝트에서 사용하는 포트 번호는 3000이므로 "localhost:3000/MEMBERDB"이다.

    '/'는 "localhost:3000/"이며, 실제적으로는 '/'를 제외한 "localhost:3000" 또는 "127.0.0.1:3000"과 동일한 의미를 갖는다.

    도메인을 얻기 전까지는 개발 단계에서는 로컬 호스트를 이용하기 때문에, Node.js가 로컬 서버로 구동되며 포트번호는 3000번을 사용하고 있다고 가정하고 설명을 하도록 하겠다.


    ['/'] : 'localhost:3000/' 또는 'localhost:3000'

    [그림 9.] Main 페이지에 대한 논리 흐름.


    ['/MEMBERDB']'localhost:3000/MEMBERDB'

    [그림 10.] Members 페이지에 대한 논리 흐름.


    ['/DATAVIEW']'localhost:3000/DATAVIEW'

    [그림 11.] Data View 페이지에 대한 논리 흐름.


    ['/LOGIN']'localhost:3000/LOGIN'

    [그림 12.] Log in에 대한 논리 흐름.


    ['/SIGN_UP']'localhost:3000/SIGN_UP'

    [그림 13.] Sign up에 대한 논리 흐름.


    ['SUBMIT']'localhost:3000/SUBMIT'

    [그림 14.] Submit에 대한 논리 흐름.


    이제 전체적인 논리 흐름을 어렴풋이나마 파악하였다면 주요 코드에 대해 하나씩 자세히 알아보기로 하겠다.



    app.js


    코드 전체를 한줄 한줄 설명하는 것은 지루한 일이며 어떤 면에서는 시간낭비가 될 수도 있으므로 중요하다고 판단되는 부분에 대해서만 다루도록 하겠다. 나머지 부분에 대해서는 Node.js 또는 패키지 모듈에 해당 공식사이트의 도큐먼트를 참고하기 바란다.

    [part 1]

    /** * Module dependencies: ejs / ejs-locals / express / mongoose */ var express = require('express') , routes = require('./routes') , user = require('./routes/user') , http = require('http') , engine = require('ejs-locals') , fs = require('fs') , util = require('util') , url = require('url') , crypto = require('crypto') , path = require('path');

    모듈 의존성(depenency)를 명시하는 부분이다. 이것은 마치 C코드의 "include", C# 코드의 "using", Jave 코드의 "import" 등과 같이 패키지 또는 라이브러리를 호출하는 것과 유사하다.


    [part 2]

    // ------- Hash Key Generation for Password ------- var myHash = function myHash(key){ var hash = crypto.createHash('sha1'); hash.update(key); return hash.digest('hex'); }

    회원가입 시 사용자의 패스워드를 암호화하는 부분이다. Hash key로 변환하는 함수이며, 헥사코드를 결과로 반환한다.


    [part 3]

    // ------- Create Session ------- var createSession = function createSession(){ return function(req, res, next){ if(!req.session.login){ req.session.login = 'logout'; } next(); }; };

    서버 측에서 사용자가 로그인 상태인지 로그아웃 상태인지를 판단하기 위해 "req.session.login"에 문자열 형태로 'login' 또는 'logout'을 입력받는다.


    [part 4]

    // ------- all environments ------- app.set('port', process.env.PORT || 3000); app.set('views', __dirname + '/views'); app.set('view engine', 'ejs'); app.use(express.favicon()); app.use(express.logger('dev')); app.use(express.bodyParser({limit:'800mb', uploadDir:__dirname + '/public/se/uploadTmp'})); app.use(express.methodOverride()); app.use(express.cookieParser()); app.use(express.session({ secret: 'keyboard cat', cookie: { maxAge: 600000 }})); app.use(createSession()); app.use(app.router); app.use(express.static(path.join(__dirname, 'public'))); app.engine('ejs',engine);

    몇가지 주요 라인에 대해서 설명하도록 하겠다.

    • app.set('view engine', 'ejs'); : view engine을 'ejs'로 설정한다.
    • app.use(express.bodyParser({limit:'800mb', uploadDir:__dirname + '/public/se/uploadTmp'})); : 사용자가 포스팅한 글을 불러오기 위한 것이다. 포스팅 된 글은 HTML의 <body> 태그로 삽입되며, 이 태그 내의 DOM을 파싱하기 위함이다.
    • app.use(express.session({ secret: 'keyboard cat', cookie: { maxAge: 600000 }}));블로그나 은행 등의 웹 사이트를 이용하다 보면 로그인 후 장시간 아무 반응이 없을 경우 자동으로 로그아웃 처리되는 것을 경험한 적이 있을 것이다.


    [part 5]

    app.get('/',routes.index); // Main (page: 0)

    웹 브라우저의 주소창에 'localhost:3000/'가 입력되거나 또는 Main 페이지로 이동 시 실행된다. "get" 방식으로 실행되며, "index.js"의 "exports.index"로 라우팅 된다.

    서버에 데이터 처리를 요청할 때, "get"과 "post" 두 가지 방식이 있는데, 차이에 대한 자세한 설명은 이 곳을 참고하기 바란다.


    [part 6]

    app.get('/MEMBERDB', routes.member_db); // Members (page: 1)

    웹 브라우저의 주소창에 'localhost:3000/MEMBERDB'로 이동하거나 또는 Members 버튼 클릭 시 실행된다. "get" 방식으로 실행되며, "index.js"의 "exports.member_db"로 라우팅 된다.


    [part 7]

    app.get('/DATAVIEW', routes.dataview); // Data View (page: 2)

    웹 브라우저의 주소창에서 'localhost:3000/DATAVIEW'로 이동하거나 또는 Data View 버튼 클릭 시 실행된다. "get" 방식으로 실행되며, "index.js"의 "exports.dataview"로 라우팅 된다.


    [part 8]

    // Login Button app.post('/LOGIN', function(req,res,next) { req.username = req.body.username; req.password = myHash(req.body.password); next(); },routes.login_post);

    웹 브라우저의 주소창에서 'localhost:3000/LOGIN'으로 이동하거나 또는 Login 버튼 클릭 시 실행된다. "post" 방식으로 실행되며, 사용자의 아이디(username)을 입력받고, 패스워드(password)는 HashKey 형식으로 입력받는다. next()는 실행이 완료된 후 "routes.login_post()"에 의해 "index.js"의 "exports.login_post"로 라우팅 된다.


    [part 9]

    // Logout Button (Enabled after loggin in) app.get('/LOGOUT', routes.logout);

    Logout 버튼 클릭 시 실행되며, "index.js"의 "exports.logout"으로 라우팅 된다.


    [part 10]

    // Sign up Button app.get('/SIGN_UP', routes.sign_up); app.post('/SIGN_UP', function(req,res,next) { if(req.body.password == req.body.confirm_password) { req.username = req.body.username; req.password = myHash(req.body.password); req.email = req.body.email; next(); } else { res.redirect('/'); }; },routes.sign_up_post);

    Sign up 버튼 클릭 시 실행된다. "get"에 의해 "indexjs"의 " exports.sign_up"으로 라우팅 되는 동시에, "post"에 의해 사용자가 입력한 패스워드와 재입력한 패스워드가 동일한지 판단하여, 동일하다면 가입을 허락하고 사용자의 username, (HashKey로 변환 된) password, email을 저장한 후, "index.js"의 "exports.sign_up_post"로 라우팅 된다. 만약 입력한 패스워드와 재입력한 패스워드가 다를 경우 Main 페이지로 이동시킨다.


    [part 11]

    // Check User Name Button (Displayed in Sign up page) app.get('/CHECKUSERNAME',routes.checkusername);

    Sign up 페이지에서 "Check User Name" 버튼 클릭 시 실행되며, "index.js"의 "exports.checkusername"으로 라우팅 된다.


    [part 12]

    // Naver SmartEditor Image Upload app.post('/UPLOAD',function(req,res) { var str = req.header('User-Agent'); var os = str.search("Win"); // if Windows: n is a numberic data greater than 0, if not the case(Mac/Linux) n is equal to -1 var fileName = req.files.file.path; if(os == -1) { // for either Mac or Linux fileName = fileName.split('/')[fileName.split('/').length-1]; } else { // for Windows fileName = fileName.split('\\')[fileName.split('\\').length-1]; } res.writeHeader(200,{'Content-Type':'text/plain'}); res.write('&bNewLine=true'); res.write('&sFileName=' + fileName); res.write('&sFileURL=/se/uploadTmp/' + fileName); res.end(); });

    사용자가 포스팅 시, 이미지 업로드를 처리하는 부분이다. 에디터는 Naver 스마트에디터와 연동하였다. 이미지의 파일 경로 설정 시 Mac OS와 Windows의 경로에서 폴더 구분자가 서로 다르기 때문에 현재 사용자의 OS를 판단하기 위해 "req.header('User-Agent')"를 사용하였다. 즉, 사용자의 OS가 Mac이나 Linux인 경우 변수 "os"에 -1이 입력되며 Windows의 경우 0과 같거나 보다 큰 정수값이 입력된다.


    [part 13]

    // Naver SmartEditor Content Submit app.post('/SUBMIT', routes.insertData);

    사용자가 글 작성을 완료 후 "Submit" 버튼을 클릭할 때 실행되며, "index.js"의 "exports.insertData"로 라우팅 된다.



    index.js


    index.js는 "routes" 폴더 내에 있어야 하며, 주로 "app.js"로부터의 라우팅 된 데이터를 처리한다.


    [part 1]

    // ------- DB Connection ------- var fs = require('fs'); var mongoose = require('mongoose'); var url = require('url');

    모듈 의존성을 명시한 것인데, MongoDB의 Node.js 모듈 패키지인 Mongoose의 모듈 의존성에 주목할 필요가 있다.


    [part 2]

    // ------- connects to MongoDB ------- mongoose.connect('mongodb://localhost/membership');

    MongoDB 서버와 연결하는 부분이다. 현재 MongoDB 서버는 로컬로 실행되고 있으며 DB의 이름은 "membership"으로 설정하였다.


    [part 3]

    // ------- get the connection from mongoose ------- var db = mongoose.connection;

    연결된 MongoDB의 인스턴스를 가져오고 이를 변수 "db"에 입력한다.


    [part 4]

    // ------- creates DB schema for MongoDB ------- // for membership var memberSchema = mongoose.Schema({ username: 'string', password: 'string', email: 'string' }); // for contents var dataSchema = mongoose.Schema({ title: 'string', content: 'string', });

    MongoDB의 DB 스키마를 명시하는 부분이다. 회원 정보 관리를 위한 "memberSchema"와 포스팅 되는 콘텐츠 관리를 위한 "dataSchema"의 두 개의 스키마를 정의하였다.


    [part 5]

    // ------- compiles our schema into a model ------- var Member = mongoose.model('Member', memberSchema); var Data = mongoose.model('Data', dataSchema);

    [코드 4]에서 정의된 DB 스키마를 모델로 컴파일한다. 회원 관리 스키마의 모델은 "Member", 포스팅 콘텐츠 관리를 위한 스키마의 모델은 "Data"이다.


    [part 6]

    // ------- route functions ------- // Main (page: 0) exports.index = function(req, res) { res.status(200); res.render('index', { title: 'GCHOI', page: 0, url: req.url, login: req.session.login, username: req.session.username }); };

    [코드 6] 이후로는 모두 라우팅 된 데이터 처리 함수이다. res.render()의 첫번째 인자는 렌더링 할 ejs 파일명을 의미한다. 즉, index.ejs 파일과 연동된다는 것이며, index.ejs에 넘겨 줄 데이터가 명시되었다. 예를 들면, "title"이라는 변수에는 'GCHOI' 를, "page"라는 변수에는 0을 넘겨준다.


    [part 7]

    // Members (page: 1) exports.member_db = function(req, res) { var uri = url.parse(req.url,true).query; if(uri.cmd == "del"){ Member.remove({_id: uri.id}, function(err,result) { res.status(300); res.redirect('/MEMBERDB'); }); } else{ res.status(200); Member.count({}, function(err, count){ Member.find({}, function(err,result){ res.render('memberdb', { title: 'Member DB', page: 1, url: req.url, database: 'local', collectionName: 'members', documentCount: count, myMember: result, login: req.session.login, username: req.session.username }); }); }); } };

    Member 페이지의 회원 정보를 표시 처리를 하는 부분이다. 

    우선 if(uri.cmd == "del")은 member.ejs에서 "del"이라는 "cmd"가 요청되는 경우를 처리하는 것인데 Members 페이지에서 회원을 삭제 요청이 실행된다. 삭제가 완료되면 res.redirect('/MEMBERDB')에 의해 /MEMBERDB로 이동된다.

    삭제 명령이 아닌 경우, "memberdb.ejs"를 렌더링하게 된다.


    [part 8]

    // Data View (page: 2) exports.dataview = function(req, res) { Data.count({}, function(err, count) { Data.find({}, function(err,result) { for(var i = 0; i < count; i++) { unblockTag(result[i].content); }; res.status(200); res.render('dataview', { title: 'Data View', page: 2, documentCount: count, myData: result, url: req.url, login: req.session.login, username: req.session.username }); }); }); };

    Data View 페이지에 대한 라우팅을 처리하는 부분이다. 현재 DB에 저장된 포스팅 수를 알아내고(Data.count({}, function(err, count)), DB로부터 포스팅 된 콘텐츠를 모두 불러내어(Data.find({}, function(err,result)) 최신 포스팅이 가장 위로 위치할 수 있도록 한다. 그리고 불러온 내용을 "dataview.ejs"에 전달하고 렌더링한다.


    [part 9]

    // Login Post exports.login_post = function(req, res) { res.status(200); // Pull Member info out of MongoDB here... Member.findOne({ username: req.username, password: req.password }, function (err, member) { if(member != null) { req.session.login = 'login'; req.session.username = req.username; }; res.status(200); // after logging in, stay in the current page res.redirect(url.parse(req.url,true).query.url); }); };

    Login 관련 데이터를 처리하는 부분이다. 사용자의 아이디(req.username)과 패스워드(req.password)로 쿼리하여 DB로부터 회원을 찾아내고, 아이디와 패스워드가 일치하면(member != null 이면) 로그인 상태로 변경하고(req.session.login = 'login';), 로그인을 했던 시점의 페이지로 복귀한다(res.redirect(url.parse(req.url,true).query.url);).


    [part 10]

    // Logout exports.logout = function(req, res) { req.session.login = 'logout'; res.status(200); // after logging out, stay in the current page res.redirect(url.parse(req.url,true).query.url); };

    Logout을 처리하기 위한 라우팅 함수이다. 로그아웃을 요청하면 로그인 상태에서 로그아웃 상태로 변경하고(req.session.login = 'logout';), 로그아웃을 요청했던 시점의 페이지로 복귀한다(res.redirect(url.parse(req.url,true).query.url);).


    [part 11]

    // Sign up exports.sign_up = function(req, res) { res.status(200); res.render('sign_up', { title: 'Sign up', url: req.url, page:5, login: req.session.login, username: req.session.username, existingUsername: 'null' }); };

    Sign up을 처리하기 위한 라우팅 함수이며, "sign_up.ejs"에 데이터를 실어서 렌더링한다.


    [part 12]

    // Check Username exports.checkusername = function(req, res) { var uri = url.parse(req.url,true); Member.findOne({ username: uri.query.id }, function (err, member) { res.writeHead(200, {'Content-Type': 'text/html'}); if(member != null) { res.end('true'); } else { res.end('false'); } }); }

    Sign up 페이지에서 아이디가 중복되는지를 판단하기 위한 함수이며, "Check User Name" 버튼 클릭 시 실행된다. 해당 아이디가 있는지 DB에 쿼리를 한다.


    [part 13]

    // Sign up Post exports.sign_up_post = function(req, res) { res.status(200); var curUsername = req.username; if(curUsername == "") { res.redirect('/'); } else { Member.findOne({ username: curUsername }, function (err, member) { if (err) return handleError(err); if(member == null) { // new username // add myMember into the model var myMember = new Member({ username: curUsername, password: req.password, email: req.email }); myMember.save(function (err, data) { if (err) {// TODO handle the error console.log("error"); } console.log('member is inserted'); }); res.redirect('/MEMBERDB'); } else { // in case that Username already exists res.redirect('/'); } }); } };

    Sign up의 완료 처리를 위한 라우팅 함수이다. 만약 Sign up 페이지에서 Username란에 아무 이름도 입력하지 않은 채(curUsername == "") Submit 버튼을 클릭하면 Main 페이지로 이동시킨다(res.redirect('/');).

    만약 정상적으로 회원 가입을 위한 정보를 입력했다면 DB에 Username, Password, Email을 저장한 후, Members 페이지로 이동한다(res.redirect('/MEMBERDB');). 기존의 다른 사용자와 아이디가 겹칠 경우 DB에 저장 안 하고 Main 페이지로 이동시킨다(res.redirect('/');).


    [part 14]

    // Insert user content(s) through Naver SmartEditor exports.insertData = function(req, res) { if(req.session.login=='login'){ var myData = new Data({ title: req.body.title, content: req.body.ir1}); myData.save(function (err, data) { if (err) {// TODO handle the error console.log("error"); } console.log('message is inserted'); }); } res.status(200); // after inserting data, stay in the current page res.redirect(url.parse(req.url,true).query.url); };

    로그인 후, Insert Data 버튼을 클릭하여 글을 작성 후 포스팅을 완료하는 라우팅 함수이다. 로그인 상태를 판별하여(req.session.login=='login') 로그인 상태인 경우에만 글을 작성할 수 있도록 처리하였다. 포스팅이 완료된 후에는 포스팅 요청을 한 시점의 페이지로 복귀한다(res.redirect(url.parse(req.url,true).query.url);).



    layout.ejs


    layout.ejs는 기본 레이아웃을 정의하는 HTML5 문서이다.


    [part 1]

    <head> <title><%=title%></title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel='stylesheet' type='text/css' href='/css/bootstrap.min.css'/> <link href="css/bootstrap-glyphicons.css" rel="stylesheet"> <script src="http://code.jquery.com/jquery.js"></script> <script type="text/javascript" src="../se/js/HuskyEZCreator.js" charset="utf-8"></script> <script src="/js/bootstrap.min.js"></script> <style> body { margin: 0; padding: 50px 0px 0px 150px; background: #CFCFC9 -webkit-gradient(linear, left top, left bottom, from(#B9B), to(#CFCFC9)); background: #CFCFC9 -moz-linear-gradient(top, #B9B, #CFCFC9); background-attachment:fixed; background-image : url('/images/bg.jpg'); color: #555; -webkit-font-smoothing: antialiased; font-family: Helvetica, Arial, sans-serif; } h1, h2, h3, h4, h5, h6, .h1,.h2,.h3,.h4,.h5,.h6 { font-family: Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; } hr { margin-top: 20px; margin-bottom: 20px; border: 0; border-top: 1px solid #AAA; } .page-header { padding-bottom: 9px; margin: 40px 0 0px; border-bottom: 0px solid #EEE; } </style> </head>

    <head>를 정의한 부분이며, Twitter의 Boostrap에 대한 명시하고, 커스텀 CSS를 명시하였다.


    [part 2]

    <!-- Login Modal --> <div class="modal fade" id="myModal"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <h4 class="modal-title">Member Login</h4> </div> <form method="post" action="/LOGIN?url=<%= url %>"> <div class="modal-body"> <fieldset> <div class="form-group"> <label>Username</label> <input type="text" class="form-control" name="username" id="username" placeholder="Username" maxlength="25"> </div> <div class="form-group"> <label>Password</label> <input type="password" class="form-control" name="password" id="password" placeholder="Password" maxlength="16"> </div> </fieldset> </div> <div class="modal-footer"> <button type="submit" class="btn btn-primary">Login</button> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> </div> </form> </div><!-- /.modal-content --> </div><!-- /.modal-dialog --> </div><!-- /.modal -->

    Log in 버튼 클릭 시 아이디와 패스워드를 입력할 수 있는 모달로 나오는 부분이다.


    [part 3]

    <!-- Insert Modal --> <div class="modal fade" id="insertModal"> <div class="modal-dialog" style="width:80%"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> <h4 class="modal-title">Insert Data</h4> </div> <form action="/SUBMIT?url=<%= url %>" method="post"> <fieldset> <div class="modal-body"> <div class="form-group"> <label for="exampleInputEmail">Title</label> <input type="text" class="form-control" name="title" id="irTitle" placeholder="Title" maxlength="25"> </div> <textarea name="ir1" id="ir1" rows="10" cols="100" style="width:100%; height:612px; display:none;"></textarea> <!--textarea name="ir1" id="ir1" rows="10" cols="100" style="width:100%; height:412px; min-width:610px; display:none;"></textarea--> </div> <div class="modal-footer"> <input type="button" class="btn btn-default navbar-btn" onclick="submitContents(this);" value="Submit" /> </div> </fieldset> </form> </div><!-- /.modal-content --> </div><!-- /.modal-dialog --> </div><!-- /.modal -->

    포스팅 작업 시 글과 사진을 입력할 수 있도록 모달로 나오는 부분이다.


    [part 4]

    <div class="navbar navbar-inverse navbar-fixed-top"> <ul class="nav navbar-nav"> <li><a class="navbar-brand" href="/"><span class="glyphicon glyphicon-fire"></span>GCHOI</a></li> <li <% if(page == 0){%> class="active" <% } %> > <a href="/">Main</a> </li> <li <% if(page == 1){ %>class="active" <% } %>> <a href="/MEMBERDB">Members</a> </li> <li <% if(page == 2){ %>class="active" <% } %>> <a href="/DATAVIEW">Data View</a> </li> </ul> <ul class="nav navbar-nav pull-right" style="margin:4px;"> <!-- Button trigger modal --> <% if(login =='login'){ %> <font color="white">Welcome, <%= username %>&nbsp&nbsp&nbsp&nbsp&nbsp</font> <% }else{ %> <% } %> <% if(login!='login'){ %><a data-toggle="modal" href="#myModal" class="btn btn-primary navbar-btn btn-small" style="height:32px">Login</a><% }else{ %> <a data-toggle="modal" href="/LOGOUT?url=<%= url %>" class="btn btn-primary navbar-btn btn-small" style="height:32px">Logout</a><% } %> <% if(login!='login'){ %><a href="/SIGN_UP" class="navbar-link"><button type="button" class="btn btn-default navbar-btn btn-small" style="height:32px">Sign up</button></a><% } %> <% if(login=='login'){ %><a data-toggle="modal" href="#insertModal" class="btn btn-default navbar-btn btn-small" style="height:32px">Insert Data</a><% } %> </ul> </div>

    레이아웃의 상단 네이게이션 바(NavBar)를 정의하는 부분이다. ejs의 문법에 주목할 필요가 있는데, <% ... %>와 같은 형식으로 데이터를 처리한다.

    • <li <% if(page == 0){%> class="active" <% } %> ><a href="/">Main</a></li> : page 값이 0이면(Main 페이지) Main 부분을 활성화한다.
    • <li <% if(page == 1){ %>class="active" <% } %>><a href="/MEMBERDB">Members</a></li> : page 값이 1이면(Members 페이지) Members 부분을 활성화한다.
    • <li <% if(page == 2){ %>class="active" <% } %>><a href="/DATAVIEW">Data View</a></li> : page 값이 2이면(Data View 페이지) Data View 부분을 활성화한다.

    또한 로그인 상태에 따라 우측 상단의 버튼을 변경한다. 즉,

    • 로그아웃 시,
      • Login
      • Sign up
    • 로그인 시,
      • Logout
      • Insert Data

    로 표시한다.


    [part 5]

    <div style="width:800px; background: #CFCFC9 -webkit-gradient(linear, left top, left bottom, to(#777), from(#CFCFC9));background: #CFCFC9 -moz-linear-gradient(top, #CFCFC9, #777)"> <div style="margin:0px 12px 0px 12px"> <div style="width:100%; height:16px"></div> <%- body %> <hr /> <font color=#333><center><img src="/images/gchoi.png" /></center></font> <font color=#BBB><center>with Express3 / MongoDB / EJS / Twitter Bootstrap</center></font> <div style="width:100%; height:20px"></div> </div> </div> <div style="width:800px; height:15px; background-color:#444"></div>

    앞서 layout.ejs는 다른 ejs의 공통되는 부분을 처리하기 위한 것이라고 설명한 바 있다. 그렇다면 다른 ejs 문서들이 layout.ejs를 포함할 수 있도록 해야 하는데, 위의 빨간색 글자로 표시한 <%- body %>이 바로 그 부분이다.


    [part 6]

    <script type="text/javascript"> var oEditors = []; // 추가 글꼴 목록 //var aAdditionalFontSet = [["MS UI Gothic", "MS UI Gothic"], ["Comic Sans MS", "Comic Sans MS"],["TEST","TEST"]]; nhn.husky.EZCreator.createInIFrame({ oAppRef: oEditors, elPlaceHolder: "ir1", sSkinURI: "../se/SmartEditor2Skin.html", htParams : { bUseToolbar : true, // 툴바 사용 여부 (true:사용/ false:사용하지 않음) bUseVerticalResizer : true, // 입력창 크기 조절바 사용 여부 (true:사용/ false:사용하지 않음) bUseModeChanger : true, // 모드 탭(Editor | HTML | TEXT) 사용 여부 (true:사용/ false:사용하지 않음) //aAdditionalFontList : aAdditionalFontSet, // 추가 글꼴 목록 fOnBeforeUnload : function(){ //alert("완료!"); } }, //boolean fOnAppLoad : function(){ //예제 코드 //oEditors.getById["ir1"].exec("PASTE_HTML", ["로딩이 완료된 후에 본문에 삽입되는 text입니다."]); }, fCreator: "createSEditor2" }); function pasteHTML() { var sHTML = "<span style='color:#FF0000;'>이미지도 같은 방식으로 삽입합니다.<\/span>"; oEditors.getById["ir1"].exec("PASTE_HTML", [sHTML]); } function showHTML() { var sHTML = oEditors.getById["ir1"].getIR(); alert(sHTML); } function submitContents(elClickedObj) { oEditors.getById["ir1"].exec("UPDATE_CONTENTS_FIELD", []); // 에디터의 내용이 textarea에 적용됩니다. // 에디터의 내용에 대한 값 검증은 이곳에서 document.getElementById("ir1").value를 이용해서 처리하면 됩니다. try { elClickedObj.form.submit(); } catch(e) {} } function setDefaultFont() { var sDefaultFont = '궁서'; var nFontSize = 24; oEditors.getById["ir1"].setDefaultFont(sDefaultFont, nFontSize); } </script>

    Naver 스마트에디터에 대한 데이터 처리를 하는 스크립트이다. 자세한 내용은 Naver 개발자 센터의 Naver 스마트에디터를 참고한다.



    index.ejs


    HTML5 구조로 된 Main 페이지 문서이다. index.ejs는 사실상 별 내용은 없다.

    <% layout('layout') -%> <div class="navbar"> <div class="container"> <div class="nav-collapse collapse navbar-responsive-collapse"> <ul class="nav navbar-nav"> <li><h3>Welcome to Dr. GEOL CHOI's Home</h3></li> </ul> </div><!-- /.nav-collapse --> </div> </div> <ul class="breadcrumb"> <br /> <p><a href="http://gchoi.net">http://www.gchoi.net</a></p> <p><a href="http://cinema4dr12.tistory.com">http://cinema4dr12.tistory.com</a></p> <br /> <center> <p><img src="images/CJ_BG.png" class="img-rounded" width="100%"/></p> </center> <br /> </ul>

    다만, 위의 빨간색 글자로 표시한 <% layout('layout') -%>는 layout.ejs와 연동하는 기능을 하는 부분임을 주목하자.



    memberdb.ejs


    memberdb.ejs는 Members 페이지를 표시하기 위한 HTML5 형식의 ejs 문서이다.


    [part 1]

    <% layout('layout') -%> <div class="navbar"> <div class="container"> <div class="nav-collapse collapse navbar-responsive-collapse"> <ul class="nav navbar-nav"> <li><a>Current Database : <%= database %></a></li> <li><a>Current Collection : <%= collectionName %></a></li> <li><a>Number of Documents : <%= documentCount %></a></li> </ul> </div><!-- /.nav-collapse --> </div> </div>

    앞서 설명한 바와 같이, <% layout('layout') -%>는 layout.ejs와 연동하는 부분이다.

    현재 데이터베이스의 형태(<li><a>Current Database : <%= database %></a></li>), 컬렉션 이름(<li><a>Current Collection : <%= collectionName %></a></li>), 도큐먼트 개수(<li><a>Number of Documents : <%= documentCount %></a></li>)에 대한 정보를 표시하고 있다.


    [part 2]

    <!-- begin list--> <table class="table table-hover" style = "background-color:#EEE"> <tr style = "background-color:#fff"> <td><font size=2pt><strong><center>MemberName</center></strong></font></td> <td><font size=2pt><strong><center>Password</center></strong></font></td> <td><font size=2pt><strong><center>Email</center></strong></font></td> <td><font size=2pt><strong><center>Edit</center></strong></font></td> <td><font size=2pt><strong><center>Delete</center></strong></font></td> <% for(var i = 0; i < documentCount; i++){ %> <tr> <td><%= myMember[i].username %></td> <td><%= myMember[i].password %></td> <td><%= myMember[i].email %></td> <td width=60><center><a href="#" style="text-decoration:none"><span class="glyphicon glyphicon-edit"></span></a></center></td> <td width=60><center><a href="/MONGO?cmd=del&id=<%= myMember[i]._id %>" style="text-decoration:none"><span class="glyphicon glyphicon-remove"></span></a></center></td> </tr> <% } %> </tr> <tr> </table> <!-- end list-->

    테이블 형식으로 회원 가입자의 아이디, 패스워드, 이메일과 회원정보 수정 및 삭제 아이콘을 표시하고 있다.



    dataview.ejs


    Data View 페이지의 내용을 표시한다.


    [part 1]

    <% layout('layout') -%> <!-- begin list--> <table class="table table-hover" style = "background-color:#ffffff"> <% for(var i = documentCount-1; i>=0; i--){ %> <tr> <td> <h2><%= myData[i].title %></h2> <% if(login=='login'){ %><p><a href="/EDIT?id=<%= myData[i]._id %>">Edit</a> / <a href="/DELETE?id=<%= myData[i]._id %>">Delete</a></p><% } %> <%- myData[i].content %> </td> </tr> <% } %> </table> <!-- end list--> <div class="alert alert-success">Running MongoDB server at http://localhost:3000</div>

    DB로부터 불러온 포스팅 된 콘텐츠를 표시한다. DB에 저장된 콘텐츠 형식은 HTML5 태그로 되어 있다.



    sign_up.ejs


    sign_up.ejs는 Sign up 버튼 클릭 시 보이는 페이지를 포함하는 문서이며, 회원 가입에 대한 항목을 포함하고 있다.


    [part 1]

    <div class="navbar"> <div class="container"> <div class="nav-collapse collapse navbar-responsive-collapse"> <ul class="nav navbar-nav"> <li><h4><strong>Interactive database management system</strong></h4></li> </ul> </div><!-- /.nav-cÏllapse --> </div> </div>

    페이지 내 제목을 표시하는 부분이다.


    [part 2]

    <ul class="breadcrumb"> <br /> <form method="post" action="/SIGN_UP"> <fieldset> <legend>Sign up</legend> <div class="form-group"> <label for="exampleInputEmail">Username</label> <input type="text" class="form-control" name="username" id="user" placeholder="Username" maxlength="25"> </div> <center><a onclick="checkUserName()" style="width:100%" class="btn btn-default">Check User Name</a></center> <div class="form-group"> <label for="exampleInputPassword">Password</label> <input type="password" class="form-control" name="password" id="password" placeholder="Password" maxlength="16"> </div> <div class="form-group"> <label for="exampleInputPassword">Confirm Password</label> <input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="Confirm Password" maxlength="16"> </div> <div class="form-group"> <label for="exampleInputEmail">Email</label> <input type="email" class="form-control" name="email" id="email" placeholder="Email" maxlength="64"> </div> <center><button type="submit" style="width:100%" class="btn btn-default">Submit</button></center> </fieldset> </form> <br /> </ul>

    회원 가입에 필요한 항목을 입력하는 부분이다. <form> 태그에 정의된 속성을 보면, 방식은 post이며(method="post") 해당 액션은 '/SIGN_UP'임(action="/SIGN_UP")을 알 수 있다. 앞서 "app.js" 파일의 app.post('/SIGN_UP', function(req,res,next)와 연동되어 있음을 알 수 있다. 특히 req.body.xxxx 라는 데이터가 보일 것인데 xxxx는 sign_up.ejs에 정의된 <input> 태그의 name 속성에 해당하는 것이다. 예를 들면, "app.js"에서 req.body.username은 "sign_up.ejs"의 <input> 태그의 name="username" 속성이다.


    [part 3]

    <script> function checkUserName() { var username = $('#user').val(); if(username == '') { alert("Please enter Username"); } else { $.get('/checkusername?id=' + username, function(data,err) { if(data == 'true') { $('#user').val(''); alert('Username already exists. Please enter another Username'); } else { alert('You can use this Username'); } }); } }; </script>

    Check User Button 클릭 시 실행되는 스크립트인데, AJAX 형식으로 요청($.get('/checkusername?id=' + username, function(data,err))하여 Username이 중복되는지 확인하는 것이다. 즉, " get" 방식으로 '/checkusername'을 요청하면, "app.js"에서 app.get('/CHECKUSERNAME',routes.checkusername)이 실행되고, "index.js"의 exports.checkusername = function(req, res)로 라우팅 되어 처리된다.



    맺음말


    지금까지 Node.js 서버 스크립팅과 MongoDB를 활용하여 회원 관리 페이지를 만들어 보았다. 처음 접하는 분들에게는 꽤나 복잡하게 얽히고 섥힌 것처럼 보일 수 있지만 몇 번 연습해보고 논리 흐름을 잘 이해하면 많은 프로젝트에 적용할 수 있는 유용한 자산이 되리라 믿는다.

    완벽한 기능을 모두 구현하기도 전에 이 글을 쓰게 된 점은 매우 아쉽지만, 곧 기능을 완벽에 가깝게 마무리하여 다시 포스팅 할 수 있도록 하겠다.

     한편으로는, 본 프로젝트에 대한 전반적인 이해를 하였다면 본인 스스로가 기능을 추가할 수 있으리라고도 생각된다.

    많은 도움이 되셨기를...

    "아는 만큼 공유하고 베풀자"  -2013.3.13 Geol Choi

    'Data Science > MongoDB' 카테고리의 다른 글

    [MongoDB] Administration / Monitoring  (0) 2014.03.24
    [MongoDB] Administration / Starting MongoDB  (0) 2014.03.24
    [MongoDB] Database References  (0) 2014.03.10
    [MongoDB] GridFS  (0) 2014.03.06
    [MongoDB] Capped Collections  (0) 2014.03.06
    Comments