본문 바로가기
전공 수업/웹 서버 프로그래밍(Node.js)

[12주 차] - Express.js 설치, Express 구조, Middle-ware, Router 객체로 Routing 분리

by TwoJun 2023. 5. 27.

    과목명 : 웹 서버 프로그래밍(Web Server-side programming with Node.js)

수업일자 : 2023년 05월 18일 (목)

Node.js & Express.js

 

 

 

 

 

1. Express.js & Express generator

(1) http 모듈로 웹 서버를 구성하고자 하면 코드가 길어지고 확장성이 감소하는 특징을 가지게 된다.

- 이러한 문제를 Node.js 기반의 웹 서버를 구성할 수 있는 프레임워크인 Express.js로 해결할 수 있습니다.

 

- Node.js 기반의 서버 사이드 프레임워크로는 대표적으로 Express.js가 존재하고 Koa, Hapi 등이 존재합니다.

 

- Express.js의 구조를 빠르게 갖출 수 있는 패키지가 express-generator입니다.

Command : npm install express-generator 또는 npm i -g express-generator

 

 

 

 

1-1. generator 사용하기

(1) express 프로젝트명 --view=pug

- 템플릿 엔진(Template engine)을 pug로 사용합니다.

Command : express learn-express --view=pug

 

(2) learn-express 디렉토리에 관련 모듈들이 설치된 것을 확인할 수 있습니다.

Command : cd learn-express, ls

 

(3) learn-express 디렉토리에서 npm i 또는 npm install 커맨드로 package.json에 기록된 패키지를 모두 설치합니다.

Command : npm i / 관련 모듈을 설치하고 있는 Powershell
모듈 설치가 완료된 것을 확인할 수 있다.

 

 

 

 

1-2. learn-express : Express 초기 디렉토리 구조

learn-express 디렉토리의 초기 구조

 

(1) app.js

- Express의 본체 역할을 담당하는 스크립트 파일로써, 핵심적인 서버 스크립트 역할을 수행하고 여러 가지 미들웨어를 관리할 수 있습니다.

 

(2) bin/www

- 서버를 실행할 수 있는 스크립트 파일입니다.

 

(3) public 디렉토리

- 외부에서 접근 가능한 파일들을 모아두는 디렉토리입니다.

 

(4) views

- 템플릿 엔진을 기반으로 하는 템플릿 파일들을 모아두는 디렉토리입니다.

 

(5) routes

- 서버의 라우터와 관련 로직 파일들을 모아두는 디렉토리입니다. (추후에 models를 생성해 데이터베이스에 사용함.)

 

 

 

 

1-3. Express 서버 실행, 서버 콘솔 확인

(1) package.json 파일에서 서버를 실행하는 scripts 명령어를 확인합니다.

{
  "name": "learn-express",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "express": "~4.16.1",
    "http-errors": "~1.6.3",
    "morgan": "~1.9.1",
    "pug": "2.0.0-beta11"
  }
}

 

(2) 서버 실행 : npm start

- www 파일을 보면 서버의 시작 포트가 3000번으로 지정되어 있습니다.

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

 

Command : npm start

 

(3) localhost:3000으로 접속해 서버의 실행 결과를 확인할 수 있습니다.

localhost:3000으로 접속해서 서버가 정상적으로 실행된 것을 확인할 수 있다.

 

(4) localhost:3000/users로 이동하면 아래와 같은 화면을 확인할 수 있고 서버 스크립트를 실행한 powershell 환경의 console log를 확인해 보면 아래와 같은 내용이 표시됩니다.

localhost:3000/users
클라이언트가 보낸 요청 로그가 서버 스크립트 콘솔에 기록된 것을 확인할 수 있다.

- GET /users 200 1.133 ms - 23 해당 로그는 서버 컴퓨터 사양에 따라 모두 다를 수 있습니다.  

- HTTP 요청 주소 / 상태 코드, 응답 속도, 응답 Byte

 

 

 

 

 

 

2. Express 구조 이해하기

2-1. bin/www

(1) 해당 파일은 HTTP 모듈에 Express 모듈을 연결하고 포트를 지정할 수 있는 파일입니다.

 

(2) 콘솔 명령어를 만들기 위해 #!/usr/bin/env node 부분은 주석 처리되어 있습니다.

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('learn-express:server');
var http = require('http');

 

 

(3) bin/www의 핵심 코드

var app = require('../app');
var debug = require('debug')('learn-express:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

(4) app, debug, http 모듈을 가져옵니다.

- debug 모듈의 경우 콘솔에 로그를 남길 수 있는 모듈입니다.

 

 

(5) app.set('port', port)

- 해당 부분의 코드가 서버가 실행될 포트를 결정합니다.

 

- process.env 객체에 포트 속성이 존재한다면 해당 값을 사용하게 되고 없다면 Default 값으로 3000번 포트를 사용할 수 있도록 설계되어 있습니다.

 

 

(6) http.createServer()에서 불러온 app 모듈을 넣습니다.

- app 모듈이 createServer() 메소드의 콜백 함수 역할을 수행합니다.

 

 

(7) listen을 하는 부분은 HTTP 기반 웹 서버와 동일합니다.

 

 

 

 

2-2. app.js

Express의 구조

 

(1) 서버 사이드의 핵심적인 역할을 수행하는 파일입니다.

- express 패키지를 호출하여 app 객체를 생성합니다.

 

- app.set() 메소드로 Express의 옵션을 설정할 수 있습니다.

 

- app.use() 메소드로 Middle-ware(미들웨어)에 연결합니다.

 

- 마지막에 app 객체를 모듈로 만듭니다.

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

 

 

 

 

 

 

3. Middle-ware

3-1. Express는 Middle-ware로 구성

Middle-ware의 요청 흐름

(1) app.use() 메소드를 통해 동작합니다.

 

(2) logger, json, urlencoded, cookieParser, static Middle-ware가 존재합니다.

 

(3) next() 메소드를 통해 다음 Middle-ware로 넘어갈 수 있습니다.

 

(4) Router와 Error-handler도 Middle-ware입니다.

 

(5) 아래 코드는 Middle-ware의 핵심 코드입니다.

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

 

 

 

 

3-2. Custom Middle-ware 생성하기

(1) 직접 사용자 지정 Middle-ware를 생성할 수 있습니다.

- app.use(logger('dev')); 코드 위에 작성하고 localhost:3000번으로 접속합니다.

//...
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(function(req, res, next) {
    console.log(req.url, 'middle-ware');
    next();
});
app.use(logger('dev'));
//...

 

(2) next() 메소드를 호출해야 다음 코드 로직으로 이동할 수 있습니다.

- next() 메소드를 주석 처리하는 경우 응답이 전송되지 않습니다.

 

- 다음 Middle-ware(Router middle-ware)로 넘어가지 않기 때문입니다.

 

- next() 메소드에 전달인자로 값을 넣게 되면 Error-handler로 이동합니다.

 

- 아래는 next() 메소드의 동작 방식입니다.

 

next() 메소드의 동작 방식

 

 

 

 

3-3. Error handling(오류 처리) Middle-ware

(1) Middle-ware 마지막 부분에 Error handling middle-ware를 두어 전체적인 오류 사항을 처리합니다.

- express-generator는 404 처리 Middle-ware와 Error-handler를 자동으로 생성합니다.

 

- 여기서 404 오류란, 찾고자 하는 페이지(또는 요청 주소) 또는 라우터로 등록되지 않은 주소로 요청이 들어올 때 발생할 수 있습니다.

 

- Error handler는 서버 사이드에서 발생한 오류를 처리합니다.

// 404 처리 middle-ware
app.use(function(req, res, next) {
    next(createError(404));
});
// Error handler
app.use((function(req, res, next) {
    res.locals.message = err.message;
    res.locals.error = req.app.get('env') === 'development' ? err: {};
    
    res.status(err.status || 500);
    res.render('error');
});

 

 

 

 

3-4. app.use() 메소드 응용

(1) app.use() 메소드에 여러 개의 Middle-ware를 설정할 수 있습니다.

app.use('/', function(req, res, next) {
    console.log('첫 번째 middle-ware');
    next();
}, function(req, res, next) {
    console.log('두 번째 middle-ware');
    next();
}, function(req, res, next) {
    console.log('세 번째 middle-ware');
    next();
});

 

(2) Express middle-ware들도 다음과 같은 코드로 축약할 수 있습니다.

- 축약 전 코드

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

 

- 축약 후 코드

app.use(logger('dev'), express.json(), express.urlencoded({ extended: false}),
cookieParser(), express.static(path.join(__dirname, 'public')));

 

 

 

 

3-5. 50% 확률로 접속에 실패하는 Middle-ware

(1) next() 메소드를 호출하지 않으면 다음 Middle-ware로 넘어가지 않는다는 메소드의 성질을 이용합니다.

 

(2) res.status: HTTP 상태 코드를 부여합니다.

- 404 코드의 경우 Not Found(요청 페이지가 존재하지 않을 때)

 

(3) res.send: HTTP 본문에 대한 응답입니다.

- res.end() 메소드의 개량된 버전입니다.(Express 전용 메소드)

app.use(function(req, res, next) {
    if (+new Date() % 2 === 0) {
        return res.status(404).send('50% 실패');
    } else {
        next();
    }
}, function(req, res, next) {
    console.log('50% 성공');
    next();
});

 

 

 

 

3-6. morgan

(1) 서버로 들어온 요청과 응답을 기록해 주는 Middle-ware입니다.

 

(2) logger 함수의 인자로 'dev' 대신 short, common, combined등을 줄 수 있습니다.

- 로그의 자세한 정도를 선택할 수 있습니다.

 

- GET / 200 51.267 ms - 1539

- 순서대로 HTTP 요청 주소, 상태 코드, 응답 속도, 응답 바이트입니다.

// ...
var logger = require('common');
// ...
app.use(logger('dev'));
/...

 

 

 

 

3-7. body-parser

(1) 요청에 대한 본문을 해석할 수 있는 Middle-ware입니다.

(1) Form data나 AJAX 요청의 데이터를 처리합니다.

 

(2) JSON Middle-ware는 요청 본문이 JSON인 경우 해석하고, urlencoded Middle-ware는 Form 요청을 해석합니다.

// ...
var bodyParser = reqiure('body-parser');
// ...
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
// ...

 

(3) body-parser가 Express에 내장되어 다음과 같이 사용할 수 있습니다.

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

 

(4) Multipart 데이터(이미지, 동영상 등)인 경우는 다른 multer라는 middle-ware를 사용해야 합니다.

 

 

(5) 본문이 버퍼 또는 Text 데이터일 때

app.use(bodyParser.raw());
app.use(bodyParser.text());

 

(7) urlencoded({ extended: false })

- false  값의 경우 노드의 Query-string 모듈을 사용하여 Query-String을 해석합니다.

 

-  true 값의 경우 Qs 모듈을 사용하여 Query-string을 해석하고 qs는 npm 패키지, querystring 모듈의 확장 버전입니다.

 

 

(8) Body-parser 패키지가 본문을 해석한 후 req.body() 부분에 추가합니다.

- JSON 형식 : { name: 'wonjun', job: 'student' }와 같은 형식으로 Body-parser에 그대로 들어가게 됩니다.

 

- URL-encoded 형식 : Name=wonjun&job=student 형식의 데이터로 JSON 형식으로 변환되어 들어가게 됩니다.

 

 

 

 

3-8. cookie-parser

(1) cookie-parser의 경우 요청 헤더의 쿠키 데이터를 해석하는 Middle-ware입니다.

// ...
var cookieParser = require('cookie-parsser');
// ...
app.use(cookieParser());
// ...

 

(2) 해석된 쿠키 데이터들은 req.cookies 객체에 삽입됩니다.

- Ex) name=wonjun의 쿠키를 보냈다면 req.cookies = { name: 'wonjun' } 형식으로 객체에 삽입됩니다.

 

 

(3) 쿠키를 암호화하기 위해선 인수로 암호화 키를 삽입합니다.

app.use(cookieParser('secret code'));

 

 

 

 

3-9. static

(1) 정적인 파일들을 제공할 수 있는 Middle-ware입니다.

// ...
app.use(express.static(path.join(__dirname, 'public')));
// ...

 

(2) public/stylesheets/style.css 파일은 http://localhost:3000/stylesheets/style.css로 접근 가능합니다.

 

(3) 요청 주소에 public이 존재하지 않습니다.

 

(4) 컨텐츠 요청 주소와 실제 컨텐츠의 주소를 다르게 설정할 수 있습니다

- 요청 주소 : /img

- 컨텐츠 주소 : /public 

- 이러한 형식으로 작성하게 되면 서버의 구조를 파악하기 어려워지면서 보안성이 강화될 수 있습니다.

app.use('/img', express.static(path.join(__dirname, 'public')));

 

(5) public 디렉토리 내부에 abc.png 파일이 존재한다면 http://localhost:3000/img/abc.png 주소로 접근할 수 있습니다.

 

 

 

 

3-10. Middle-ware 효율적으로 배치하기

(1) static Middle-ware는 파일을 발견하면 next() 메소드를 실행하지 않습니다.

- 요청에 해당하는 파일을 찾으면 응답으로 해당 파일을 전송하고 자체적으로 라우팅 기능을 수행하게 됩니다. 그렇지 않으면 요청을 라우터로 넘기게 됩니다.

 

(2) static Middle-ware가 파일을 찾으면 logger, static 모듈만 호출하게 되며 json, urlencoded, cookieParser 모듈은 호출되지 않습니다.

// ...
app.use(logger('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.join());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
//...

 

 

 

 

3-11. Express-session 

(1) learn-express 디렉토리로 이동한 후 npm install express-session 또는 npm i express-session 커맨드로 express-session모듈을 설치합니다.

Command : npm install express-session

learn-express 디렉토리 내부의 node_modules 디렉토리로 이동해서 express-session 디렉토리가 생성된 것을 확인할 수 있습니다.

Command : ls

 

(2) Session 관리용 Middle-ware

// ...
var logger = require('morgan');
var session = require('express-session');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

app.use(cookieParser('secret code'));
app.use(session({
    resave: false,
    saveUninitialized: false,
    secret: 'secret code',
    cookie: {
        httpOnly: true,
        secure: false,
    },
}));

(1) express-generator에 내장되어 있지 않은 Middle-ware로써, 별도 설치가 필요합니다.

 

(2) Session 쿠키에 대한 설정(secret: 쿠키 암호화, cookie: 세션 쿠키 옵션)

 

 

(3) resave 

- 요청이 들어왔을 때 세션에 수정 사항이 발생하지 않아도 다시 저장할지에 대한 여부입니다.

 

- 만약 옵션값이 true인 경우, 매 요청마다 세션을 저장하게 됩니다.

 

 

(4) saveUninitialized 

- 세션에 저장할 내역이 없더라도 세션을 저장할지 결정합니다.

 

- 만약 옵션값이 true인 경우, 별도의 내용이 없는 세션을 계속 저장하게 됩니다.

 

 

(5) httpOnly 

- 클라이언트에서 쿠키를 확인하지 못하도록 설정하려면 true값을 주어야 합니다.

 

 

(6) secure

- HTTPS 환경에서 사용하면 true 값을 가지게 됩니다.

 

 

 

 

3-12. connect-flash

(1) 일회성 메시지를 표시하는 표시용 Middle-ware입니다.

- 커맨드 라인에서 npm i connect-flash 명령으로 별도의 설치가 필요합니다.

Command : npm i connect-flash

 

(2) cookie-parser, express-session보다 하위에 위치하는 모듈로 상호 간 의존 관계가 존재합니다.

 

(3) routes/users.js 파일을 다음과 같이 수정합니다.

const express = require('express');

const router = express.Router();

// GET /user 라우터
router.get('/', (req, res) => {
  res.send('Hello, User');
});

router.get('/flash', function(req, res) {
  req.session.message = 'session message';
  req.flash('message', 'flash message');
  res.redirect('/users/flash/result');
});

router.get('/flash/result', function(req, res) {
  res.send(`${req.session.message} ${req.flash('message')}`);
});

module.exports = router;

 

(4) localhost:3000/users/flash 페이지로 접속하면  localhost:3000/users/flash/result로 리다이렉트 되면서 Flash message가 삽입됩니다. 이후에 새로고침 시엔 Flash mesage가 표시되지 않습니다.

 

 

 

 

 

 

4. Router 객체로 Routing 분리하기

4-1. 라우터(Router)

(1) app.use() 메소드로 Router 모듈과 연결할 수 있고 Router도 일종의 Middle-ware입니다.

// ...
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
// ...
app.use('/', indexRouter);
app.use('/users', usersRouter);
// ...

 

(2) app.use() 메소드 대신 get, post, put, patch, delete와 같은 메소드를 사용할 수 있습니다.

app.use('/', function(req, res) {
  console.log('/ 주소의 요청일 때 실행됩니다. HTTP 메소드와는 연관이 없습니다.');
  next();
});

app.get('/', function(req, res) {
  console.log('GET 메소드 / 주소의 요청일 때만 실행됩니다.');
  next();
});

app.post('/data', function(req, res) {
  console.log('POST 메소드 /data 주소의 요청일 때만 실행됩니다.');
  next();
});

 

 

 

 

4-2. Router 파일 

(1) express.Router() 메소드로 Router 객체를 생성합니다.

 

(2) module.exports로 Router 모듈을 생성할 수 있습니다.

- routes/index.js

const express = require('express');
const router = express.Router();

// GET / 라우터
router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

module.exports = router;

 

- routes/user.js

const express = require('express');
const router = express.Router();

// GET /user 라우터
router.get('/', function(req, res, next) {
  res.send('respond with a resource');

module.exports = router;

 

 

 

 

4-3. Router 활용하기

(1) Router도 Middle-ware이므로 여러 개의 Middle-ware를 연결할 수 있습니다. 

router.get('/', middleware1, middleware2, middleware3);

 

(2) next('route') 메소드로 다음 Router로 건너뛸 수 있습니다.

router.get('/', function(req, res, next) {
  next('route');
}, function(req, res, next) {
  console.log('실행되지 않습니다.');
  next();
}, function(req, res, next) {
  console.log('실행되지 않습니다.');
  next();
});

router.get('/', function(req, res) {
  console.log('실행됩니다.');
  res.render('index', { title: 'Express' });
});

 

 

 

 

4-4. 주소 와일드카드(Address Wildcard)

(1) 주소의 일부분이 수정되는 경우 [:속성]으로 지정할 수 있습니다.

router.get('/users/:id', function(req, res) {
  console.log(req.params, req.query);
});

 

(2) req.params 부분에 속성 값이 저장됩니다.

Ex) users/123 req.params.id = '123'

 

(3) Query-string의 경우 req.query 부분에 저장됩니다.

- /users/123?limit=5&skip=10

 

- 콘솔에서 확인되는 출력값  : { id: '123' }(req.params 객체) { limit: '5', skip: '10' }(req.query 객체)

 

- http://localhost:3000/users/123?limit=5&skip=10

 

 

 

 

4-5. Express 응답 메소드

(1) Express에 메소드를 추가합니다.

- res.send(버퍼 또는 문자열) 또는 HTML 또는 JSON 형식 : 기본 응답 메소드

 

- res.sendFile(파일 경로) : 파일 전송

 

- res.json(JSON 데이터) : JSON 형식의 데이터 응답

 

- res.redirect(주소) : 페이지 Redirection

 

- res.render('템플릿 파일 경로', { 변수 }) : 템플릿 엔진 렌더링

 

 

(2) 추가적으로 하나의 요청에 대한 응답은 한 번만 보내야 하며 두 번 이상 보내면 오류가 발생하게 됩니다.

 

 

 

 

 

 

5. Reference

Node.js 교과서(Node.js Textbook) - 저자 : 조현영 

https://www.zerocho.com/book/1

 

ZeroCho Blog

ZeroCho의 Javascript와 Node.js 그리고 Web 이야기

www.zerocho.com

 

- 학부에서 수강했던 전공 수업 내용을 정리하는 포스팅입니다.

- 내용 중에서 오타 또는 잘못된 내용이 있을 시 댓글로 남겨주시면 감사하겠습니다!

댓글