6.1 익스프레스 프로젝트 시작하기
- package.json 생성
제일 먼저 package.json을 생성한다.
npm init 명령어를 콘솔에서 호출해 단계적으로 내용물을 입력해도 되고, npm init -y를 입력해 파일을 만든 뒤 내용을 수정해도 된다.
// package.json
{
"name": "learn-express",
"version": "0.0.1",
"description": "최하호의 익스프레스 성장기",
"main": "app.js",
"scripts": {
"start": "nodemon app"
},
"author": "haho",
"license": "MIT",
"dependencies": {
"express": "^4.18.2"
}
}
script 부분에 start 속성을 넣으면 nodemon app을 하면 app.js를 nodemon으로 실행한다는 뜻이다.
서버 코드에 수정 사항이 생길 때마다 매번 서버를 재시작하기는 귀찮으므로 nodemon 모듈로 서버를 자동으로 재시작한다.
- Express 설치하기
npm i express 명령을 통해 Express를 설치한다.
nodemon을 사용한다면 npm i -D nodemon을 통해 nodemon을 설치한다.
const express = require('express');
const app = express();
app.set('port', process.env.PORT || 3000);
app.get('/', (req, res) => {
res.send('Hello Choi ha ho');
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
Express 모듈을 실행해 app 변수에 할당한다. 익스프레스 내부 http 모듈이 내장되어 있으므로 서버의 역할을 할 수 있다.
app.set('port' 포트)로 서버가 실행될 포트를 설정한다.
app.get(주소, 라우터)는 주소에 대한 GET 요청이 올 때 어떤 동작을 할지 적는 부분이다.
- 서버 실행하기
npm start를 통해 서버를 실행한다.
[HTML로 응답]
res.send 대신 res.File을 사용하고 파일의 경로를 paht 모듈을 사용해서 지정한다.
// index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="styleheet" href="./test.css">
<title>Gun Gong pot</title>
</head>
<body>
<div class="two">
<h1>Choi Ha Ho
<span>중요한 것은 꺾이지 않는 마음</span>
</h1>
</div>
</body>
</html>
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '/index.html'))
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
6.2 자주 사용하는 미들웨어
[ middleware ]
요청과 응답의 중간에 위치하기 때문에 미들웨어라고 부른다.
미들웨어는 app.use와 함께 사용된다.
HTTP 요청이 들어온 순간부터 순차적으로 실행된다.
HTTP 요청과 응답 객체를 처리하거나, 다음 미들웨어를 실행할 수 있다.
예시 1.
const express = require('express');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use((req, res, next) => {
console.log('모든 요청에 다 실행됩니다.');
next();
});
app.get('/', (req, res, next) => {
console.log('GET / 요청에서만 실행됩니다.');
next();
}, (req, res) => {
throw new Error('에러는 에러 처리 미들웨어로 갑니다.');
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).send(err.message);
});
app.listen(app.get('port'), () => {
console.log(app.get('port'), '번 포트에서 대기 중');
});
위 예시는 next라는 세 번째 매개변수를 사용했다.
next를 실행하면 다음 미들웨어로 넘어가고 실행하지 않으면 다음 미들웨어가 실행되지 않는다.
app.use나 app.get 같은 라우터에 미들웨어 여러 개를 장착할 수 있다.
현재 app.get 라우터에 미들웨어 두 개가 연결되어 있다.
에러 처리 미들웨어는 매개변수가 err, req, res, next로 네 개이다.
모든 매개변수를 사용하지 않더라도 매개변수가 반드시 네 개여야 한다.
[dotenv]
dotenv 패키지는 .env 파일을 읽어서 process.env로 만든다.
이름의 뜻은 dot + env이다.
[ morgan ]
app.use(morgan('dev'));
인수로 dev 외에 combined, common, short, tiny 등을 넣을 수 있다.
- dev 모드 기준
GET / 500 7.409 ms - 50
[HTTP 메서드] [주소] [HTTP 상태 코드] [응답 속도] - [응답 바이트]
요청과 응답을 한눈에 볼 수 있어 편리하다.
[ static ]
정적인 파일들을 제공하는 라우터 역할을 한다.
기본적으로 제공되기에 따로 설치할 필요가 없다.
app.use('요청 경로', express.static('실제 경로'));
app.use('/', express.static(path.join(__dirname, 'public')));
함수의 인수로 정적 파일들이 담겨 있는 폴더를 지정하면 된다.
ex) public/stylesheets/style.css는 http://localhost:3000/stylesheets/style.css로 접근할 수 있다.
[ body-parser ]
요청의 본문에 있는 데이터를 해석해서 req.body 객체로 만들어주는 미들웨어이다.
보통 폼 데이터나 AJAX 요청의 데이터를 처리한다. (멀티파트 데이터는 처리 못함)
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
express 4.17.0 버전부터 body-parse 미들웨어의 기능이 express에 내장되어 설치할 필요가 없다.
extended가 false이면 노드의 querystring 모듈을 사용해 쿼리스트링을 해석하고 true이면 qs 모듈을 사용해 쿼리스트링을 해석한다.
ex) JSON 형식으로 { name : 'haho', book: 'nodejs' }를 본문으로 보내면 그대로 req.body에 들어가고, URL-encoded 형식으로 name=haho&book=nodejs를 본문으로 보내면 req.body에 { name: 'haho', book: 'nodejs' }가 들어간다.
[ cookie-parser ]
요청에 동봉된 쿠키를 해석해 req.cookies 객체로 만든다.
app.use(cookieParse(비밀 키));
해석된 쿠키들은 req.cookies 객체에 들어간다.
ex) name=haho 쿠키를 보내면 req.cookies는 { name: 'haho' }가 된다.
첫 번째 인수로 비밀키를 넣을 수 있다.
서명된 쿠키가 있는 경우 제공한 비밀키를 통해 해당 쿠키가 내 서버가 만든 쿠키임을 검증할 수 있다.
서명이 붙으면 쿠키는 name=haho.sing과 같은 모양이 되고 req.signedCookies 객체에 들어있다.
res.cookie('name', 'haho', { // 쿠키 생성
expires: new Date(Date.now() + 9000000),
httpOnly: true,
secure: true,
}};
res.clearCookie('name', 'haho', { httpOnly: true, secure: true }); // 쿠키 제거
[ express-session ]
세션 관리용 미들웨어이다.
로그인 등의 이유로 세션을 구현하거나 특정 사용자를 위한 데이터를 임시적으로 저장해둘 때 유용하다.
세션은 사용자별로 req.session 객체 안에 유지된다.
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
name: 'session-cookie',
}));
인수로 세션에 대한 설정을 받는다.
- resave: 요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정한다.
- saveUninitialized: 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정한다.
- secret: 쿠키를 서명하는데 필요한 값
- cookie: maxAge, domain, path, expires, sameSite, httpOnly, secure 등 일반적인 쿠키 옵션이 제공된다.
- name: 쿠키 이름
req.session.name = 'zerocho'; // 세션 등록
req.sessionID; // 세션 아이디 확인
req.session.destroy(); // 세션 모두 제거
[ 미들웨어 특성 활용하기 ]
미들웨어는 req, res, next를 매개변수로 갖는 함수로써 app.use, app.get, app.post 등으로 장착한다.
특정한 주소의 요청에만 미들웨어가 실행되게 하려면 첫 번째 인수로 주소를 넣으면 된다.
app.use(
morgan('dev'),
express.static('/', path.join(__dirname, 'public')),
express.json(),
express.urlencoded({ extended: false }),
cookieParser(process.env.COOKIE_SECRET),
);
위와 같이 여러 개의 미들웨어를 장착할 수 있으며 다음 미들웨어로 넘어가려면 next 함수를 호출한다.
next를 호출하지 않는 미들웨어는 res.send나 res.sendFile 등의 메서드로 응답을 보내야 한다.
next()에 route라는 문자열을 넣으면 다음 라우터의 미들웨어로 이동하고, 그 외의 인수를 넣으면 에러 처리 미들웨어로 이동한다.
- res.locals
세션을 사용한다면 req.session 객체에 데이터를 넣어도 되지만 세션이 유지되는 동안에만 데이터도 계속 유지된다는 단점이 있다. 만약 요청이 끝날 때까지만 데이터를 유지하고 싶다면 res.locals 객체에 데이터를 넣어두면 된다.
app.use((req, res, next) => {
res.locals.data = '데이터 넣기';
next();
}, (req, res, next) => {
console.log(res.locals.data); // 데이터 받기
next();
});
현재 요청이 처리되는 동아 res.locals 객체을 통해 미들웨어 간에 데이터를 공유할 수 있다.
새로운 요청이 도면 res.locals는 초기화된다.
- app.set과의 차이
app.set은 익스프레스에서 전역적으로 사용되므로 하나의 요청에서만 유지되어야 하는 값을 넣기에는 부적절하다.
res.locals 객체는 하나의 요청 안에서만 유지되므로 res.locals 객체를 통해 요청에 종속되는 데이터를 전달하는 것이 좋다.
- 미들웨어 안에 미들웨어 넣기
기존 미들웨어의 기능을 확장할 수 있기 때문에 유용하다.
app.use(morgan('dev'));
// 또는
app.use((req, res, next) => {
morgan('dev')(req, res, next);
});
[ multer ]
이미지, 동영상 등을 비롯한 여러 가지 파일을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어이다.
멀티파트 형식이란 enctype이 multipart/form-data인 폼을 통해 업로드하는 데이터 형식을 의미한다.
const multer = require('multer');
const upload = multer({
storage: multer.diskStorage({
destination(req, file, done) {
done(null, 'uploads/');
},
filename(req, file, done) {
const ext = path.extname(file.originalname);
done(null, path.basename(file.originalname, ext) + Date.now() + ext);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
- storage: 어디에 어떤 이름으로 저장할지를 넣는다.
- destination, filename: req 매개변수에는 요청에 대한 정보, file 객체에는 업로드한 파일에 대한 정보, done 매개변수는 함수이다.
- req나 file 데이터를 가공해서 done으로 넘기는 형식이다.
- [파일명 + 현재시간.확장자] 파일명으로 업로드하고 있다. 현재시간을 넣는 이유는 파일명이 겹치는 것을 막기 위함이다.
- limits: 업로드에 대한 제한 사항을 설정할 수 있다.
* 파일 하나만 업로드 하는 경우
app.post('/upload', upload.single('image'), (req, res) => {
console.log(req.file, req.body);
res.send('ok');
});
single 미드웨어를 사용한다.
single 미드웨어를 라우터 미들웨어 앞에 넣어두면, multer 설정에 따라 파일 업로드 후 req.file 객체가 생성된다.
* 파일 여러 개 업로드 하는 경우
app.post('/upload', upload.array('many'), (req, res) => {
console.log(req.files, req.body);
res.send('ok');
});
array 미들웨어를 사용한다.
업로드 결과도 req.file 대신 req.files 배열에 들어 있다.
* 파일을 업로드 하지 않고 멀티파트 형식으로 업로드하는 경우
app.post('/upload', upload.none(), (req, res) => {
console.log(req.body);
res.send('ok');
});
none 미들웨어를 사용한다.
파일을 업로드하지 않았으므로 req.body만 존재한다.
6.3 Router 객체로 라우팅 분리하기
express를 사용하는 이유 중 하나는 라우팅을 깔끔하게 관리할 수 있다는 점이다.
라우터를 많이 연결하면 app.js 코드가 매우 길어짐으로 express에서는 라우터를 분리할 수 있는 방법을 제공한다.
- route 폴더를 만들어 안에 각각 라우터 파일을 만든다.
- 만들어진 라우터 파일을 app.js에 연결한다.
// user.js
const express = require('express');
const router = express.Router();
router.get('/user', (req, res) => {
res.send("Hello User");
});
module.exports = router;
// admin.js
const express = require('express');
const router = express.Router();
router.post('/admin', (res, req) => {
res.send('Hello admin');
});
module.exports = router;
// app.js
const express = require('express');
const userRouter = require('./routes/user');
const adminRouter = require('./routes/admin');
const app = express();
app.use('/', userRouter);
app.use('/', adminRouter);
app.listen(8080, () => {
console.log('8080포트에서 서버 실행 중');
})
userRouter를 app.use('/')에 연결하였기 때문에 위와 같이 /user 라우터에 결과가 나오게 된다.
* :id 형식의 라우터
const express = require('express');
const router = express.Router();
router.get('/user:id', (req, res) => {
console.log(req.params, req.query);
});
router.get('/user/what', (req, res) => {
console.log('전혀 실행되지 않는다.');
});
module.exports = router;
주소에 :id와 같은 문자를 사용하여 이 부분에 다른 값을 넣을 수 있게 한다.
ex) /user/1, /user/54243 등 처리 가능
단, 일반 라우터보다 뒤에 위치해야 한다. 위와 같이 /user/what와 같은 라우터는 /user:id 뒤에 있기 때문에 실행되지 않는다.
6.4 req, res 객체 살펴보기
express의 req, res 객체는 http 모듈의 req, res 객체를 확장한 것이다.
[ req ]
- req.app: req 객체를 통해 app 객체에 접근할 수 있다.
- req.body: body-parser 미들웨어가 만드는 요청의 본문을 해석한 객체이다.
- req.cookies: cookie-parser 미들웨어가 만드는 요청의 쿠키를 해석한 객체이다.
- req.ip: 요청의 ip 주소가 담겨 있다.
- req.params: 라우트 매개변수에 대한 정보가 담긴 객체이다.
- req.query: 쿼리스트링에 대한 정보가 담긴 객체이다.
- req.signedCookies: 서명된 쿠키들은 req.cookies 대신 여기에 담겨 있다.
- req.get(헤더 이름): 헤더의 값을 가져오고 싶을 때 사용하는 메서드이다.
[ res ]
- res.app: req.app처럼 res 객체를 통해 app 객체에 접근할 수 있다.
- res.cookie(키, 값, 옵션): 쿠키를 설정하는 메서드이다.
- res.clearCookie(키, 값, 옵션): 쿠키를 제거하는 메서드이다.
- res.end(): 데이터 없이 응답을 보낸다.
- res.json(JSON): JSON 형식의 응답을 보낸다.
- res.locals: 하나의 요청 안에서 미들웨어 간에 데이터를 전달하고 싶을 때 사용하는 객체이다.
- res.redirect(주소): 리다이렉트할 주소와 함께 응답을 보낸다.
- res.render(뷰, 데이터): 다음 절에서 다룰 템플릿 엔진을 렌더링해서 응답할 때 사용하는 메서드이다.
- res.send(데이터): 데이터와 함께 응답을 보낸다. 데이터는 문자열일 수도, HTML일 수도, 버퍼일 수도, 객체나 배열일 -수도 있다.
- res.sendFile(경로): 경로에 위치한 파일을 응답한다.
- res.set(헤더, 값): 응답의 헤더를 설정한다.
- res.status(코드): 응답 시의 HTTP 상태 코드를 지정한다.
[ 메서드 체이닝 ]
연속적인 코드 줄에서 개체의 Method를 반복적으로 호출하는 것을 의미한다.
req나 res 객체의 메서드는 메서드 체이닝을 지원하는 경우가 많다.
메서드 체이닝을 사용하면 코드의 양을 줄일 수 있다.
res
.status(201)
.cookie('test', 'test')
.redirect('/admin');
6.5 템플릿 엔진 사용하기
템플릿 엔진은 자바스크립트를 사용해서 HTML을 렌더링할 수 있게 한다.
따라서 기존 HTML 문법과는 살짝 다를 수 있고, 자바스크립트 문법이 들어 있기도 한다.
[ 퍼그 ]
문법이 간단하여 코드양이 줄어들기 때문에 꾸준히 사용한다.
Ruby와 문법이 비슷하다.
익스프레스와 연결하려면 다음과 같은 코드가 있어야 한다.
...
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.use(morgan('dev'));
...
views는 템플릿 파일들이 위치한 폴더를 지정하는 것이다.
ex) res.render('index')라면 views/index.pug를 렌더링한다.
* 기존 HTML과 다르게 화살표 <>와 닫는 태그가 없다.
탭 또는 스페이스로만 태그의 부모 자식 관계를 규명한다.
* 속성 중 아이디와 클래스가 없는 경우에는 아래와 같이 표현이 가능하다.
div 태그는 생략 가능하다.
* HTML 텍스트는 태그 또는 속성 뒤에 한 칸을 띄고 입력하면 된다.
* 에디터에서 텍스트를 여러 줄 입력하고 싶다면 다음과 같이 |를 넣는다.
* style이나 script 태그로 css 또는 자바스크립트 코드를 작성하고 싶다면 다음과 같이 태그 뒤에 .을 붙인다.
- 변수
자바스크립트 변수에 템플릿을 렌더링할 수 있다.
res.render를 호출할 때 보내는 변수를 퍼그가 처리한다.
* 변수를 텍스트로 사용하고 싶다면 =을 붙인 후 변수를 입력한다.
텍스트 중간에 변수를 넣으려면 #{변수}를 사용한다.
* -를 입력하면 뒤에 자바스크립트 구문을 작성할 수 있다.
* 이스케이프를 원하지 않는다면 =대신 !=을 사용한다.
- 반복문
HTML과 다르게 반복문도 사용할 수 있으며 반복 가능한 변수인 경우에만 해당된다.
* each로 반복문을 돌릴 수 있다.
ul
each alphabet in ['a', 'b', 'c', 'd', 'f']
li= alphabet
* 반복문 사용 시 인덱스도 가져올 수 있다.
ul
each alphabet number in ['a', 'b', 'c', 'd', 'f']
li= (number+1) + '번째 ' + alphabet
- 조건문
* 조건문으로 편리하게 분기 처리할 수 있으며 if, else if, else를 사용할 수 있다.
if isFruit
div 과일입니다.
else
div 채소입니다.
* case 문도 가능하다.
case people
when 'haho'
p 사람입니다.
when 'gunwoo'
p 사람입니다.
when 'apple'
p 과일입니다.
default
p 이건 뭐지?
- include
다른 퍼그나 HTML 파일을 넣을 수 있다.
따로 관리할 수 있어 페이지마다 동일한 HTML을 넣어야 하는 번거로움을 없앤다.
- extends와 block
레이아웃을 정할 수 있으며, 공통되는 레이아웃 부분을 따로 관리할 수 있어 좋다.
레이아웃이 될 파일에는 공통된 마크업을 넣되, 페이지마다 달라지는 부분을 block으로 비워둔다.
block [블록명]
[ 넌적스 ]
퍼그의 HTML 문법 변화에 적응하기 힘든 사람에게 유용한 템플릿 엔진이며, 파이어폭스를 개발한 모질라에서 만들었다.
HTML 문법을 그대로 사용하되 추가로 자바스크립트 문법을 사용할 수 있으며, 파이썬 템플릿 엔진인 Twig와 문법이 상당히 유사하다.
view engine을 넌적스로 교체한다.
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
configure의 첫 번째 인수로 views 폴더에 경로를 넣고, 두 번째 인수로 옵션을 넣는다.
이때 express 속성에 app 객체를 연결한다. watch 옵션이 true이면 HTML파일이 변경될 때 템플릿 엔진을 다시 렌더링한다.
- 변수
res.render 호출 시 보내는 변수를 넌적스가 처리한다.
변수는 {{ }}로 감싼다.
<h1>{{title}}</h1>
<p>Welcome to {{title}}</p>
<button class="{{title}}" type="submit">전송</button>
<input placeholder="{{title}} 연습" />
* 내부에 변수를 사용할 수도 있다. 변수를 선언할 때는 {% set 변수 = '값' %}를 사용한다.
* HTML을 이스케이프하고 싶지 않다면 {{ 변수 | safe }}를 사용한다.
- 반복문
넌적스에서는 특수한 문을 {% %} 안에 쓴다.
<ul>
{% set alphabet = ['a', 'b', 'c', 'd', 'f'] %}
{% for item in alphabet %}
<li>{{ item }}</li>
{% endfor %}
</ul>
* 반복문에서 인덱스를 사용하고 싶다면 loop.index라는 특수한 변수를 사용할 수 있다.
<ul>
{% set alphabet = ['a', 'b', 'c', 'd', 'f'] %}
{% for item in alphabet %}
<li>{{ loop.index }}번째 {{ item }}</li>
{% endfor %}
</ul>
- 조건문
조건문은 {% if 변수 %} {% elif %} {% else %} {% endif %}로 이뤄져 있다.
{% if isFruit %}
<div>과일입니다.</div>
{% else %}
<div>채소입니다.</div>
{% endif %}
* case문은 없지만 elif를 통해 분기 처리할 수 있다.
{% if people === 'haho' %}
<p>사람입니다.</p>
{% elif people === 'gunwoo' %}
<p>사람입니다.</p>
{% elif number === 1 %}
<p>숫자입니다.</p>
{% else %}
<p>무엇도 아닙니다.</p>
{% endif %}
- include
다른 HTML 파일을 넣을 수 있다.
- extends와 block
레이아웃을 정할 수 있으며, 공통되는 레이아웃 부분을 따로 관리할 수 있어 좋다.
레이아웃이 될 파일에는 공통된 마크업을 넣되, 페이지마다 달라지는 부분을 block으로 비워둔다.
block [블록명]
[ 에러 처리 미들웨어 ]
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
404 에러가 발생하면 '요청 받은 메소드, url 라우터가 없습니다'를 표시하고 다음 미들웨어로 넘긴다.
에러 처리 미들 웨어는 error라는 템플릿 파일을 렌더링 한다.
error 객체의 스택 트레이스는 시스템 환경이 production이 아닌 경우에만 표시된다.
스택 트레이스를 통해 서버 폴더 구조를 유추할 수 있으므로 숨겨야한다.
'Programming > NodeJS' 카테고리의 다른 글
Node.js 교과서 [NoSQL] #8 (0) | 2024.02.18 |
---|---|
Node.js 교과서 [MySQL] #7 (1) | 2024.02.17 |
Node.js 교과서 [npm] #5 (0) | 2024.02.10 |
Node.js 교과서 [요청과 응답] #4 (1) | 2024.02.04 |
Node.js 교과서 [이벤트, 예외 처리] #3.4 (0) | 2024.02.04 |