$ npm init
위와 같이 완료하면 package.json이라는 파일이 생긴다.
ES6 환경에서 진행할 것이기 때문에 package.json에 "type": "module"을 입력한다.
$ npm i express express-session dotenv morgan nunjucks cookie-parser nunjucks
개발 전용 라이브러리
$ npm i -D nodemon
express 프레임워크를 통해 서버를 실행시킬 메인 파일을 생성한다.
나는 app.js라는 이름을 생성했다.
import express from "express";
// 환경변수 세팅
dotenv.config();
// express
const app = express();
// 서버 포트 세팉
app.set("port", process.env.PORT || 3000);
const server = app.listen(app.get("port"), () => {
console.log(app.get("port") + "번 포트에서 서버 실행");
});
cookie_secret, api_key, db_password 등 민감한 정보를 코드 상에 넣으면 유출될 가능성이 높기 때문에 환경변수 파일을 따로 만들어 관리한다.
이때 dotenv 라이브러리를 통해 쉽게 환경변수를 관리할 수 있다.
.env 라는 이름의 파일을 생성해 환경변수를 넣어주고 app.js에 dotenv를 추가한다.
import dotenv from "dotenv";
// 환경변수 세팅
dotenv.config();
현재의 프로젝트 설정은 ESM이 아닌 ES6 환경을 기반이 때문에 dirname을 바로 사용하면 "dirname is not undefined"라는 오류가 발생할 것이다.
따라서 __dirname을 사용하기 위해 경로를 직접 설정해주어야 한다.
import path from "path";
// ES6 모듈 __dirname 에러 방지
const __dirname = path.resolve();
express에서는 템플릿언어를 통해 서버에서 동적으로 웹 페이지를 생성해 클라이언트에게 응답할 수 있다. 이 프로젝트 세팅에서는 nunjucks를 사용했다.
import nunjucks from "nunjucks";
// 템플릿 세팅 미들웨어
app.set("view engine", "html");
nunjucks.configure(path.join(__dirname, "views"), {
express: app,
watch: true,
});
위 코드는 "views라는 폴더안에서 동적 웹페이지 파일을 불러오겠다"라는 의미로 views 폴더 또한 직접 생성해주어야 한다.
또한 동적 웹페이지(html) 파일에서도 자바스크립트, css가 필요하다. 이때 자바스크립트, css는 코드가 변하는 것이 아니기 떄문에 정적 파일이라고 부르는데 이 파일들을 불러오기 위해서도 경로를 설정해주어야 한다.
// 정적 파일들을 public이라는 폴더로 접근할수 있게 해주는 미들웨어
app.use(express.static(path.join(__dirname, "public")));
cors는 한 주소에서 다른 주소의 리소스를 요청하는 것을 보안상의 이유로 방지하는 체제이다. 이 때문에 네트워크가 오류가 발생할 수 있으므로 다음과 같이 설정한다.
// cors 허용
app.use(cors({
origin: '*',
}));
하지만 위의 코드는 모든 주소에서 해당 서버로의 요청을 허용하는 것이기 때문에 보안상으로 취약해질 수 있기에 배포하는 과정에서 수정해야할 필요가 있다.
morgan 라이브러리는 서버의 요청과 응답 로깅을 간편하게 도와주는 라이브러리이다. morgan은 개발단계이기에 다음과 같이 설정한다.
import morgan from "morgan";
app.use(morgan("dev"));
morgan을 설정하면 다음과 같은 로그를 볼 수 있다.
post, patch 등의 요청에서 클라이언트는 서버에게 body를 통해서 데이터를 보낸다. 하지만 express 서버에서 그 body를 req객체에 저장하기 위해서는 다음과 같은 설정이 필요하다.
// 요청을 처리할 수 있게 변환해주는 미들웨어
app.use(express.urlencoded({extended: false}));
app.use(express.json());
위의 코드는 둘다 body의 데이터를 req객체에 저장해주어서 서버에서 처리할 수 있게 해주는 코드이다. 그렇다면 두 코드는 무슨 차이가 있을까?
바로 body 데이터의 형태의 차이 유무이다.
express.urlencoded({extended: false})는 x-www-form-urlencoded를 처리하고, express.json()는 json 형태의 데이터를 처리한다.
클라이언트로 부터 쿠키를 받아 검증하고 사용자를 추적하고 관리하기 위해 세션을 처리해야 한다. 다음과 같이 설정한다.
// 쿠키 및 세션 처리 미들웨어
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(sessionMiddleware);
세션 미들웨어는 다음과 같다.
import session from "express-session";
import dotenv from "dotenv";
dotenv.config();
export const sessionMiddleware = session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
}
});
서버에서 에러가 발생한다고 해서 바로 서버가 종료된다면 매우 안타까운 상황이다.. 따라서 라우터가 없거나 에러가 발생한 경우를 처리해주어야 한다.
// 라우터 404 에러 방지 미들웨어
app.use(routerMiddleware);
// 에러 핸들링 미들웨어
app.use(errorMiddleware);
라우터 미들웨어
export const routerMiddleware = (req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
};
에러 미들웨어
export const errorMiddleware = (err, req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV !== "production" ? err : {};
res.status(err.status || 500);
res.send(err.message);
};
하지만 위의 코드를 추가한다고 해서 어떻게 라우터 에러 방지 및 에러 처리되는지를 이해해야 한다. 이를 이해하려면 express에서 요청이 들어왔을때 미들웨어의 처리 순서를 알아야 한다.
다음과 같이 서버에 요청이 들어올 경우 미들웨어를 전부 순회를 하다가 처리가 끝나면 응답한다. app.js의 코드 전체를 살펴보자
import express from "express";
import cors from "cors";
import dotenv from "dotenv";
import path from "path";
import nunjucks from "nunjucks";
import morgan from "morgan";
import cookieParser from "cookie-parser";
import { sessionMiddleware } from "./appMiddleware/session.middleware.js";
import { routerMiddleware } from "./appMiddleware/router.middleware.js";
import { errorMiddleware } from "./appMiddleware/error.middleware.js";
// 환경변수 세팅
dotenv.config();
// express
const app = express();
// ES6 모듈 __dirname 에러 방지
const __dirname = path.resolve();
// 서버 포트 세팉
app.set("port", process.env.PORT || 3000);
// 템플릿 세팅 미들웨어
app.set("view engine", "html");
nunjucks.configure(path.join(__dirname, "views"), {
express: app,
watch: true,
});
// middleware
// middleware
// cors 허용
app.use(cors({
origin: '*',
}));
app.use(morgan("dev"));
// 정적 파일들을 public이라는 폴더로 접근할수 있게 해주는 미들웨어
app.use(express.static(path.join(__dirname, "public")));
// 요청을 처리할 수 있게 변환해주는 미들웨어
app.use(express.urlencoded({extended: false}));
app.use(express.json());
// 쿠키 및 세션 처리 미들웨어
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(sessionMiddleware);
// 라우터 404 에러 방지 미들웨어
app.use(routerMiddleware);
// 에러 핸들링 미들웨어
app.use(errorMiddleware);
const server = app.listen(app.get("port"), () => {
console.log(app.get("port") + "번 포트에서 서버 실행");
});
라우터 방지 및 에러 처리 미들 웨어는 맨 마지막에 위치하는 것을 볼 수 있다. 모든 미들웨어 및 라우터를 거치고 요청에 해당하는 주소의 라우터가 없을때 혹은 중간에 에러가 발생했을때 미들웨어의 next를 거쳐서 끝에 도달하여 처리되는 것이다. 이 구조를 알아야 앞으로의 express 인생이 편해진다..