npm에는 서버를 제작하는 과정에서 겪게 되는 불편을 해소하고 편의 기능을 추가한 웹 서버 프레임워크가 있다. 대표적인 것이 익스프레스
이다.
express와 nodemon을 설치한다.
npm i express
npm i -D nodemon
npm init
명령어를 이용해 package.json 파일을 먼저 만든다.
{
"name": "learn-express",
"version": "1.0.0",
"description": "익스프레스를 배우자",
"main": "app.js",
"scripts": {
"start": "nodemon app",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Doozuu",
"license": "ISC",
}
서버 역할을 할 app.js를 다음과 같이 적어준다.
const express = require("express");
const app = express();
app.set("port", process.env.PORT || 3000);
app.get("/", (req, res) => {
res.send("Hello, Express");
});
app.listen(app.get("port"), () => {
console.log(app.get("port"), "번 포트에서 대기 중");
});
npm start
를 입력하면 다음과 같이 3000번 포트에서 대기 중이라고 나온다.단순한 문자열 대신 HTML로 응답하고 싶다면 res.sendFile 메서드를 사용하면 된다. 단, 파일의 경로를 path 모듈을 사용해서 지정해야 한다.
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"), "번 포트에서 대기 중");
});
중간
에 위치하기 때문에 미들웨어라고 부른다. 익스프레스 서버에 미들웨어 연결해보기
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"), "번 포트에서 대기 중");
});
미들웨어를 통해 요청과 응답에 다양한 기능을 추가할 수 있고, 이미 많은 사람이 유용한 기능들을 패키지로 만들어뒀다.
dotenv를 제외한 다른 패키지는 미들웨어이다. dotenv는 process.env를 관리하기 위해 설치한다.
npm i morgan cookie-parser express-session dotenv
.env
COOKIE_SECRET=cookiesecret
app.js
const express = require("express");
const morgan = require("morgan");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const dotenv = require("dotenv");
const path = require("path");
dotenv.config();
const app = express();
app.set("port", process.env.PORT || 3000);
app.use(morgan("dev"));
app.use("/", express.static(path.join(__dirname, "public")));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(
session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
name: "session-cookie",
})
);
const multer = require("multer");
const fs = require("fs");
try {
fs.readdirSync("uploads");
} catch (error) {
console.error("uploads 폴더가 없어 uploads 폴더를 생성합니다.");
fs.mkdirSync("uploads");
}
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 },
});
app.get("/upload", (req, res) => {
res.sendFile(path.join(__dirname, "multipart.html"));
});
app.post("/upload", upload.single("image"), (req, res) => {
console.log(req.file);
res.send("ok");
});
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"), "번 포트에서 대기 중");
});
요청과 응답에 대한 정보를 콘솔에 기록한다.
app.use(morgan('dev'))
인수로 dev 외에 combined, common, short, tiny 등을 넣을 수 있다.
dev 모드 기준으로 GET / 500 7.409ms - 50
은 각각 HTTP 메서드, 주소, HTTP 상태 코드, 응답 속도, - 응답 바이트를 의미한다.
요청과 응답을 한눈에 볼 수 있어 편리하다.
정적인 파일들을 제공하는 라우터 역할을 한다.
기본적으로 제공되기에 따로 설치할 필요 없이 express 객체 안에서 꺼내 장착하면 된다.
app.use('요청 경로', express.static('실제 경로'));
함수의 인수로 정적 파일들이 담겨 있는 폴더를 지정하면 된다.
정적 파일들을 알아서 제공해주므로 fs.readFile로 파일을 직접 읽어서 전송할 필요가 없다.
요청의 본문에 있는 데이터를 해석해서 req.body 객체로 만들어주는 미들웨어이다.
보통 폼 데이터나 AJAX 요청의 데이터를 처리한다.
폼 전송은 URL-encoded 방식을 주로 사용한다.
단, 이미지, 동영상, 파일 등의 데이터는 처리하지 못한다. 이 경우에는 뒤에 나오는 multer 모듈을 사용하면 된다.
app.use(express.json()); // json 형식의 데이터 전달 방식
app.use(express.urlencoded({extended : false})); // 주소 형식으로 데이터 전달
버퍼나 텍스트 요청을 처리할 필요가 있다면 body-parser를 설치한 후 다음과 같이 추가한다.
npm i body-parser
const bodyParser = require('body-parser');
app.use(bodyParser.raw());
app.use(bodyParser.text());
요청에 동봉된 쿠키를 해석해 req.cookie 객체로 만든다.
app.use(cookieParser(비밀 키));
해석된 쿠키들은 req.cookies 객체에 들어간다.
예를 들어 name=Doozuu 쿠키를 보냈다면 req.cookies는 {name:'Doozuu'}가 된다.
첫 번째 인수로 비밀 키를 넣어줄 수 있다.
서명된 쿠키가 있는 경우, 제공한 비밀 키를 통해 해당 쿠키가 내 서버가 만든 쿠키임을 검증할 수 있다. 서명이 붙으면 쿠키가 name=Doozuu.sign과 같은 모양이 된다. 서명된 쿠키는 req.cookies 대신 req.signedCookies 객체에 들어간다.
세션 관리용 미들웨어이다. 로그인 등의 이유로 세션을 구현하거나 특정 사용자를 위한 데이터를 임시적으로 저장해둘 때 매우 유용하다. 세션은 사용자별로 req.session 객체 안에 유지된다.
app.use(session({
resave : false,
saveUninitialized : false,
secret : process.env.COOKIE_SECRET,
cookie : {
httpOnly : true,
secure : false,
}
name : 'session-cookie',
}));
인수로 세션에 대한 설정을 받는다. resave는 요청이 올 때 세션에 수정사항이 생기지 않아도 세션을 다시 저장할지 설정하는 것이고, saveUninitialized는 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정하는 것이다.
express-session은 세션 관리 시 클라이언트에 쿠키를 보낸다. 안전하게 쿠키를 전송하려면 쿠키에 서명을 추가해야 하고, 쿠키를 서명하는 데 secret의 값이 필요하다. cookie-parser의 secret과 같게 설정하는 것이 좋다. 세션 쿠키의 이름은 name 옵션으로 설정한다.
cookie 옵션은 세션 쿠키에 대한 설정이다.
이미지, 동영상 등을 비롯한 여러 가지 파일을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어이다.
멀티파트 형식이란 다음과 같이 enctype이 multipart/form-data인 폼을 통해 업로드 하는 데이터의 형식을 의미한다.
<form id="form" action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="image1" />
<input type="file" name="image2" />
<input type="text" name="title" />
<button type="submit">업로드</button>
</form>
익스프레스를 사용하는 이유 중 하나는 바로 라우팅을 깔끔하게 관리할 수 있다는 점이다.
app.js에서 app.get 같은 메서드가 라우터 부분이다. 라우터를 많이 연결하면 코드가 매우 길어지므로 익스프레스에서는 라우터를 분리할 수 있는 방법을 제공한다.
routes 폴더를 만들고 그 안에 index.js 와 user.js 를 작성한다.
index.js
const express = require('express');
const router = express.Router();
// GET / 라우터
router.get('/', (req, res) => {
res.send('Hello, Express');
});
module.exports = router;
user.js
const express = require('express');
const router = express.Router();
// GET /user 라우터
router.get('/', (req, res) => {
res.send('Hello, User');
});
module.exports = router;
app.js
index.js와 user.js를 app.use를 통해 app.js에 연결.
에러 처리 미들웨어 위에 404 상태 코드를 응답하는 미들웨어를 하나 추가.
const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
const path = require('path');
dotenv.config();
const indexRouter = require('./routes'); // index.js 가져오기
const userRouter = require('./routes/user'); // user.js 가져오기
const app = express();
app.set('port', process.env.PORT || 3000);
app.use(morgan('dev'));
app.use('/', express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
name: 'session-cookie',
}));
app.use('/', indexRouter); // /로 접속하면 /routes에 있는 내용을 볼 수 있다.
app.use('/user', userRouter); // /user로 접속하면 /routes/user에 있는 내용을 볼 수 있다.
app.use((req, res, next) => {
res.status(404).send('Not Found');
});
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()를 호출하면 다음 미들웨어가 실행된다.- 라우터 주소에는 정규표현식을 비롯한 특수한 패턴을 사용할 수 있다. 자주 쓰이는 패턴으로
라우트 매개변수
라는 패턴이 있다.router.get('/user/:id', (req,res) => { console.log(req.params, req.query); })
:id
부분에는 다른 값을 넣을 수 있다. /user/1이나 /user/123 등의 요청도 이 라우터가 처리하게 된다. 이 방식의 장점은 :id에 해당하는 1이나 123을 조회할 수 있다는 점이며, req.params 객체 안에 들어 있다.req.params.id
로 조회할 수 있다.- 단, 이 패턴을 사용할 때 주의할 점은 일반 라우터보다 뒤에 위치해야 한다는 것이다. 다양한 라우터를 아우르는 와일드카드 역할을 하므로 일반 라우터보다 뒤에 위치해야 다른 라우터를 방해하지 않는다.
- 주소에 쿼리스트링을 쓸 때도 있다. 쿼리스트링의 키-값 정보는 req.query 객체 안에 들어 있다.
- 라우터에서 자주 쓰이는 활용법으로 app.route나 router.route가 있다.
- 다음과 같이 주소는 같지만 다른 코드가 있을 때 이를 하나의 덩어리로 줄일 수 있다.
익스프레스의 req, res 객체는 http 모듈의 req, res 객체를 확장한 것이다.
기존 http 모듈의 메서드도 사용할 수 있고, 익스프레스가 추가한 메서드나 속성을 사용할 수도 있다.
다만, 익스프레스의 메서드가 워낙 편리하므로 기존 http 모듈의 메서드는 잘 쓰이지 않는다.
자주 쓰이는 메서드
- req 메서드
- res 메서드
템플릿 엔진은 자바스크립트를 사용해서 HTML을 렌더링할 수 있게 한다.
대표적인 템플릿 엔진으로 Pug
와 Nunjucks
가 있다.
(요즘에는 템플릿 엔진 대신 프론트엔드에서 리액트나 뷰를 더 많이 사용하는 추세이다.)
예전 이름인 제이드로 더 유명한 퍼그는 꾸준한 인기를 얻고 있다.
문법이 간단해서 코드양이 줄어들기 때문이다.
루비를 사용해봤다면 문법이 비슷해서 빠르게 적응할 수 있다.
단, HTML과는 문법이 많이 달라서 호불호가 갈린다.
설치
npm i pug
app.js에 아래 코드 추가
app.set('view engine', pug)
넌적스는 퍼그의 HTML 문법 변화에 적응하기 힘든 분에게 유용한 템플릿이며, 파이어폭스를 개발한 모질라에서 만들었다.
HTML 문법을 그대로 사용하되 추가로 자바스크립트 문법을 사용할 수 있다.
설치
npm i nunjucks
app.js
사용법
변수 : 변수를 {{}}로 감싼다.
반복문 : 넌적스에서는 특수한 문을 {% %} 안에 쓴다.
조건문 : {% if 변수 %}, {% elif %}, {% else %}
include : 다른 HTML 파일을 넣을 수 있다.