6장. 익스프레스 웹 서버 만들기

Doozuu·2023년 5월 9일
0

Node.js

목록 보기
5/6

npm에는 서버를 제작하는 과정에서 겪게 되는 불편을 해소하고 편의 기능을 추가한 웹 서버 프레임워크가 있다. 대표적인 것이 익스프레스이다.

익스프레스의 장점

  • http 모듈의 요청과 응답 객체에 추가 기능들을 부여했다. 기존 메서드들도 계속 사용할 수 있지만, 편리한 메서드들을 추가해 기능을 보완했다.
  • 코드를 분리하기 쉽게 만들어 관리하기도 용이하다.
  • 더 이상 if문으로 요청 메서드와 주소를 구별하지 않아도 된다.



6.1 익스프레스 프로젝트 시작하기

express와 nodemon을 설치한다.

npm i express
npm i -D nodemon

npm init 명령어를 이용해 package.json 파일을 먼저 만든다.

  • start 속성을 추가해서 app.js를 nodemon 모듈로 자동으로 재시작하도록 해준다.(서버 코드에 수정 사항이 생길 때마다 매번 재시작해야 하는 수고로움을 덜어줌.)
{
  "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를 다음과 같이 적어준다.

  • express 모듈을 실행해 app 변수에 할당한다. 익스프레스 내부에 http 모듈이 내장되어 있으므로 서버의 역할을 할 수 있다.
  • app.set('port', 포트)로 서버가 실행될 포트를 설정한다. process.env 객체에 PORT 속성이 있다면 그 값을 사용하고, 없다면 기본값으로 3000번 포트를 이용하도록 한다.
  • app.get(주소, 라우터)는 주소에 대한 GET 요청이 올 때 어떤 동작을 할지 적는 부분이다. 익스프레스에서는 res.write나 res.end 대신 res.send를 사용하면 된다.
  • GET 요청 외에도 POST, PUT, PATCH 등에 대한 라우터를 위한 app.post, app.put, app.patch 등의 메서드가 존재한다.
  • listen을 하는 부분은 http 웹 서버와 동일하다. 포트는 app.get('port')로 가져온다.
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"), "번 포트에서 대기 중");
});



6.2 자주 사용하는 미들웨어

미들웨어는 익스프레스의 핵심이다.

  • 요청과 응답의 중간에 위치하기 때문에 미들웨어라고 부른다.
  • 뒤에 나오는 라우터와 에러 핸들러 또한 미들웨어의 일종이므로 미들웨어가 익스프레스의 전부라고 해도 과언이 아니다.
  • 미들웨어는 요청과 응답을 조작해 기능을 추가하기도 하고, 나쁜 요청을 걸러내기도 한다.
  • 미들웨어는 app.use와 함께 사용한다. app.use(미들웨어) 형태이다.

익스프레스 서버에 미들웨어 연결해보기

  • app.use에 매개변수가 req,res,next인 함수를 넣으면 된다.
  • 미들웨어는 위에서부터 아래로 순서대로 실행되면서 요청과 응답 사이에 특별한 기능을 추가할 수 있다. next는 다음 미들웨어로 넘어가는 함수이다. next를 실행하지 않으면 다음 미들웨어가 실행되지 않는다.
  • 주소를 첫 번째 인수로 넣어주지 않는다면 미들웨어는 모든 요청에서 실행되고, 주소를 넣는다면 해당하는 요청에서만 실행된다.
  • 에러 처리 미들웨어는 매개변수가 err,req,res,next이다. 모든 매개변수를 사용하지 않더라도 매개변수가 반드시 4개여야 한다. res.status 메서드로 HTTP 상태 코드를 지정할 수 있다.
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

  • 설치한 패키지들을 불러온 뒤 app.use에 연결한다.
  • dotenv 패키지는 .env 파일을 읽어서 process.env로 만든다. process.env.COOKIE_SECRET에 cookiesecret 값이 할당된다.
  • process.env를 별도의 파일로 관리하는 이유는 보안과 설정의 편의성 때문이다. 비밀 키들을 소스 코드에 그대로 적어두면 소스 코드가 유출되었을 때 키도 같이 유출된다. 따라서 .env 같은 별도의 파일에 비밀 키를 적어두고 dotenv 패키지로 비밀 키를 로딩하는 방식으로 관리하곤 한다.
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"), "번 포트에서 대기 중");
});

6.2.1 morgan

요청과 응답에 대한 정보를 콘솔에 기록한다.

app.use(morgan('dev'))

인수로 dev 외에 combined, common, short, tiny 등을 넣을 수 있다.
dev 모드 기준으로 GET / 500 7.409ms - 50 은 각각 HTTP 메서드, 주소, HTTP 상태 코드, 응답 속도, - 응답 바이트를 의미한다.
요청과 응답을 한눈에 볼 수 있어 편리하다.

6.2.2 static

정적인 파일들을 제공하는 라우터 역할을 한다.
기본적으로 제공되기에 따로 설치할 필요 없이 express 객체 안에서 꺼내 장착하면 된다.

app.use('요청 경로', express.static('실제 경로'));

함수의 인수로 정적 파일들이 담겨 있는 폴더를 지정하면 된다.
정적 파일들을 알아서 제공해주므로 fs.readFile로 파일을 직접 읽어서 전송할 필요가 없다.

6.2.3 body-parser

요청의 본문에 있는 데이터를 해석해서 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 객체에 들어간다.

6.2.5 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는 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정하는 것이다.

express-session은 세션 관리 시 클라이언트에 쿠키를 보낸다. 안전하게 쿠키를 전송하려면 쿠키에 서명을 추가해야 하고, 쿠키를 서명하는 데 secret의 값이 필요하다. cookie-parser의 secret과 같게 설정하는 것이 좋다. 세션 쿠키의 이름은 name 옵션으로 설정한다.

cookie 옵션은 세션 쿠키에 대한 설정이다.

6.2.7 multer

이미지, 동영상 등을 비롯한 여러 가지 파일을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어이다.
멀티파트 형식이란 다음과 같이 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>



6.3 Router 객체로 라우팅 분리하기

익스프레스를 사용하는 이유 중 하나는 바로 라우팅을 깔끔하게 관리할 수 있다는 점이다.
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가 있다.
  • 다음과 같이 주소는 같지만 다른 코드가 있을 때 이를 하나의 덩어리로 줄일 수 있다.



6.4 req, res 객체 살펴보기

익스프레스의 req, res 객체는 http 모듈의 req, res 객체를 확장한 것이다.
기존 http 모듈의 메서드도 사용할 수 있고, 익스프레스가 추가한 메서드나 속성을 사용할 수도 있다.
다만, 익스프레스의 메서드가 워낙 편리하므로 기존 http 모듈의 메서드는 잘 쓰이지 않는다.

자주 쓰이는 메서드

  • req 메서드
  • res 메서드



6.5 템플릿 엔진 사용하기

템플릿 엔진은 자바스크립트를 사용해서 HTML을 렌더링할 수 있게 한다.

대표적인 템플릿 엔진으로 PugNunjucks가 있다.
(요즘에는 템플릿 엔진 대신 프론트엔드에서 리액트나 뷰를 더 많이 사용하는 추세이다.)

6.5.1 퍼그(제이드)

예전 이름인 제이드로 더 유명한 퍼그는 꾸준한 인기를 얻고 있다.
문법이 간단해서 코드양이 줄어들기 때문이다.
루비를 사용해봤다면 문법이 비슷해서 빠르게 적응할 수 있다.
단, HTML과는 문법이 많이 달라서 호불호가 갈린다.

설치

npm i pug

app.js에 아래 코드 추가

app.set('view engine', pug)

6.5.2 넌적스

넌적스는 퍼그의 HTML 문법 변화에 적응하기 힘든 분에게 유용한 템플릿이며, 파이어폭스를 개발한 모질라에서 만들었다.
HTML 문법을 그대로 사용하되 추가로 자바스크립트 문법을 사용할 수 있다.

설치

npm i nunjucks

app.js

사용법

변수 : 변수를 {{}}로 감싼다.
반복문 : 넌적스에서는 특수한 문을 {% %} 안에 쓴다.
조건문 : {% if 변수 %}, {% elif %}, {% else %}
include : 다른 HTML 파일을 넣을 수 있다.

profile
모든게 새롭고 재밌는 프론트엔드 새싹

0개의 댓글