과목명 : 웹 서버 프로그래밍(Web Server-side programming with Node.js)
수업일자 : 2023년 05월 18일 (목)
1. Express.js & Express generator
(1) http 모듈로 웹 서버를 구성하고자 하면 코드가 길어지고 확장성이 감소하는 특징을 가지게 된다.
- 이러한 문제를 Node.js 기반의 웹 서버를 구성할 수 있는 프레임워크인 Express.js로 해결할 수 있습니다.
- Node.js 기반의 서버 사이드 프레임워크로는 대표적으로 Express.js가 존재하고 Koa, Hapi 등이 존재합니다.
- Express.js의 구조를 빠르게 갖출 수 있는 패키지가 express-generator입니다.
1-1. generator 사용하기
(1) express 프로젝트명 --view=pug
- 템플릿 엔진(Template engine)을 pug로 사용합니다.
(2) learn-express 디렉토리에 관련 모듈들이 설치된 것을 확인할 수 있습니다.
(3) learn-express 디렉토리에서 npm i 또는 npm install 커맨드로 package.json에 기록된 패키지를 모두 설치합니다.
1-2. learn-express : 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);
(3) localhost:3000으로 접속해 서버의 실행 결과를 확인할 수 있습니다.
(4) localhost:3000/users로 이동하면 아래와 같은 화면을 확인할 수 있고 서버 스크립트를 실행한 powershell 환경의 console log를 확인해 보면 아래와 같은 내용이 표시됩니다.
- 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
(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로 구성
(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() 메소드의 동작 방식입니다.
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모듈을 설치합니다.
learn-express 디렉토리 내부의 node_modules 디렉토리로 이동해서 express-session 디렉토리가 생성된 것을 확인할 수 있습니다.
(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 명령으로 별도의 설치가 필요합니다.
(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
- 학부에서 수강했던 전공 수업 내용을 정리하는 포스팅입니다.
- 내용 중에서 오타 또는 잘못된 내용이 있을 시 댓글로 남겨주시면 감사하겠습니다!
댓글