본문 바로가기
JavaScript/Node.js

Node) 익스프레스로 웹 서버 만들기

by 박채니 2022. 12. 29.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[Node.js 교과서]의 책을 참고하여 포스팅한 개인 공부 내용입니다.

 

익스프레스 웹 서버 만들기

 

익스프레스

  • npm에는 서버를 제작하는 과정에서 겪게 되는 불편을 해소하고 편의 기능을 추가한 웹 서버 프레임워크
  • http 모듈의 요청과 응답 객체에 추가 기능들 부여
  • 코드를 분리하기 쉽게 만들어 관리에 용이

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

 

learn-express 폴더 생성 후 package.json 생성

package.json

{
  "name": "learn-express",
  "version": "0.0.1",
  "description": "익스프레스를 배우자",
  "main": "index.js",
  "scripts": {
    "start": "node app.js",
    "build": "tsc -p .",
    "dev": "nodemon --watch \"src/**/*.ts\" --exec \"ts-node\" src/app.ts"
  },
  "author": "chany",
  "license": "MIT",
  "dependencies": {
    "@types/express": "^4.17.15",
    "@types/node": "^18.11.18",
    "express": "^4.18.2",
    "ts-node": "^10.9.1",
    "typescript": "^4.9.4"
  },
  "devDependencies": {
    "nodemon": "^2.0.20"
  }
}

nodemon

- 서버 코드에 수정 사항이 생길 때마다 매번 서버를 재시작하는 일을 대신해줌 (자동으로 재시작)

- 개발용으로만 사용할 것을 권장

 

app.ts (서버 역할)

import express, { Request, Response } from "express";

const app = express();

app.set("port", process.env.PORT || 3000);

app.get("/", (req: Request, res: Response) => {
  res.send("Hello, Express");
});

app.listen(app.get("port"), () => {
  console.log(app.get("port"), "번 포트에서 대기 중");
});

익스프레스 내부에 http 모듈이 저장되어 있기 때문에 서버의 역할이 가능합니다.

  • app.set('port', 포트)
    • 서버가 실행될 포트 설정
    • app, set(키, 값)을 사용해 데이터 저장 가능
  • app.get(키)
    • 키 값을 가져옴
  • app.get(주소, 라우터)
    • 주소에 대한 GET 요청이 올 때 어떤 동작을 할지 작성 
    • res.send()를 사용하여 응답
$ yarn start

yarn run v1.22.19
$ node app.js
3000 번 포트에서 대기 중

 

만일 문자열이 아닌 HTML 파일로 응답을 하고 싶다면, res.sendFile() 메소드를 사용합니다. (path 모듈 사용)

index.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <title>익스프레스 서버</title>
  </head>
  <body>
    <h1>익스프레스</h1>
    <p>배워봅시다.</p>
  </body>
</html>

 

app.ts

import express, { Request, Response } from "express";
import path from "path";

const app = express();

app.set("port", process.env.PORT || 3000);

app.get("/", (req: Request, res: Response) => {
  // res.send("Hello, Express");
  res.sendFile(path.join(__dirname, "index.html"));
});

app.listen(app.get("port"), () => {
  console.log(app.get("port"), "번 포트에서 대기 중");
});

 


자주 사용하는 미들웨어

 

미들웨어

  • 요청과 응답 중간에 위치
  • 라우터와 에러 핸들러 또한 미들웨어의 일종
  • 위에서 아래로 순서대로 실행되면서 요청과 응답을 조작해 기능을 추가하거나 나쁜 요청을 걸러줌
  • app.use(미들웨어) 로 사용

app.ts

...

app.use((req: Request, res: Response, next: NextFunction) => {
  console.log("모든 요청에 다 실행됩니다.");
  next();
});

app.get(
  "/",
  (req: Request, res: Response, next: NextFunction) => {
    console.log("GET / 요청에서만 실행됩니다.");
    next();
  },
  (req: Request, res: Response) => {
    throw new Error("에러는 에러 처리 미들웨어로 이동합니다.");
  }
);

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  console.error(err);
  res.status(500).send(err.message);
});

...

app.use에 매개변수가 req, res, next인 함수를 넣어줍니다.

next 매개변수는 다음 미들웨어로 넘어가는 함수이고, next를 실행하지 않으면 다음 미들웨어가 실행되지 않습니다.

주소를 첫 번째 인수로 넣지 않으면 모든 요청에서 실행되고, 주소를 넣어주면 해당 주소에서만 미들웨어가 실행됩니다.

 

app.use와 app.get 같은 라우터에 미들웨어를 여러 개 장착할 수도 있습니다.

app.get 라우터에는 두 개의 미들웨어가 장착된 것을 확인할 수 있습니다. 마찬가지로 next를 호출해야 다음 미들웨어로 넘어갑니다.

 

app.get 라우터의 두 번째 미들웨어에서 에러가 발생하였고, 아래의 있는 에러 처리 미들웨어에 전달이 됩니다.

에러 처리 미들웨어를 연결하지 않아도 기본적으로 익스프레스가 에러 처리를 하지만 실무에선 연결해주는 것이 좋다고 합니다.

 

@콘솔출력값
모든 요청에 다 실행됩니다.
GET / 요청에서만 실행됩니다.
Error: 에러는 에러 처리 미들웨어로 이동합니다.
...

 

외에도 미들웨어를 통해 요청과 응답에 다양한 기능을 추가할 수 있으며, 실무에 자주 사용하는 패키지를 설치해보겠습니다.

 

$ yarn add morgan cookie-parser express-session dotenv

dotenv는 process.env를 관리하기 위해 설치한 것으로 미들웨어는 아닙니다. (나머지는 다 미들웨어)

 

app.ts

import express, { NextFunction, Request, Response } from "express";
import path from "path";
import morgan from "morgan";
import cookieParser from "cookie-parser";
import session from "express-session";
import dotenv from "dotenv";

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!, // Type 'string | undefined' is not assignable to type 'string | string[]'.
    cookie: {
      httpOnly: true,
      secure: false,
    },
    name: "session-cookie",
  })
);

app.use((req: Request, res: Response, next: NextFunction) => {
  console.log("모든 요청에 다 실행됩니다.");
  next();
});

...

 

.env

COOKIE_SECRET=cookiesecret

설치했던 패키지들을 app.use에 연결하였고, req, res, next는 미들웨어 내부에 들어있으므로 생략 가능합니다.

next도 내부적으로 호출하기 때문에 다음 미들웨어로 넘어갈 수 있습니다.

dotenv는 .env 파일을 읽어서 process.env로 만드는 역할을 하며, process.env.COOKIE_SECRET에 cookiesecret 값이 할당됩니다.

보안과 설정의 편의성 때문에 process.env를 별도 파일로 관리합니다.

 

morgan

 

app.use(morgan("dev"));
  • 요청과 응답에 대한 정보를 콘솔에 기록하는 logger
  • app.use(morgan('dev')) 처럼 사용
  • dev 외에 combined, common, short, tiny 등을 넣을 수 있음
모든 요청에 다 실행됩니다.
GET / 요청에서만 실행됩니다.
Error: 에러는 에러 처리 미들웨어로 이동합니다.
...
GET / 500 10.476 ms - 56

GET / 500 10.476 ms - 56이 morgan에 의해 찍힌 로그입니다.

[HTTP 메소드] [주소] [HTTP 상태 코드] [응답 속도] - [응답 바이트]를 의미합니다.

 

static

 

app.use("/", express.static(path.join(__dirname, "public")));
  • 정적인 파일들을 제공하는 라우터 역할
  • 별도의 설치 없이 express 객체 안에서 꺼내 장착
  • app.use('요청 경로', express.static('실제 경로')) 처럼 사용
  • 정적인 파일들이 담겨 있는 폴더를 지정하여 함수의 인수로 넣어줌
  • 예) public/stylesheets/style.css → http://localhost:3000/stylesheets/style.css로 접근 가능
  • 실제 서버의 폴더 경로에는 public이 들어있지만, 요청 주소에는 public이 들어있지 않아 보안에 도움됨
  • 정적 파일들을 알아서 제공해주기 때문에 fs.readFile로 파일을 읽어 전송할 필요가 없음
    요청 경로에 해당하는 파일이 없으면  알아서 내부적으로 next 호출 (파일 발견 시 다음 미들웨어는 실행되지 않음)
body-parser

 

app.use(express.json());
app.use(express.urlencoded({ extended: false }));
  • 요청의 본문에 있는 데이터를 해석해서 req.body 객체로 만들어줌
  • 폼 데이터나 AJAX 요청의 데이터를 처리 (멀티파트 데이터는 처리하지 못함)
  • app.use(express.json()); app.use(express.urlencoded({ extended: false }); 처럼 사용
    • JSON : JSON 형식의 데이터 전달 방식
    • URL-encoded : 주소 형식으로 데이터를 보내는 방식
      extended 옵션이 false면 노드의 querystring 모듈을 이용해 쿼리스트링을 해석
      extended 옵션이 true면 qs 모듈을 사용해 쿼리스트링 해석 (qs: querystring 모듈 기능 확장한 모듈-npm 패키지)
  • 익스프레스에 내장되어 있으므로 별도 설치가 필요 없지만, 버퍼 데이터(Raw) 혹은 텍스트 데이터(Text) 요청을 처리해야 한다면 설치해야 함
  • POST와 PUT 요청의 본문을 전달 받으려면 req.on('data'), req.on('end')를 사용했지만, 해당 미들웨어를 이용하면 내부적으로 스트림을 처리하여 req.body에 추가
cookie-parser

 

app.use(cookieParser(process.env.COOKIE_SECRET));
  • 요청에 동봉된 쿠키를 해석해 req.cookies 객체 생성
  • app.use(cookieParser(비밀키)) 처럼 사용
  • 예) name=zerocho 쿠키를 보냈다면, req.cookies는 { name: 'zerocho' } 
  • 유효 기간이 지난 쿠키는 알아서 걸러냄
  • 서명된 쿠키가 있는 경우, 비밀 키를 통해 해당 쿠키가 내 서버가 만든 쿠키임을 검증할 수 있음
    서명이 붙으면 쿠키가 name=zerocho.sign의 모양을 가짐 / req.signedCookies 객체에 들어있음
  • 쿠키 생성 - res.cookie(키, 값, 옵션)
    쿠키 제거 - res.clearCookie : 키와 값 외에 옵션도 정확히 일치해야 지워지지만, expires나 maxAge는 일치하지 않아도 됨
  • 쿠키 옵션 중 signed를 true로 설정하면 쿠키 뒤에 서명이 붙음 (대부분 켜두는 것이 좋음)
express-session

 

app.use(
  session({
    resave: false,
    saveUninitialized: false,
    secret: process.env.COOKIE_SECRET!, // Type 'string | undefined' is not assignable to type 'string | string[]'.
    cookie: {
      httpOnly: true, // 클라이언트에서 쿠키를 확인 못하게 설정
      secure: false, // https가 아닌 환경에서도 사용 가능하도록 설정 - 배포 시엔 true로 설정하는 것이 좋음
    },
    name: "session-cookie",
  })
);
  • 세션 관리용 미들웨어 (세션을 구현하거나 특정 사용자를 위한 데이터를 임시 저장할 때 유용)
  • 사용자별로 req.session 객체 안에 유지
  • cookie-parser 미들웨어 뒤에 놓는 것이 안전
  • 세션 관리 시 클라이언트에게 쿠키를 보냄
  • 옵션
    • resave : 요청이 올 때 세션에 수정 사항이 생기지 않더라도 세션을 다시 저장할지 설정
    • saveUninitialized : 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정
    • secret : 안전하게 쿠키를 전송하려면 쿠키에 서명을 추가하고, 쿠키를 서명하는 데에 secret 값이 필요하므로 cookie-parser의 secret과 같에 설정하는 것이 좋음
    • name : 세션 쿠키의 이름을 설정 (기본 이름 - connect.sid)
    • cookie : 세션 쿠키에 대한 설정
  • 세션 등록 - req.session.name
  • 세션 아이디 확인 - req.sessionID
  • 세션 모두 제거 - req.session.destroy

 

미들웨어의 특성 활용하기

 

app.use(
    morgan('dev'),
    express.static(path.join(__dirname, 'public')),
    express.json(),
    express.urlencoded({ extended: false}),
    cookieParser(process.env.COOKIE_SECRET)
)

이처럼 동시에 여러 개의 미들웨어를 장착할 수도 있습니다.

내부적으로 next를 호출하지 않는 미들웨어는 res.send나 res.sendFile 등의 메소드로 응답을 보내야 합니다.

express.static은 res.sendFile 메소드로 응답을 보내므로, 정적 파일을 제공하는 경우 아래 미들웨어들은 실행되지 않습니다.

** 미들웨어 장착 순서에 따라 어떤 미들웨어는 실행이 되지 않을 수도 있음!

 

next함수에 인수를 넣을 수도 있습니다.

  • next('route') : 다음 라우터의 미들웨어로 이동
  • next(error) : 에러 처리 미들웨어로 이동

 

미들웨어 간의 데이터를 전달할 수도 있습니다.

app.use((req, res, next) => {
	res.locals.data = '데이터 넣기';
	next();
}, (req, res, next) => {
	console.log(res.locals.data); // 데이터 받기
	next();
}

현재 요청이 처리되는 동안 res.locals 객체를 통해 미들웨어 간에 데이터를 공유할 수 있고, 새로운 요청이 오면 res.locals은 초기화됩니다.

 

미들웨어 안에 미들웨어를 넣는 방식

app.use(morgan('dev'));
// 또는
app.use((req, res, next) => {
	morgan('dev')(req, res, next);
});

기존 미들웨어의 기능을 확장할 수 있기 때문에 유용합니다.

이에 따라 분기처리도 가능합니다.

 

app.use((req, res, next) => {
	if(process.env.NODE_ENV === 'production') {
		morgan('combinded')(req, res, next);
    } else {
		morgan('dev')(req, res, next);
    }
}

 

multer

 

  • 이미지, 동영상 등을 비롯한 여러 가지 파일을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어

 

multipart.html

<form action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="image" />
  <input type="text" name="title" />
  <button type="submit">업로드</button>
</form>

폼을 통해 업로드하는 파일은 body-parser로는 처리할 수 없고, 직접 파싱도 어렵기 때문에 multer 미들웨어를 사용합니다.

 

multer 설치

$ npm i multer

// $ yarn add multer

 

app.ts

...

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: Request, res: Response) => {
  res.sendFile(path.join(__dirname, "multipart.html"));
});

app.post("/upload", upload.single("image"), (req: Request, res: Response) => {
  console.log(req.file);
  res.send("ok");
});

...

 

multipart.html

<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>

 


Router 객체로 라우팅 분리하기

  • 라우터 생성 시 요청 메소드와 주소별로 분기 처리를 하느라 복잡해지는 상황을 익스프레스는 깔끔하게 관리할 수 있음
  • app.get 메소드가 라우터 부분
  • 익스프레스는 라우터를 분리할 수 있는 방법을 제공

routes/index.ts

import express, { Request, Response } from "express";

const router = express.Router();

// GET / 라우터
router.get("/", (req: Request, res: Response) => {
  res.send("Hello, Express");
});

export default router;

 

routes/user.ts

import express, { Request, Response } from "express";

const router = express.Router();

// GET /user 라우터
router.get("/", (req: Request, res: Response) => {
  res.send("Hello User~");
});

export default router;

 

app.ts

...
import indexRouter from "./routes";
import userRouter from "./routes/user";

...

// 라우터 연결
app.use("/", indexRouter);
app.use("/user", userRouter);

app.use((req: Request, res: Response, next: NextFunction) => {
  res.status(404).send("Not Found");
});

...

index.ts와 user.ts는 비슷하지만, app.use로 연결할 때의 차이로 인해 다른 주소의 라우터 역할을 하고 있습니다.

indexRouter는 app.use('/')에 연결했고, userRouter는 app.use('/user')에 연결했습니다.

indexRouter는 use의 '/'와 get의 '/'가 합쳐져 GET / 라우터가 되었고, userRouter는 use의 '/user'와 get의 '/'가 합쳐져 GET /user 라우터가 되었습니다.

app.use로 연결할 때 주소가 합쳐집니다.

Not Found를 응답하는 app.use 미들웨어는 일치하는 라우터가 없을 때 404 상태 코드를 응답하는 역할을 합니다.

 

라우트 매개변수 패턴

router.get('/user/:id', (req, res) => {
	console.log(req.params, req.query);
};

user/1, user/123 요청을 해당 라우터가 처리할 수 있습니다.

req.params.id로 조회할 수 있으며, 반드시 일반 라우터보다 뒤에 위치해야 합니다.

 

주소에 쿼리스트링을 쓸 때도 있습니다.

예) /users/123?limit=5&skip=10 주소 요청

req.params : { id: 123 }
req.query : { limit: 5, skip: 10 }

 

라우터에서 자주 쓰이는 활용법 (app.route나 router.route)

- 주소는 같지만 메소드는 다른 코드가 있을 때 하나로 줄일 수 있음

router.get('/abc', (req, res) => {
	res.send('GET /abc');
});
router.post('/abc', (req, res) => {
	res.send('POST /abc');
})

해당 코드를 아래로 변경 가능

router.route('/abc')
	.get((req, res) => {
		res.send('GET /abc');
    }),
	.post((req, res) => {
		res.send('POST /abc');
    });

 


req, res 객체 알아보기

  • 익스프레스의 req, res 객체는 http 모듈의 req, res 객체를 확장한 것
  • 기존 http 모듈의 메소드 사용 및 익스프레스가 추가한 메소드나 속성 사용 가능

자주 쓰는 객체 아래 링크 참조!

https://velog.io/@hi4190/reqres-%EA%B0%9D%EC%B2%B4-%EC%82%B4%ED%8E%B4%EB%B3%B4%EA%B8%B0

 

req,res 객체 살펴보기

req.app:req객체를 통해 app객체에 접근할수있다 req.app.get('port')와 같은식으로 사용req.body : body-parser 미들웨어가 만드는 요청의 본문을 해석한것req.cookies: cookie-parser 미들웨어가 만드는 요청의 쿠키

velog.io