이번 플젝에서 express를 사용하게 되었다. 요새는 프론트도 SSR을 위해 express를 띄워두고 서비스 한다던데 이번 기회에 잘 알아두자! 미들웨어를 중심으로 공식문서를 보며 정리해보았다.
Node.js에 대해 깊게 공부하면 한도 끝도 없으니, 간단히만 알아보자.
Node.js는 JavaScript 런타임인데, 그냥 내 로컬 환경에서도 JavaScript 언어를 실행할 수 있게 해주는 녀석이라고 생각하면 되겠다. 원래 JavaScript는 브라우저에서 밖에는 동작하지 않았다.
공식문서에는 Node.js를 Chrome V8 JavaScript 엔진으로 빌드 된 JavaScript 런타임(환경)이라고 설명하고 있다. 쉽게 말해서 Node.js란 JavaScript를 실행할 수 있는 프로그램이라고 볼 수 있다.
Node.js가 만들어지기 전 브라우저만이 JavaScript 실행기였다면, Node.js가 만들어 진 후에는 JavaScript 실행기가 하나 더 만들어졌다고 말할 수 있다.
이 외에도, 논블로킹, 비동기, 이벤트 루프, C++ addon, libuv, 이그니션, 터보팬, jit 컴파일러 등의 키워드가 Node.js와 함께 등장하지만, 여기서 자세히 다루지는 않겠다.
Express는 Node.js 진영에서 가장 많이 사용되는 웹 프레임워크이다. Express의 공식문서에 소개된 문장은 다음과 같다.
Fast, unopinionated, minimalist web framework for Node.js
빠르고 미니멀하다는 것 까지는 알겠는데, unopinionated는 뭘까? Opinionated Framework에는 Django나 Spring boot가 있다. 이런 프레임워크는 개발자가 프레임워크가 제공하는 기능을 사용하기 위해서는 프레임워크가 요구하는 규칙을 따라야 한다. 반대로, unopinionated는 개발자가 프레임워크가 제공하는 기능을 사용할지 말지를 결정할 수 있다는 것이다. 프레임워크의 디자인 철학 중 하나이다.
unopinionated는 다음과 같은 특징을 가진다.
미리 정의된 규칙이나 강제성이 적다: Express는 개발자에게 많은 자유를 제공하며, 애플리케이션의 구조나 패턴에 대해 강력한 제약을 가하지 않는다. 따라서 Express 애플리케이션을 개발할 때, 개발자가 많은 결정을 내릴 수 있다.
최소한의 설정과 제한된 추상화: Express는 가능한 최소한의 설정을 제공하며, 웹 애플리케이션의 구조를 개발자가 직접 설계하고 관리해야 한다. 이로써 개발자가 자신의 프로젝트에 적합한 아키텍처와 패턴을 선택할 수 있다.
미리 정의된 폴더 구조나 파일 구조가 없음: Express는 프로젝트의 폴더 구조나 파일 구조에 대해 엄격한 규칙을 강제하지 않는다. 따라서 개발자가 자신의 프로젝트에 맞게 구조를 설계할 수 있다.
django를 주로 사용했던 나에게는 다소 머리가 아픈 부분이다... 이런 구조 때문에 express는 django나 spring보다 더 유연한 프레임워크로 간주된다. 하지만, 이런 유연성은 개발자가 프로젝트의 구조를 직접 설계해야 하고 더 큰 책임을 부여하는 단점으로 이어지기도 한다.
express는 놀랍도록 쉽고 별게 없다. 다음과 같이 express를 설치하고, 간단한 서버를 만들어보자.
$ npm install express
// app.js
import express from "express";
const app = express();
app.get("/qqq", (req, res) => {
res.send("Hello World!");
});
app.listen(3000, () => {
console.log("백엔드 API 서버가 켜졌어요!!!");
});
$ node app.js
localhost:3000/qqq로 접속하면 Hello World!가 출력된다. 물론 좋은 구조는 아니다.
express-generator는 이렇게 자유분방한 express에 구조를 한스푼 추가해주는 녀석이다. express-generator를 설치하고, express 애플리케이션을 생성해보자.
# mac에서 권한요류 발생시 sudo 추가
npm install express-generator -g
# express가 안된다면 npx express-generator 명령으로
express hello-express --view=ejs # ejs는 템플릿 엔진
cd hello-express
npm install
npm start
이렇게 생성된 파일구조는 다음과 같다.
.
├── app.js
├── bin
│ └── www
├── package.json
├── public
│ ├── images
│ ├── javascripts
│ └── stylesheets
│ └── style.css
├── routes
│ ├── index.js
│ └── users.js
└── views
├── error.ejs
├── index.ejs
└── layout.ejs
7 directories, 9 files
위에서 생성된 각 파일의 역할은 다음과 같다.
미들웨어는 또 뭘까? 웹 프레임워크에서 가장 중요한 개념이라고 봐도 무방하겠다. 프레임워크에서의 미들웨어는 웹 애플리케이션 또는 API의 요청(request)과 응답(response) 처리 중간에 위치하는 소프트웨어 구성 요소이다. 미들웨어는 클라이언트로부터의 요청이 서버에 도달하기 전 또는 서버에서 응답이 클라이언트에게 보내지기 전에 요청 또는 응답을 가로채고 수정, 분석 또는 다양한 작업을 수행한다. (뭔가 가로챈다고 하니 어감이 별로지만 꼭 필요한 작업이다.) 미들웨어를 사용하면 웹 애플리케이션의 동작을 확장하고 수정할 수 있으며, 코드를 모듈화하고 재사용성을 높일 수 있다.
express에도 당연히 middleware가 존재한다. 미들웨어의 주요 특징은 다음과 같다.
미들웨어에는 다음과 같은 다섯가지 종류가 있다.
가장 많이 본 미들웨어이다. app.use()
와 app.METHOD()
(GET, PUT 또는 POST 등)이 그것이다. app
instance에 바인드 하여 앱 전체에 적용되는 미들웨어이다.
var app = express();
app.use("/user/:id", function (req, res, next) {
console.log("Request Type:", req.method);
next();
});
라우터 레벨 미들웨어는 express.Router() 인스턴스에 바인드된다는 점을 제외하면 애플리케이션 레벨 미들웨어와 동일한 방식으로 작동한다.
var app = express();
var router = express.Router();
router.use("/user/:id", function (req, res, next) {
console.log("Request Type:", req.method);
next();
});
// mount the router on the app
app.use("/", router);
이렇게만 보면 어플리케이션 레벨과 라우터 레벨의 미들웨어가 어떤 차이가 있는지 잘 모르겠다. 이에 대해 더 자세히 공부해보자.
적용 범위:
Application-level middleware:
express.json()
미들웨어는 HTTP 요청의 본문 데이터(JSON 형식)를 파싱하여 req.body
객체에 저장하는 데 사용된다. 이 미들웨어는 애플리케이션 어디에서든 사용 가능하다.Router-level middleware:
express.Router()
를 사용하여 생성한 라우터 객체에 미들웨어를 추가할 때 이 미들웨어는 해당 라우터와 그 하위 경로에서만 실행된다.적용 방법:
Application-level middleware:
app.use()
또는 app.<HTTP_METHOD>()
를 통해 Express 애플리케이션에 추가된다.app.use()
를 사용하여 애플리케이션 전체에 로깅 미들웨어를 추가할 수 있다.Router-level middleware:
router.use()
또는 router.<HTTP_METHOD>()
를 통해 특정 라우터에 추가되다./api
경로에 대한 라우터에만 인증 미들웨어를 추가하려면 해당 라우터에서 router.use()
를 사용하여 미들웨어를 추가한다.적용 순서:
Application-level middleware:
Router-level middleware:
/api
경로에 대한 라우터에만 인증 미들웨어를 추가하면 /api
로 들어오는 요청에만 인증이 적용된다.Application-level Middleware 예시:
const express = require("express");
const app = express();
// Application-level 미들웨어: 모든 요청에 대해 실행된다.
app.use((req, res, next) => {
console.log("Application-level Middleware");
next(); // 다음 미들웨어 또는 라우트 핸들러로 제어를 전달한다.
});
// 라우트 핸들러: Application-level 미들웨어 다음에 실행된다.
app.get("/", (req, res) => {
res.send("홈 페이지");
});
app.listen(3000, () => {
console.log("서버가 3000 포트에서 실행 중입니다.");
});
위 코드는 app.use()를 사용하여 Application-level 미들웨어를 추가하고, 모든 요청에 대해 실행되는 예시이다.
Router-level Middleware 예시:
const express = require("express");
const app = express();
// Router-level 미들웨어: 특정 라우터에만 적용된다.
const router = express.Router();
router.use((req, res, next) => {
console.log("Router-level Middleware");
next(); // 다음 미들웨어 또는 라우트 핸들러로 제어를 전달한다.
});
// 라우트 핸들러: Router-level 미들웨어 다음에 실행된다.
router.get("/api", (req, res) => {
res.send("API 라우트");
});
app.use("/myapp", router); // 라우터를 /myapp 경로에 마운트한다.
app.listen(3000, () => {
console.log("서버가 3000 포트에서 실행 중입니다.");
});
위 코드는 router.use()를 사용하여 Router-level 미들웨어를 추가하고, 특정 라우터(/myapp 경로)에만 적용되는 예시이다. Router-level 미들웨어는 /myapp/api 경로에 대한 요청에만 실행된다.
에러 핸들링 미들웨어는 말 그대로 에러를 처리하고 응답을 생성하는 데 사용된다. 이 미들웨어는 항상 4개의 인수가 필요하다. (err, req, res, next) 에러가 발생하면 이 미들웨어가 호출되어 에러를 처리하고 클라이언트에게 에러 응답을 보낸다.
app.use(function (err, req, res, next) {
console.error(err.stack); // 에러가 발생하면 콘솔에 에러 스택을 출력 후
res.status(500).send("Something broke!"); // 클라이언트로 500코드와 함께 응답 메세지를 보낸다.
});
Express에서 기본적으로 제공해주는 미들웨어이다. express.static (정적 파일 제공), express.json (JSON 파싱), express.urlencoded (URL-encoded 데이터 파싱) 등이 있다.
기본 제공 미들웨어와 반대로, Express에서 제공하지 않는 미들웨어이다. 대표적으로 body-parser, cookie-parser, multer 등이 있다. 이들을 사용하려면 별도로 설치해야한다.
npm install cookie-parser
var express = require("express");
var app = express();
var cookieParser = require("cookie-parser");
// load the cookie-parsing middleware
app.use(cookieParser());
express() 호출을 통해 생성되는 app 객체에서 제공해 주는 메소드들이다.
미들웨어를 추가하는 메소드이다. 미들웨어는 요청과 응답 사이에서 실행되는 함수이다. 미들웨어 함수는 다음과 같은 세 개의 인수를 가진다.
HTTP 메소드에 따라 라우트를 추가하는 메소드이다.
서버를 시작하는 메소드이다. app.listen()
메소드는 다음과 같은 세 개의 인수를 가진다.
모든 HTTP 메소드에 대해 라우트를 추가하는 메소드이다. app.all()
메소드는 다음과 같은 두 개의 인수를 가진다.
라우트 매개변수를 설정하는 메소드이다. app.param()
메소드는 다음과 같은 두 개의 인수를 가진다.
애플리케이션의 경로를 반환하는 메소드이다. app.path()
메소드는 다음과 같은 하나의 인수를 가진다.