관리 메뉴

진취적 삶

06 익스프레스 웹 서버 만들기 본문

개발 도서/Node.js 교과서

06 익스프레스 웹 서버 만들기

hp0724 2023. 7. 14. 11:43

익스프레스 : npm 서버 제작과정을 편하게 해주는 웹 서버 프레임워크

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

npm init -y는 모든 값을 기본값으로

{
    "name": "learn_express",
    "version": "0.0.1",
    "description": "익스프레스를 배우자 ",
    "main": "app.js",
    "scripts": {
      "start": "nodemon app"
    },
    "author": "suhaHwang",
    "license": "MIT",
    "dependencies": {
      "express": "^4.18.2"
    }
  }

start: ‘nodemon app’ 은 서버코드를 수정하면 nodemon이 서버를 자동으로 재시작

nodemon이 실행되는 콘솔에 rs를 입력해서 수동으로 재시작할수도 있다.

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

npm i -D nodemon 은 nodemon을 개발의존성으로 설치하는것

-D 플래그는 --save-dev 의 축약형이다.

const express = require("express");
const path = require("path");
//express 내부에 http 모듈 내장
const app = express();
//서버가 실행될 포트 설정
app.set("port", process.env.PORT || 3000);
//주소에 대한 get요청이 올때 어떤 동작을할거?
app.get("/", (req, res) => {
  res.sendFile(path.join(__dirname, "./index.html"));
});

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

6.2 자주 사용하는 미들웨어

미들웨어 : 요청과 응답의 중간에 위치

미들웨어는 요청과 응답을 조작해 기능을 추가하기도하고 나쁜 요청을 걸러내기도 한다.

const express = require("express");
const path = require("path");
//express 내부에 http 모듈 내장
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"), "번 포트에서 대기중 ");
});

app.use 에 매개변수가 req,res,next 인 함수를 넣으면 된다.

미들웨어는 위에서부터 아래로 순서대로 실행

next는 다음 미들웨어로 넘어가는 함수 next를 실행하지 않으면 다음 미들웨어가 실행되지 않는다.

주소를 첫번쨰 인수로 넣어주지 않으면 모든 요청에서 실행 ,주소를 넣으면 해당하는 요청에서만 실행

app.use(미들웨어) 모든 요청에서 미들웨어 실행

app.use(’/abc’,미들웨어) abc로 시작하는 요청에서 미들웨어 실행
app.post(’/abc’,미들웨어) abc로 시작하는 post요청에서 미들웨어 실행

app.use 나 app.get 같은 라우터에 미들웨어 여러개 장착 가능 다만 next 를 호출해야 다음 미들웨어로 넘어갈수있음

app.use((err, req, res, next) 에러 처리 매개변수는 반드시 4개여야 한다.

실무에서는 직접 에러 처리 미들웨어를 연결해주는것이 좋다.

에러처리는 가장 아래에 위치하도록 한다.

dotenv 패키지는 . + env이다.

process.env 을 별도의 파일로 관리하는 이유는 보안과 설정의 편의성 때문이다. 소스코드가 유출되더라도 .env 파일만 잘 관리하면 비밀 키는 지킬수있다.

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();
//express 내부에 http 모듈 내장
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((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"), "번 포트에서 대기중 ");
});

6.2.1 morgan

GET / 500 7.758 ms - 47 morgan 미들웨어 나오는 것

app.use(morgan("dev")); 

인수로 combined, common ,short,tiny,dev, 등등이 있다 .

개발환경에서는 dev 배포환경에서는 combined를 저자는 애용

GET / 500 7.758 ms - 47 은 각각 [HTTP 메서드] [주소] [HTTP 상태 코드] [응답 속도] - [응답 바이트]

6.2.2 static

static 미들웨어는 정적인 파일들을 제공하는 라우터 역할을한다.

app.use('요청 경로'. express.static('실제 경로') )
app.use("/", express.static(path.join(__dirname, "public")));

실제 경로에는 public이 들어가있지만 요청 주소에는 public이 들어가 있지않아서 서버의 구조를 쉽게 파악할수 없다.

요청 경로에 해당 파일이 없으면 알아서 내부적으로 next 호출

6.2.3 body-parser

요청의 본문에 있는 데이터를 해석해서 req.body 객체로 만들어주는 미들웨어 보통 폼 데이터나 AJAX 요청의 데이터를 처리한다.

app.use(express.json());
app.use(express.urlencoded({ extended: false }));

body-parser 는 JSON 과 URL-encoded 형식의 데이터 외에도 Raw, Text 형식의 데이터를 추가로 해석 Raw는 요청의 본문이 버퍼 데이터 Text는 텍스트 데이터 일때 해석하는 미들웨어

express.json()은 JSON 형식의 데이터 전달방식

express.urlencoded 는 주소 형식으로 데이터를 보내는 방식

app.use(express.urlencoded({ extended: false })); 의 경우 옵션이 false 이면 노드의 querystring 모듈을 사용해 쿼리스트링을 해석, true 이면 qs 모듈을 사용해 쿼리스트링 해석

6.2.4 cookie-parser

cookie-parser는 요청에 동봉된 쿠키를 해석해 req.cookies 객체로 만든다.

서명된 쿠키는 req.cookies 대신 req.signedCookies에 들어간다.

app.use(cookieParser (비밀 키))
app.use(cookieParser(process.env.COOKIE_SECRET));

cookie-parser 은 쿠키를 생성할때 쓰이는것이 아니다 쿠키를 생성/제거 하려면 res.cookie, res.clearCookie 메서드를 사용해야한다.

res.cookie('name','suhahwang',{
	expires:new Date(Date.now()+900000),
	httpOnly:true,
	secure:true,
})
res.clearCookie('name','suhahwang',{httpOnly:true,secure:true});

**httpOnly**와 **secure**옵션은 보안을 강화하는 옵션입니다. httpOnly 옵션을 사용하면 JavaScript를 통해 쿠키에 접근하는 것을 막을 수 있습니다. secure 옵션을 사용하면 HTTPS 프로토콜을 사용하는 경우에만 쿠키를 전송할 수 있습니다.

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 는 세션에 저장할 내역이 없더라도 처음부터 세션 생성할지 결정하는것
  • secret: process.env.COOKIE_SECRET 쿠키 서명
  • cookie는 세션 쿠키에 대한 설정
  • store 에는 데이터베이스를 연결하자

6.2.6 미들웨어의 특성 활용하기

미들웨어는 req,res,next를 매개변수로 갖는 함수로서 app.use 나 app.get 등으로 장착

특정한 주소의 요청에만 미들웨어가 실행되게 하려면 첫번쨰 인수로 주소를 넣으면된다.

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));ㅁ

동시에 여러개의 미들웨어를 장착할수도 있다. 다음 미들웨어로 넘어가려면 next를 호출해야 하지만 위의 미들웨어들은 내부적으로 next를 호출하고 있다. next를 호출하지 않는 미들웨어는 res.send 나 res.sendFile 등의 메서드로 응답을 보내야 한다.

next도 호출하지 않고 응답도 보내지 않으면 클라이언트는 응답을 받지못해 하염없이 기달려야함

res.locals 객체에 데이터를 넣어두면 요청이 끝날때까지 데이터를 유지한다.

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

app.set 또한 익스프레스에서 데이터를 저장할수 있지만 app.set은 익스프레스 전역적으로 사용되므로 하나의 요청 안에서만 유지되어야 하는 값에는 부적절하다.

res.locals 객체는 하나의 요청안에서만 유지된다.

6.2.7 multer

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

멀티파트 형식이란 enctype 이 multipart/form-data인 폼을 통해 업로드하는 데이터 형식

<form action="/upload" method="post" enctype="multipart/form-data">
      <input type="file" name="image" />
      <input type="text" name="title" />
      <button type="submit">업로드</button>
</form>
const multer = require("multer");
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 },
});

destination 과 filename 함수의 req 매개변수는 요청에 대한 정보 file 객체에는 업로드한 파일에 대한 정보

done 매개변수는 함수이다 첫번째 인수에는 에러가 있으면 에러 넣고 두번째 인수에는 실제 경로나 파일이름을 넣으면 된다.

uploads 라는 폴더에 [파일명 +현재시간 .확장자] 파일명으로 업로드

limits 속성에는 업로드에 대한 제한 사항

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

여러 파일을 업로드 할때는 multiple 작성

const multer = require("multer");
const fs = require("fs");
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();
//express 내부에 http 모듈 내장
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",
  })
);

try {
  fs.readFileSync("uploads");
} catch (error) {
  if (!fs.existsSync("uploads")) {
    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.3 Router 객체로 라우팅 분리하기

next(route)를 호출하는 경우 주소와 일치하는 다음 라우터로 넘어간다.

const express = require("express");
const router = express.Router();

//GET / 라우터
router.get(
  "/",
  (req, res, next) => {
    next("route");
  },
  (req, res, next) => {
    console.log("실행 안됨");
    next();
  },
  (req, res, next) => {
    console.log("실행 안됨");
    next();
  }
);
router.get("/", (req, res) => {
  console.log("실행돰");
  res.send("hello");
});

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

주소는 같지만 메서드는 다른 코드가 있을때 이를 하나의 덩어리로 만든다.

6.4 req,res 객체 살펴보기

req 객체

  • req.app : req 객체를 통해 app 객체에 접근할수 있다. req.app.get(’port’)
  • req.body :body-parser 미들웨어가 만드는 요청의 본문을 해석한 객체
  • req.cookies :cookies-parser 미들웨어가 만드는 요청의 쿠키를 해석한 객체
  • req.ip :요청의 ip 주소가 담겨 있다.
  • req.params : 라우트 매개변수에 대한 정보가 담긴 객체
  • req.query : 쿼리스트링에 대한 정보가 담긴 책
  • req.signedCookies : 서명된 쿠키들은 req.cookies 대신 여기에 담겨있다.
  • req.get (헤더 이름) :헤더의 값을 가져오고 싶을 때 사용하는 메서드

res 객체

  • res.app :res 객체를 통해 app 객체이 접근할수 있다.
  • res.cookie(키,값,옵션) :쿠키를 설정하는 메서드
  • res.clearCookie(키,값,옵션) :쿠키를 제거하는 메서드
  • res.end() :데이터 응답없이 보냄
  • res.json(JSON) :JSON 형식의 응답을 보냄
  • res.locals : 하나의 요청 안에서 미들웨어 간에 데이터를 전달하고싶을때 사용
  • res.redirect (주소) :리다이렉트할 주소와 함께 응답을 보낸다
  • res.render(뷰,데이터) : 다음 절에서 다룰 템플릿 엔진을 렌더링해서 응답할때 사용하는 메서드
  • res.send(데이터) : 데이터와 함께 응답을 보낸다. 데이터는 문자열일수도,HTML일수도 버퍼일수도,객체나 배열이수도 있다.
  • res.sendFile(경로) : 경로에 위치한 파일을 응답한다.
  • res.set(헤더 ,값) : 응답의 헤더를 설정
  • res.status(코드) : 응답 시의 HTTP 상태 코드를 지정

req 나 res 둘다 메서드 체이닝 가능

res.status(201).cookie('test','test').redirect('/admin')

6.5 템플릿 엔진 사용하기

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

대표적인 템플릿 엔진인 퍼그와 넌적스를 살펴보자

6.5.1 퍼그(제이드)

app.set("views", path.join(__dirname, "views"));
app.set("view engine", "pug");

views는 템플릿 파일들이 위치한 폴더를 지정

view engine은 어떠한 종류의 템플릿 엔진을 사용하지 나타낸다

6.5.1.1 HTML 표현

HTML 과 다르게 화살괄호와 닫는 태그가 없다.

탭 또는 스페이스로만 태그의 부모 자식 관계를 규명한다.

자식 태그는 부모 태그보다 들여쓰기 되어야 한다.

body 
        #login-button
        .post-image 
        span#highlight 
        p.hidden.full 
        p welcome to Express 
        button(type='submit')  전송 
        P 
            | 안녕하세요 
            | 여러줄을 입력합니다

6.5.1.2 변수

자바스크립트 변수를 템플릿에 랜더링 할수있다.

//GET / 라우터
router.get("/", (req, res, next) => {
  res.render("index", { title: "Express" });
});
 router.get("/", (req, res, next) => {
  res.locals.title='Express'
 res.render('index')
 }); 

변수 사용법

h1= title 
        p Welcome to #{title}
        button (class =title,type='submit')전송
        input(placeholder = title+' 연습')
- const node ='node js '
        - const js ='JavaScript'
        p #{node}와 #{js}

6.5.1.3 반복문

ul 
            each fruit in ['사과', '배', '오렌지']
            li fruit

            each fruit, index in ['사과', '배', '오렌지']
            li = (index +1) +'번째'+fruit

6.5.1.4 조건문

if lsLoggedIn 
            div 로그인 되어있다. 
        else 
            div 로그인이 필요

6.5.1.5 include

header, footer ,네이비게이션 처럼 웹을 제작할때 공통되는 부분을 따로 관리할수있다.

6.5.1.6 extends 와 block

레이아웃을 정할수 있고 공통되는 레이아웃 부븐을 따로 관리할수 있다.

doctype html
html(lang="en")
    head
         title =title 
         link(rel="stylesheet", href="style.css")
         block style 
    body 
        header 헤더입니다
        block content 
        footer 푸더입니다
        block script
extends ex1.pug 

block content 
    main 
        p 내용입니다 

block script 
    script (src ='/main.js')

페이지 마다 달라지는 부분을 block으로 비워둔다.

'개발 도서 > Node.js 교과서' 카테고리의 다른 글

08 몽고디비  (0) 2023.07.14
07 MySQL  (0) 2023.07.14
05 패키지 매니저  (0) 2023.07.14
04 http 모듈로 서버 만들기  (0) 2023.07.14
03 노드 기능 알아보기  (0) 2023.07.14