이번 글은 제가 현재 만들어 보고 있는 Node.js와 Express, Socket.io를 활용한 채팅 서비스를 개발하면서 Express에 대한 이해가 오래 걸렸기에 그것을 정리해보고자 작성하였습니다.
Express를 이용하여 프로젝트를 진행하는 데에는 2가지 방법이 있습니다.
-
express-generator 사용하기
-
직접 프로젝트 구조 잡기
위의 방법들을 설명하고, express 프로젝트의 주요 개념과 필요 패키지들에 대해서 작성하겠습니다.
# express-generator 사용하기
첫 번째 방법은, express-generator를 사용하는 방법입니다.
npm i express-generator
express-generator는 express 프로젝트를 만드는데 필요한 기초 구조를 잡아주는 패키지입니다. 프로젝트를 시작할 때는 다음과 같이 입력합니다.
express [project_name] --view=pug
express라는 명령어 다음에 프로젝트의 이름을 적어주고, 끝에는 템플릿 엔진에 대한 옵션으로 pug를 주었습니다.
express-generator는 기본적으로 Jade라는 템플릿 엔진을 설치하는데, 이 Jade의 최신 버전이 pug입니다.
위와 같은 명령어를 수행하면 다음과 같은 프로젝트 구조가 생성됩니다.
express-generator를 통해 생성된 프로젝트를 실행 시 진입점은 bin/www입니다.
-
bin/www : 서버를 실행하는 스크립트입니다. http모듈에 express 모듈을 연결하고, 포트를 지정합니다.
-
nodemodules : 노드 패키지 모듈들이 담긴 파일입니다. 기본 프로젝트를 만들 때 필요한 패키지들만 담겨있습니다.
-
public : 브라우저 등의 클라이언트들이 접근 가능한 폴더로 이미지, 자바스크립트, css 파일들을 넣어 줄 수 있습니다.
-
routes : 주소별 라우터를 넣어두는 곳입니다.
-
views : 템플릿 파일을 보관하는 곳입니다.
- app.js : express앱의 본체입니다. 여러 미들웨어를 사용하고 있습니다.
- package.json : 프로젝트의 실행 명령어, 사용 패키지들과 같은 기록을 담고 있습니다.
- package.json : 사용하는 패키지들의 버전 정보와 같은 것들을 담고 있습니다.
직접 express-generator를 사용하여 express 앱을 만들어 보신다면 코드들이 모두 ES6이전의 문법으로 작성된 것을 볼 수 있습니다.
# 직접 구조 잡기
두 번째 방법은 직접 프로젝트의 구조를 잡는 것입니다. express-generator를 이용해 프로젝트 구조를 잡고 그것을 참고하여 재 작성하며 express 프로젝트의 구조를 파악할 수 있었습니다.
https://github.com/dorakang612/chatPrac/tree/337900a9523afb0231b5b2fa65bd7c4b6828f4bd
위는 제가 채팅 서비스를 만들면서 잡은 프로젝트 구조입니다. Node.js는 기본적으로 CommonJS 기반의 모듈 시스템을 사용해서 import와 export 같은 키워드를 사용하지 못하고 있습니다.
https://www.daleseo.com/js-babel-node/
이분의 글을 참고하시면 이를 해결할 수 있을 것 같습니다. 다음 글에서 제가 직접 해보고 위의 공유한 프로젝트 구조 코드와 현재 작성 중인 프로젝트의 코드들도 수정해 보겠습니다.
1. 먼저 프로젝트를 시작하기 위한 디렉터리를 생성합니다.
- 프로젝트 이름으로 된 폴더를 하나 생성합니다.
2. package.json 생성하기
npm init
위의 명령어를 이용하여 package.json을 생성합니다.
3. 필요한 패키지들을 설치합니다.
npm i [Package_Name]
4. 프로젝트의 구조를 잡습니다.
chatPrac
ㄴpublic // 클라이언트에서 접근 할 수 있는 폴더입니다.
ㄴimages
ㄴjavascript
ㄴstyles
ㄴmodels // DB에 관한 스키마와 DB연결 코드가 있는 폴더입니다.
ㄴroutes // 라우터들이 있는 폴더입니다.
ㄴviews // 탬플릿들이 있는 폴더입니다.
ㄴapp.js // 프로젝트의 시작점이 되는 파일입니다.
ㄴpackage.json
ㄴ.env // 환경변수, 비밀번호와 같이 보호되어야할 정보들이 담긴 파일입니다.
ㄴreadme.md
5. 필요한 파일들의 내용을 작성합니다.
app.js
const express = require("express");
const path = require("path");
const logger = require("morgan");
const cookieParser = require("cookie-parser");
const session = require("express-session");
require("dotenv").config();
const indexRouter = require("./routes");
const app = express();
// 세션 설정
const sessionMiddleware = session({
resave: false,
saveUninitialized: false,
secret: `${process.env.COOKIE_SECRET}`,
cookie: {
httpOnly: true,
secure: false,
},
});
// 탬플릿과 탬플릿 엔진 설정
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");
// express 서버의 포트 지정
app.set("port", process.env.PORT || 3000);
// express의 미들웨어 설정
// request에 대한 로그를 기록하는 미들웨어
app.use(logger("dev"));
// 정적 파일들을 접근할 수 있도록하는 미들웨어
app.use(express.static(path.join(__dirname, "public")));
// request의 본문을 분석해주는 미들웨어
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// request의 쿠키를 해석해주는 미들웨어
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(sessionMiddleware);
// index 라우터
app.use("/", indexRouter);
// 404에러를 찾고 error handler로 인계하는 미들웨어
app.use((req, res, next) => {
const err = new Error("Not Found");
err.status = 404;
next(err);
});
// error handler
app.use((err, req, res, next) => {
res.local.message = err.message;
res.local.error = req.app.get("env") === "development" ? err : {};
res.status(err.status || 500);
res.render("error");
});
// 서버 설정
const server = app.listen(app.get("port"), () => {
console.log(app.get("port"), "번 포트에서 대기중입니다.");
});
module.exports = app;
.gitignore
: https://github.com/github/gitignore/blob/master/Node.gitignore
깃허브에 올릴 때 제외할 파일들의 목록입니다.
views/
: express-genenrator로 생성되는 파일들과 동일하게 적어주었습니다.
public/styles/style.css
: 마찬가지로 express-genenrator로 생성되는 파일과 동일하게 적어주었습니다.
routes/index.js
const express = require("express");
const router = express.Router();
router.get("/", (req, res) => {
res.locals.title = "Node Chat";
res.render("index");
});
module.exports = router;
.env
: 본인이 서비스를 개발함에 있어서, 타인에게 노출되어서는 안 될 환경변수 및 데이터베이스 비밀번호 등을 키와 값의 쌍으로 작성합니다.
# express 주요 개념
http모듈을 가지고 서버를 만들 수 있지만, express 프레임워크를 사용한다면 middleware와 router를 사용 가능하기 때문에 서버 구축 시 수월하게 진행할 수 있습니다.
app.js에서 app = express();라는 코드가 존재합니다. 이는 app이라는 객체가 express 메서드를 호출하여 만들어지는 것이며, 이 app이 프로젝트의 중심이 됩니다.
-
app.set(name, value) : 서버 설정을 위한 속성을 지정합니다.
-
app.get(name) : 서버 설정의 속성을 가져옵니다.
-
app.use([path], function) : 미들웨어 함수를 사용합니다.
-
app.get([path], function) : 특정 패스로 요청된 정보를 처리합니다.
미들웨어
: 요청과 응답의 중간에 위치하기 때문에 미들웨어라 불리며, 요청과 응답을 처리하는 역할을 합니다.
-
app.use() 메서드의 인자로 들어있는 함수가 미들웨어입니다.
-
라우터와 에러 핸들러 또한 미들웨어의 일종입니다.
- 미들웨어 내부에서 next()의 역할은 다음 미들웨어로 넘어가는 것입니다. 미들웨어 제어의 핵심이라 할 수 있습니다. next()의 인자로 경로를 주면 특수기능을 하고, 경로 이외의 것을 주면 에러 내용으로 간주하여 에러 핸들러로 이동합니다.
- 에러 핸들링 미들웨어 [app.use((err, req, res, next)=>{})] 는 일반적으로 미들웨어 제일 아래에 위치하여 위에 있는 미들웨어들에서 발생하는 에러를 받아서 처리합니다.
- app.use(middleware1, middleware2, middleware3)과 같이 하나의 use에 여러 미들웨어를 장착하여 사용할 수 있습니다. 하지만 이는 가독성이 떨어질 수 있으니, 특별한 상황이 아닌 경우에는 따로 사용하는 것을 추천드립니다.
1. morgan 미들웨어
요청에 대한 정보를 콘솔에 기록해주는 미들웨어입니다.
사용하는 방법은 아래와 같습니다.
const logger = require('morgan');
app.use(logger('dev')); // short, common, combined 등의 값을 줄 수 있고, 그에 따라서 콘솔의 내용이 달라집니다.
개발 시에는 short 혹은 dev, 배포 시에는 common 혹은 combined를 사용합니다. 각각의 내용의 차이는 직접 확인해 보시기 바랍니다.
또한 사용했을 때, 요청 기록의 예시는 다음과 같습니다.
GET / 200 51.267ms - 1539
위의 기록된 양식은 dev, 즉 개발하는 용도에서 쓰이는 양식입니다.
-
GET : HTTP 메서드 요청입니다.
- / : 요청 주소입니다.
- 200 : HTTP의 상태 주소입니다.
- 51.267ms : 응답 속도입니다.
- 1539 : 요청에 대한 응답의 바이트 크기입니다.
2. body-parser 미들웨어
요청의 본문을 분석해주는 미들웨어입니다. express 4.16 버전부터는 내장된 모듈이 되었기 때문에 따로 패키지를 설치하고 불러오지 않아도 사용 가능합니다.
req.body 와 같은 것을 사용할 수 있는 것이 이 body-parser 미들웨어 덕분입니다.
꼭 해당 미들웨어가 필요한 경우가 있는데, JSON과 URL-encoded 형식의 본문 외에도 Raw(버퍼 데이터), Text 형식의 본문을 추가로 해석하기 위해 사용합니다.
app.use(express.json());
app.use(express.urlencoded({extended:false});
3. cookie-parser 미들웨어
요청에 동봉된 쿠키를 해석하기 위해 사용하는 미들웨어입니다.
const cookieParser = require('cookie-parser');
app.use(cookieParser());
위의 cookieParser()에 첫 번째 인자로 문자열을 넣어 줄 수 있는데, 암호화된 쿠키가 있는 경우에 제공한 문자열을 키로 삼아 복호화할 수 있습니다.
4. static 미들웨어
정적인 파일들을 제공하는 미들웨어로, 내장 모듈입니다.
app.use(express.static(path.join(__dirname,"public"));
요청의 경로를 읽어 정적 파일을 제공합니다. 서버 폴더의 경로와 요청의 경로가 서로 달라 보안에 보탬이 되며, 정적 파일 제공을 알아서 처리해줍니다.
static 미들웨어는 요청에 부합하는 정적 파일을 발견한 경우 응답으로 해당 파일을 전송합니다. 이 경우에는 응답을 보냈으므로 라우터가 실행되지 않지만, 파일을 못 찾은 경우엔 요청을 라우터로 넘깁니다. 때문에 자신의 서비스에 맞게 미들웨어를 배치하고, 쿠키가 정적 파일에 영향을 미치지 않는 다면, morgan 미들웨어 바로 밑에 배치를 하기도 합니다.
5. express-session 미들웨어
세션 관리용 미들웨어입니다. 로그인 등의 이유로 세션을 구현할 때 매우 유용합니다. express-generator로는 설치가 되지 않기 때문에 필요시 직접 설치하면 됩니다.
npm i express-session
사용하는 방법은 이러합니다.
...
const session = require('express-session');
...
app.use(cookieParser('secret code'));
app.use(session({
resave: false,
saveUninitialized: false,
secret: 'secret code',
cookie: {
httpOnly: true,
secure: false,
}
}));
...
인자로 세션에 대한 설정을 받습니다.
-
resave : 요청이 왔을 때 세션에 수정사항이 생기지 않더라도 다시 저장할지에 대한 설정입니다. true면 다시 저장을, false이면 수정사항이 생길 때만 다시 저장합니다.
-
saveUnitialized : 세션에 저장할 내역이 없더라도 세션을 저장할지에 대한 설정입니다. 보통 방문자를 추적할 때 사용합니다.
-
secret : 필수 항목으로 cookie-parser의 비밀키와 같은 역할입니다. 세션 쿠키를 보낼 때 사용합니다.
-
cookie : 세션 쿠키에 관한 설정으로, 일반적인 쿠키의 옵션이 모두 제공됩니다.
-
store : 위의 코드에는 나와있지 않지만 많이 사용하는 옵션입니다. 이는 session의 저장 위치를 결정하는 옵션으로 위에는 아무것도 적혀있지 않기 때문에 메모리에 세션이 저장됩니다. 메모리에 저장되다 보니, 서버가 재시작되면 세션은 모두 사라집니다. 배포 시에는 store에 DB를 연결하여 세션을 유지합니다. 자주 쓰이는 것은 redis(레디스)라는 것이 있다고 합니다. 저는 현재 mongoDB를 사용하고 mongoose를 사용하기 때문에 mongoose를 이용하는 방법도 있는 것 같아 추후 시도해 보려 합니다.
express-session은 req객체 안에 session 객체를 만듭니다(req.session). 이 객체에 값을 대입하거나 삭제함으로써 세션을 변경할 수 있습니다. 나중에 세션을 한 번에 지우려면 req.session.destroy() 메서드를 호출합니다. 현재 세션의 아이디는 req.sessionID로 확인할 수 있습니다.
- 주의 사항
미들웨어를 과도하게 많이 달아 놓으면 요청에 대한 응답 시간이 늦어질 수 있기 때문에 꼭 필요한 미들웨어만을 만들거나 추가하여 사용하도록 합니다.
라우터
익스프레스 내부에서 app.use()를 사용하므로 일종의 미들웨어라고 할 수 있습니다. 첫 번째 인자로는 주소를 받습니다.
use 메서드 외에도 HTTP 메서드를 사용할 수 있습니다.
-
use : 모든 HTTP메서드에 대해서 요청 주소만 일치하면 실행합니다.
-
HTTP 메서드 : 요청 주소와 HTTP메서드가 일치해야만 실행합니다.
const express = require("express");
const router = express.Router();
...
router.get('/',(req,res)=>{
...
});
...
module.exports = router;
app.use와 같이 router 또한 use, get, post, delete, patch 같은 메서드를 붙일 수 있습니다. use를 제외하곤 각각 HTTP 요청 메서드와 상응합니다. 또한 router 하나에 여러 개의 미들웨어를 부착할 수 있습니다.
라우터에서는 반드시 응답을 보내야 합니다. 또는 에러로 요청을 넘겨야 브라우저가 응답을 계속 기다리게 하지 않을 수 있습니다.
-
res.send() : 버퍼/ 문자열/ HTML/ JSON을 보낼 수 있는 응답 메서드입니다.
-
res.sendFile(파일 경로) : 파일을 보낼 수 있는 응답 메서드입니다.
-
res.json() : JSON 데이터를 보낼 수 있는 응답 메서드입니다.
-
res.redirect(주소) : 다른 주소로 연결해주는 응답 메서드입니다.
-
res.render('템플릿 파일 경로', {변수=값}) : 템플릿 파일을 보낼 수 있습니다. 변수와 해당 값을 인자로 넘겨주어 템플릿에서 사용할 수 있습니다.
기본 적으로 200 HTTP 상태 코드로 응답을 합니다. 이 상태 코드는 res.status(상태 코드). send()로 수정이 가능합니다.
요청에 대한 응답을 처리하지 못하면(처리할 수 있는 미들웨어가 없는 경우) 다음 미들웨어로 넘어갑니다. 에러가 있다는 404 HTTP 상태 코드를 보내야 하므로 다음 미들웨어에서 새로운 에러를 만들고 에러의 상태 코드를 404로 설정한 뒤 에러 처리 미들웨어로 넘깁니다.
미들웨어 관리의 핵심인 next() 메서드에 router에서만 동작하는 특수 기능이 있습니다.
next([route])를 사용하면 라우터에 연결된 나머지 미들웨어를 건너뛰고 해당 미들웨어로 넘어갈 수 있습니다.
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로 혹은 :type인 경우 req.params.type으로 조회할 수 있습니다.
또한 querystring도 사용이 가능합니다. req.query 객체 안에 들어있습니다. 주소로 /users/123?limit=5&skip=10이라는 주소의 요청이 들어온 경우에 req.params 객체는 { id : '123' }이며, req.query 객체는 { limit : '5' , skip : '10' }이 됩니다. 요청 주소에 대한 여러 정보가 있어 유용하게 사용 가능하지만, 일반 라우터들보다 뒤에 위치시켜, 다른 라우터들을 방해하지 않도록 해야 합니다.
템플릿 엔진
자바스크립트를 이용해서 HTML을 렌더링 할 수 있게 해 줍니다. 여러 가지 가 있지만 저는 pug를 사용 중입니다.
pug(구 Jade) : https://pugjs.org/api/getting-started.html
pug 외에도 다양한 템플릿 엔진이 있으므로 본인에게 잘 맞는 것을 선택하여 사용하시면 됩니다.
ejs : https://ejs.co/
#학습자료
https://www.gilbut.co.kr/book/view?bookcode=BN002045&perdevice=pc
'[WEB] > Node.js' 카테고리의 다른 글
React.js 와 Express 연동 (0) | 2020.06.23 |
---|---|
Node.js - Passport.js 사용 (0) | 2020.05.27 |
Node.js - Express와 Babel 그리고 Nodemon (0) | 2020.05.12 |
Node.js -Nodemon (0) | 2020.04.24 |
Nodejs[Express] - 비밀키 관리 (0) | 2020.04.13 |