6 익스프레스 웹 서버 만들기
source: categories/study/nodejs/nodejs6.md
6.1 express 서버 사용해보기
express
를 써보면 다시는 http
로 안 돌아갑니다.
물론 express
가 제일 나은 대안이라는 것은 아니고 express
의 경쟁자도 많습니다.
Koa
, fastify
, restify
, Hapi
..
그리고 express
위에서 돌아가는 또는 express
보다 더 많은 것을 갖춘 loopback
, adonis
, strapi
.. 이런 것들이 있습니다.
그래도 그 중에서 가장 많이 쓰이는 것은 express
입니다.
6.1.1 Express 소개
-
http
모듈로 웹 서버를 만들 때 코드가 보기 좋지 않고, 확장성도 떨어짐- 프레임워크로 해결
- 대표적인 것이
express
(익스프레스),koa
(코아),hapi
(하피) -
코드 관리도 용이하고 편의성이 많이 높아짐
-
위에 그래프를 보시면
express
가 압도적으로 다운로드 횟수가 많다는 것을 볼 수 있습니다.
npm에 올라와있는 패키지만 100만개가 넘다보니까 어떤게 유명한지 헷갈리잖아요?
이럴 때는 위npm trends
사이트에서 다운로드 수 많은 패키지를 설치하는 것이 안전합니다.
다운로드 횟수와 updated(최근 활동 날짜)를 보시며 결정하시는 것이 좋습니다.그리고 한 가지 더 보시면 좋은 것이
깃헙 들어가셔서 위와 같이 최근 커밋 시점 보는거?
최근 활동 기록이 1년이 넘었다고하면 그러면 조금 의심을 해보셔야됩니다.
여튼 express가 노드에선 압도적으로 많이 사용됩니다.
많이 사용된다고 가장 좋다고는 말씀 못드리겠지만 초보자분들은express
사용하시는게 좀 편하실겁니다.
-
질문: Ember는 요즘 사용 안하나요?
SPA(Single Page Application) 1세대로 Angular1, Ember, Backbone 이렇게 있는데 지금은..
react가 2세대이고 vue가 2.5세대이고 svelt 3세대 이렇게 나오고 있거든요?
그래서 사실상 1세대들은 경쟁상대가 안됩니다.
Angular1, Ember, Backbone 이런 애들은 지금 비빌 수가 없습니다.
6.1.2 package.json 만들기
-
직접 만들거나
npm init
명령어 생성-
nodemon이 소스 코드 변경시 서버를 재시작해줌
{ "name": "learn-express", "version": "0.0.1", "scripts": { "start": "nodemon app" }, "devDependencies": { "nodemon": "^2.0.7" }, "dependencies": { "express": "^4.17.1" } }
-
6.1.3 app.js 작성하기
-
서버 구동의 핵심이 되는 파일
app.set("port", 포트)
로 서버가 실행될 포트 지정app.get("주소", 라우터)
로 GET 요청이 올 때 어떤 동작을 할지 지정-
app.listen(포트, 콜백)
으로 몇 번 포트에서 서버를 실행할지 지정// app.js const express = require("express"); const app = express(); // 서버가 실행될 포트 지정 app.set("port", process.env.PORT || 3000); // / 주소로 GET 요청이 들어올 때 어떤 동작을 할지 지정 app.get("/", (req, res) => { res.send("Hello, Express"); }) // 몇번 포트에서 서버를 실행할지 지정 app.listen(app.get("port"), () => { console.log(app.get("port"), "번 포트에서 대기 중"); })
지금까진 저희가 서버를 만들 때 const http = require("http");
이런식으로 http
를 임포트했었는데, 이제는 http
가 아니라 위에서 설치한 express
를 임포트합니다.
// app.js
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("hello express");
})
app.listen(3000, () => {
console.log("익스프레스 서버 실행");
});
이 express
가 뭔가 마법을 부려서 http
서버가 실행되는 것이 아니라 설치한 express
의 코드를 보시면 그 안에서 http
서버를 사용하고 있습니다.
http
서버를 사용하고있는 express
를 저희가 가져다 쓰는 것이기 때문에 저희도 마찬가지로 http
를 사용하고 있는 것이죠.
node app
실행은 위 명령어로.
지금까진 index
를 많이 썼는데 express
는 보통 app
이라고 많이 하더라구요.
위 코드에서 주목해야되는 부분은 이 부분입니다.
app.get("/", (req, res) => {
res.send("hello express");
})
우리가 앞서 http 예시 코드를 봤을 땐 아래처럼 http.createServer
함수 안에서 분기처리가 되어있었습니다.
http.createServer(async (req, res) => {
try {
// 즉, HTTP 메소드가 GET이고
if (req.method === 'GET') {
// url이 "/" 슬래쉬일 때
if (req.url === '/') {
// 이때는 restFront.html을 읽어서 전달해줘라. 이 의미가 되는 겁니다.
const data = await fs.readFile('./restFront.html');
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
return res.end(data);
} else if (req.url === '/about') {
const data = await fs.readFile('./about.html');
res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
return res.end(data);
} else if (req.url === '/users') {
res.writeHead(200, {'Content-Type': 'application/json; charset=utf-8'});
return res.end(JSON.stringify(users));
}
// /도 /about도 /users도 아니면
try {
const data = await fs.readFile(`.${req.url}`);
return res.end(data);
} catch (err) {
// 주소에 해당하는 라우트를 못 찾았다는 404 Not Found error 발생
}
} else if (req.method === 'POST') {
if (req.url === '/user') {
let body = '';
// 요청의 body를 stream 형식으로 받음
req.on('data', (data) => {
body += data;
});
// 요청의 body를 다 받은 후 실행됨
return req.on('end', () => {
console.log('POST 본문(Body):', body);
const {name} = JSON.parse(body);
const id = Date.now();
users[id] = name;
res.writeHead(201, {'Content-Type': 'text/plain; charset=utf-8'});
res.end('ok');
});
}
} else if (req.method === 'PUT') {
if (req.url.startsWith('/user/')) {
const key = req.url.split('/')[2];
let body = '';
req.on('data', (data) => {
body += data;
});
return req.on('end', () => {
console.log('PUT 본문(Body):', body);
users[key] = JSON.parse(body).name;
res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
return res.end('ok');
});
}
} else if (req.method === 'DELETE') {
if (req.url.startsWith('/user/')) {
const key = req.url.split('/')[2];
delete users[key];
res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
return res.end('ok');
}
}
res.writeHead(404);
return res.end('NOT FOUND');
} catch (err) {
console.error(err);
res.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8'});
res.end(err.message);
}
})
하지만 express
를 사용하면
app.get("/", (req, res) => {
res.send("hello express");
})
app.post("/", (req, res) => {
res.send("hello express");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
이렇게 작성할 수 있습니다.
즉, 더 이상 if
문으로 도배를 하지 않아도 됩니다.
위와 같이 app
에다 메소드를 붙여주는 방식으로 구별이 가능합니다.
이러면 좀 더 코드 관리가 편해지겠죠?
위의 코드를 조금만 더 수정해보자면,
// app.js
const express = require("express");
const app = express();
// 아래 코드는 프로퍼티를 설정한다고 보시면 됩니다.
// 서버에 port라는 프로퍼티명에 3000이라는 프로퍼티값을 넣는다고 생각하시면 됩니다.
// 그리고 저희가 process.env를 적극적으로 활용할건데요,
// 기본적으로 포트는 process.env.PORT 이걸 입력하지 않으면 3000으로 될테지만, 혹시나 저희가 포트를 바꾸고싶다면,
app.set("port", process.env.PORT || 3000);
app.get("/", (req, res) => {
res.send("hello express");
})
app.post("/", (req, res) => {
res.send("hello express");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
// 그럼 아래와 같이 port에 할당된 값을 가져와 사용할 수 있습니다.
// 즉 위의 app.set("port", 3000)이 어떻게보면 전역에서 사용할 수 있는걸 설정했다고 보시면됩니다.
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
기본적으로 포트는 process.env.PORT 이걸 입력하지 않으면 3000으로 될테지만, 혹시나 저희가 포트를 바꾸고싶다면,
SET PORT=80
위와 같이 명령어를.. 근데 위 명령어는 사용 안하는 것이 좋습니다.
위 명령어를 사용하면 기본적으로 포트가 80으로 바뀌기 때문에 다른 프로그램을 실행할 때 문제가 발생할 수도 있기 때문에 웬만하면 위 명령어 방식은 사용하지 마시고 위와 같은 방식으로 할 수도 있다는 것만 알아두시면 됩니다.(3강에서 배운 내용)
노드 프로젝트에서 process.env.PORT
설정 방법은 따로 알려드리겠습니다.
nodemon
그리고 저희가 nodemon
패키지를 설치했는데 nodemon
패키지를 통해서 이 서비스를 실행해볼거거든요?
방금 전에는
node app
이렇게 서버를 실행했잖아요?
그런데 이번에는
nodemon app
이라고 명령어를 입력할겁니다.
개발할 때는 보통 nodemon
서버를 많이 씁니다.
저희가 get
메소드로 안 만들어놓은 요청은 express
에서 알아서 404
에러를 띄워줍니다.
물론 서버쪽 에러도 알아서 500
번대 에러를 띄워줍니다.
이런 기본적인 것들이 갖춰져있고 아까 말씀드렸다시피 if
문을 사용하지 않아도 분기처리가 쉽도록 되어있습니다.
여튼 위의 nodemon
으로 서버를 실행하면, app.js
코드를 수정하였을 때 뭔가를 실행해줍니다.
restarting due to changes...
서버를 재시작을 해주는 겁니다.
원래는 저희가 소스코드를 수정하면 서버를 종료했다가 다시 실행해줘야하는데 이게 너무 귀찮거든요? 그리고 자주 까먹습니다.
그런데 nodemon
을 통해 실행하면 알아서 현재 폴더(프로젝트 폴더)의 파일들이 바뀌는지를 검사해서 이렇게 알아서 재시작을 해줍니다.
package.json
에 scripts
부분에 명시한 명령어에 의해서 npm start
로도 실행이 가능합니다.
npm 패키지(모듈) 버전 확인
- npm outdated
- npm ls 패키지명
6.1.4 서버 실행하기
- app.js: 핵심 서버 스크립트
- public: 외부에서 접근 가능한 파일들 모아둠
- views: 템플릿 파일을 모아둠
-
routes: 서버의 라우터와 로직을 모아둠
- 추후에 models를 만들어 데이터베이스 사용
6.1.5 익스프레스 서버 실행하기
- npm start(package.json의 start 스크립트) 콘솔에서 실행
6.2 express로 html 서빙하기
-
res.sendFile
로 HTML 서빙 가능http
서버로 실습할 때 서버를 실행한 다음에html
파일을 제공했었음.
그거처럼express
에서도html
파일을 제공할 수 있음.<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <title>익스프레스 서버</title> </head> <body> <h1>익스프레스</h1> <p>배워봅시다.</p> </body> </html>
// app.js const express = require("express"); // 경로를 좀 더 확실하게 하기 위해 path 모듈 사용 const path = require("path"); const app = express(); app.set("port", process.env.PORT || 3000); app.get("/", (req, res) => { // http와 다르게 fs 모듈은 사용 안해도됨 // 아래와 같이 sendFile 메소드를 사용 res.sendFile(path.join(__dirname, "index.html")); }) app.post("/", (req, res) => { res.send("hello express"); }) app.get("/about", (req, res) => { res.send("hello express"); }) app.listen(app.get("port"), () => { console.log("익스프레스 서버 실행"); });
이렇게 간단하게 html 서빙도 가능합니다.
한 서버에 대해 한번 알고 있으면 다른 서버.. 서버는 언어가 별로 안 중요하고 서버는 이론이 더 중요하기 때문에.. 서버는 HTTP에 대해 이해하는 것이 더 중요합니다.
HTTP와 운영체제, 데이터베이스, 네트워크 이런 기반 지식이 더 중요하고 언어는 솔직히 안 중요합니다. -
npm ls
는 뭔가요?제 프로젝트에서
npm ls express
를 입력하면express
패키지가 쓰이고 있는지 알아보는 명령어입니다.위와 같이 뜨면 cookie-parser 패키지는 사용 안하고 있는 것이죠.
-
아니,
package.json
에 명시되어있는거 보면 되는거 아닌가요?node_modules
폴더를 보면 실제로 명시되어있는 것보단 훨씬 많이 설치되잖아요?위와 같이 그런 패키지들간의 의존성까지 검색을 해주기 때문에 저희 프로젝트에서 어떤 패키지가 쓰이고있는지
npm ls
명령어로 알 수 있습니다.
ls
가 헷갈리시면npm list
라고 치셔도 됩니다. -
npm ll
이라는 명령어로 좀 더 자세하게 볼 수도 있습니다.
6.2.1 nodemon의 감시기능
nodemon은 html 파일을 감시할 필요가 없습니다.
그 이유는
// app.js
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// GET / 요청이 들어오면 아래 코드가 실행되잖아요?
app.get("/", (req, res) => {
// 그럼 index.html 요청 받을 때마다 그때그때 index.html을 다시 보내주기 때문에
// nodemon 자체는 html을 감시하고 있을 필요는 없습니다.
// 그런데 이 app.js 파일 같은 경우는 노드에서 한번 실행하면 캐시에 저장하고 있는 형태잖아요? require.cache 안에 들어있는..
// 그 캐시에 저장된 내용을 계속 쓰기 때문에 바뀌지 않습니다.
// require.cache를 직접 지워주기 전까진 안 바뀌는데 require.cache를 직접 지워주는 것이 위험하기 때문에 저희는 서버를 껐다가 재시작한다고 그랬죠?
// 껐다가 재시작하면 메모리(캐시)가 날아가는 효과가 있으니깐.
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
그런데 감시를 원한다면 nodemon도 설정 파일이 있거든요?
위 사이트를 참고해서 설정파일로 html 파일도 감시하도록 만들 수 있습니다.
6.3 미들웨어 사용하기
express
에서 제일 중요한 미들웨어를 해볼건데,
// app.js
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// 아래와 같이 get /, post /, get /about 이렇게 주소를 나눠서 처리하면 코드가 깔끔해지겠죠?
// 코드가 깔끔해지지만 아래와 같이 분리하면 좋은점도 있지만 나쁜점도 있습니다.
// 아래와 같이 분리하면, 만약 공통적인 코드가 있을 때
// 아래와 같이 중복이 발생합니다.
// 이런걸 제거하기 위해 미들웨어라는 개념이 나옵니다.
app.get("/", (req, res) => {
console.log("모든 요청에 실행하고 싶어요.");
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
console.log("모든 요청에 실행하고 싶어요.");
res.send("hello express");
})
app.get("/about", (req, res) => {
console.log("모든 요청에 실행하고 싶어요.");
res.send("hello express");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
6.3.1 미들웨어
-
익스프레스는 미들웨어로 구성됨
- 요청과 응답의 중간에 위치하여 미들웨어
app.use(미들웨어)
로 장착- 위에서 아래로 순서대로 실행됨
- 미들웨어는
req
,res
,next
가 매개변수인 함수 req
: 요청,res
: 응답 조작 가능-
next()
로 다음 미들웨어로 넘어감// app.js const express = require("express"); const path = require("path"); const app = express(); app.set("port", process.env.PORT || 3000); // 아래 app.use는 모든 요청이 들어오는 코드에서 실행이 됩니다. app.use((req, res, next) => { console.log("모든 요청에 실행하고 싶어요."); }) app.get("/", (req, res) => { res.sendFile(path.join(__dirname, "index.html")); }) app.post("/", (req, res) => { res.send("hello express"); }) app.get("/about", (req, res) => { res.send("hello express"); }) app.listen(app.get("port"), () => { console.log("익스프레스 서버 실행"); });
위와 같이 서버에 요청을 보낼 때마다
app.use
에 있는 코드가 실행됩니다.
그런데 문제가 모든 라우터에 실행되는 거..
위의 코드에서 app.get
, app.post
이런거 하나하나를 라우터라고 부릅니다.
즉, 메소드(app, get..)와 주소("/", "/about", …)가 있는 애들을 라우터라고 부릅니다.
그런데 위의 움짤을 보면 미들웨서인 app.use
는 실행이 되는데 라우터는 실행이 안됩니다.(로딩창이 계속 돌아가기만 합니다.)
왜냐하면 app.use
이 부분을 실행했다고 해서 그 다음 부분을 알아서 실행하는 것이 아니라 3번째 인자인 next
를 넣어줘야됩니다.
// app.js
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// 아래 app.use는 모든 요청이 들어오는 코드에서 실행이 됩니다.
app.use((req, res, next) => {
console.log("모든 요청에 실행하고 싶어요.");
// 이렇게 3번째 인자인 next를 실행해줘야 다음 라우터 함수들 중에 일치하는 곳을 찾아갑니다.
next();
})
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
express
기본적으로 위에서 아래로 실행되지만 미들웨어들은 next
를 해줘야만 다음 코드로 넘어갑니다.
아주 중요한 부분이니 꼭 기억!!
next()
를 넣어주니깐 이제 미들웨어를 거치고 다음 코드가 실행되어 html 파일이 제대로 서빙됩니다.
이렇게 미들웨어 실행 후, 요청에 맞는 라우터가 실행이 됩니다.
라우터 매개변수
그런데 코드가 위에서 아래로 실행된다는 의미를 잘 아셔야되는게, 예를 들어 아래와 같은 와일드 카드가 있습니다.
정확한 용어는 라우트 매개변수입니다. (route parameter
라서 req.params
)
// app.js
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// 아래 app.use는 모든 요청이 들어오는 코드에서 실행이 됩니다.
app.use((req, res, next) => {
console.log("모든 요청에 실행하고 싶어요.");
// 이렇게 3번째 인자인 next를 실행해줘야 다음 라우터 함수들 중에 일치하는 곳을 찾아갑니다.
next();
})
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
// 위에서 아래로 실행되는 것이 중요한 이유가 아래와 같은 와일드 카드(라우트 매개변수)가 있어서..
// 예를 들어 저희가 서버로 /category/javascript/ 이런 요청을 보내는 경우도 있잖아요?
// 이렇게 카테고리별로 라우터를 만들고 싶다. 그런데 이런 카테고리들이 수백개가 된다면?
// 수백개 다 만들 자신이 있으신가요?
app.get("/category/javascript", (req, res) => {
res.send("hello javascript");
})
app.get("/category/node", (req, res) => {
res.send("hello node");
})
app.get("/category/react", (req, res) => {
res.send("hello react");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
위와 같이 라우터를 수백가 만들 자신이 있으신가요?
이럴 때 아래와 같이..
// app.js
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// 아래 app.use는 모든 요청이 들어오는 코드에서 실행이 됩니다.
app.use((req, res, next) => {
console.log("모든 요청에 실행하고 싶어요.");
// 이렇게 3번째 인자인 next를 실행해줘야 다음 라우터 함수들 중에 일치하는 곳을 찾아갑니다.
next();
})
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
// 그런 경우에 아래와 같이 와일드 카드(라우터 매개변수)를 넣어주는데
// :name이 javascript라고 한다면 ${req.params.name}에 javascript가 들어갈 것입니다.
app.get("/category/:name", (req, res) => {
res.send(`hello ${req.params.name}`);
})
app.get("/about", (req, res) => {
res.send("hello express");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
이런식으로 와일드 카드(라우터 매개변수)를 사용할 수 있습니다.
이렇게 와일드 카드(라우터 매개변수)를 보여드린 이유가 예를 들어, 실수로 와일드 카드 코드를 위에다가 올려놓잖아요?
// app.js
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// 아래 app.use는 모든 요청이 들어오는 코드에서 실행이 됩니다.
app.use((req, res, next) => {
console.log("모든 요청에 실행하고 싶어요.");
// 이렇게 3번째 인자인 next를 실행해줘야 다음 라우터 함수들 중에 일치하는 곳을 찾아갑니다.
next();
})
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
// 아래와 같이 와일드카드(라우터 매개변수) 코드가 있는데 그 아래 category/javascript가 있다면,
app.get("/category/:name", (req, res) => {
res.send("hello wildcard");
})
app.get("/category/javascript", (req, res) => {
res.send("hello javascript");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
위와 같이 "hello wildcard"가 나와버립니다.
이게 위에서부터 아래로 실행되기 때문에 그렇습니다.
- 전체 공통 미들웨어 하나 실행되고
-
요청에 맞는 라우터가 실행되는데, category/:name 라우터에서 걸려버림
와일드 카드니까 javascript가 :name 부분에 들어가버림.
그리고 그 다음 category/javascript 라우터는 실행이 안됨.
그 이유는 next를 안해줬으니깐.그리고 보통 라우터같은 경우는
res.send()
메소드를 실행하면 이 부분에서 끝내줘야됩니다.
이 다음 미들웨어로 넘어가면 에러가 나는 경우가 많음.여튼 와일드 카드(라우터 매개변수) 부분이 실행되고 그 아래는 실행이 안됨.
그래서 와일드 카드(라우터 매개변수)는 다른 라우터들 보다 코드가 아래에 위치해야됨.
// app.js
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// 아래 app.use는 모든 요청이 들어오는 코드에서 실행이 됩니다.
app.use((req, res, next) => {
console.log("모든 요청에 실행하고 싶어요.");
// 이렇게 3번째 인자인 next를 실행해줘야 다음 라우터 함수들 중에 일치하는 곳을 찾아갑니다.
next();
})
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
app.get("/category/javascript", (req, res) => {
res.send("hello javascript");
})
app.get("/category/:name", (req, res) => {
res.send("hello wildcard");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
그럼 category/javascript 요청을 보내면 hello javascript가 나오고
category/node 요청을 보내면 hello wildcard가 나옵니다.
애스터리스크 *
// app.js
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// 아래 app.use는 모든 요청이 들어오는 코드에서 실행이 됩니다.
app.use((req, res, next) => {
console.log("모든 요청에 실행하고 싶어요.");
// 이렇게 3번째 인자인 next를 실행해줘야 다음 라우터 함수들 중에 일치하는 곳을 찾아갑니다.
next();
})
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
app.get("/category/javascript", (req, res) => {
res.send("hello javascript");
})
app.get("/category/:name", (req, res) => {
res.send("hello wildcard");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
// 와일드 카드와 비슷한게 아래와 같이 애스터리스크가 있거든요?
// 얘는 모든 get 요청, 어떠한 요청이던지 다 처리하겠다 라는 뜻임
// 그럼 당연히 아래 애스터리스크 코드가 위에 있으면 난리나겠죠?
// 아래 코드가 제일 위에 있는 상태에서 get / 요청을 했다고 한다면 그럼 애스터리스크 코드에서 모든게 끝나버릴 겁니다.
// 그 다음으론 넘어가지도 않을 겁니다.
app.get("*", (req, res) => {
res.send("hello everybody");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
애스터리스크에서 다 끝나버리고 그 아래는 실행도 되지 않습니다.
라우터 작성 주의
이래서 애스터리스크 같은 라우터나 범위가 넓은 라우터들은 코드 아래쪽에 넣어줘야된다는 것.
6.4 미들웨어 특성 이해하기
- 정확한 미들웨어의 뜻 -
앞서 미들웨어라고 말씀드렸는데,
app.use((req, res, next) => {
console.log("모든 요청에 실행하고 싶어요.");
next();
})
이 app.use(() => {...})
가 미들웨어가 아니라 그 안에 들어있는 () => {...}
이 함수가 미들웨어입니다.
req, res, next 인자가 있는..
app.get("/", (req, res, next) => {
res.sendFile(path.join(__dirname, "index.html"));
})
사실 위 코드에서도 세번째 인자로 next가 있는데 제가 next를 안쓰고 생략을 한겁니다.
여튼 (req, res, next) => {...}
이 부분이 미들웨어고 이 미들웨어를 app.use()
에 장착을 한겁니다.
- 라우터의 미들웨어 -
그 다음에
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
app.get("/category/javascript", (req, res) => {
res.send("hello javascript");
})
위와 같은 것들을 라우터라고 불렀죠?
라우터도 미들웨어를 장착할 수 있는 공간?
왜냐면 라우터에 달려있는 콜백 함수들도 미들웨어((req, res, next) => {...}
)잖아요?
이 미들웨어 (req, res, next) => {...}
를 app.use()
, app.get()
, app.post()
에 장착하는 개념이라는 것.
- 미들웨어에 주소 정해주기 -
app.use("/about", (req, res, next) => {
console.log("모든 요청에 실행하고 싶어요.");
next();
})
그리고 app.use()
도 위와 같이 /about
경로를 입력하면 모든 곳에서 실행하고 싶은 것이아니라 about
으로 시작하는 요청에서만 실행이 됩니다.
그리고 next()
에 의해 그 아래에 위치한
app.get("/about", (req, res) => {
res.send("hello express");
})
이 코드로 찾아갈겁니다.
이런것도 미들웨어의 중요한 특성이라고 생각하시면됩니다.
- express 메소드 -
use
, get
, post
, put
, patch
, delete
다 있습니다.
이런 메소드에는 무조건 미들웨어를 하나만 써야되는 것이 아니라
app.use("/about", (req, res, next) => {
console.log("1 요청에 실행하고 싶어요.");
next();
}, (req, res, next) => {
console.log("2 요청에 실행하고 싶어요.");
next();
}, (req, res, next) => {
console.log("3 요청에 실행하고 싶어요.");
next();
})
위 코드처럼 미들웨어를 여러개 동시에 넣어줘도 됩니다.
그럼 위에서 아래로 실행되기 때문에.. 각 미들웨어마다 next()
가 있죠?
그럼 다음 미들웨어로 넘어갑니다.
근데 위와 같이 사용할 일이 있나? 라고 하실 수도 있는데 사용하는 경우가 있습니다.
그것도 나중에 실전 예제를 하면서 보여드리도록 하겠습니다.
이렇게 언급드리고 지나가는 부분이 나중에 다 실전에서 쓰이거든요?
실무에서 안 쓰이는 부분은 책에서 웬만하면 뺐기 때문에 실무에서 자주 쓰이는 패턴 또는 코드 이런거 위주로 설명을 한다고 보시면됩니다.
6.4.1 에러 처리 미들웨어
서버는 비동기이기 때문에 그리고 노드는 싱글 스레드이기 때문에 에러 처리에 민감해야됩니다.
에러 처리를 잘 해주셔야지 서버가 죽지않고 잘 돌아갑니다.
-
에러가 발생하면 에러 처리 미들웨어로
- err, req, rs, next까지 매개변수가 4개
- 첫번째 err에는 에러에 관한 정보가 담김
res.status
메소드로 HTTP 상태 코드를 지정 가능(기본값 200)- 에러 처리 미들웨어를 안 연결해도 익스프레스가 에러를 알아서 처리해주긴 함
- 특별한 경우가 아니면 가장 아래에 위치하도록 함
app.use((req, res, next) => {
console.log("1 요청에 실행하고 싶어요.");
// 이렇게 3번째 인자인 next를 실행해줘야 다음 라우터 함수들 중에 일치하는 곳을 찾아갑니다.
next();
}, (req, res, next) => {
throw new Error("에러가 났어요");
})
위와 같은 식으로 에러가 나면 어떻게 해야될까요?
미들웨어나 라우터 같은 곳에서 에러가 날 수도 있습니다.
500
에러가 납니다.
그리고 해당 에러가 어디서 났는지도 알려줍니다.
이렇게 express
는 기본적으로 에러가 났을 때 알아서 에러를 처리해줍니다.
- 에러 페이지 노출 보안문제 -
그런데 위와 같은 에러 화면이 일반 사용자들에게 그대로 노출되면 보안에 문제가 되겠죠?
위 화면을 보시면 너무 상세하게, 저희 서버 구조를 다 알려주고 있잖아요?
그래서 실제로는 express
가 기본으로 제공하는 에러처리를 사용하지 않습니다.
- 에러처리 직접하기 -
그래서 에러 처리를 직접 해주는데 그 에러 처리해주는 것 조차 미들웨어입니다.
// app.js
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// 아래 app.use는 모든 요청이 들어오는 코드에서 실행이 됩니다.
app.use((req, res, next) => {
console.log("1 요청에 실행하고 싶어요.");
// 이렇게 3번째 인자인 next를 실행해줘야 다음 라우터 함수들 중에 일치하는 곳을 찾아갑니다.
next();
}, (req, res, next) => {
throw new Error("에러가 났어요");
})
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
app.get("/category/javascript", (req, res) => {
res.send("hello javascript");
})
app.get("/category/:name", (req, res) => {
res.send("hello wildcard");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
app.get("*", (req, res) => {
res.send("hello everybody");
})
app.use((err, req, res, next) => {
console.error(err);
res.send("에러났지롱. 근데 안알려주지롱");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
위와 같이 에러 미들웨어는 라우터들 아래에 작성해줍니다.
코드 작성 순서
const app = express();
익스프레스 실행app.set("port", process.env.PORT || 3000);
포트설정- 라우터들.. 와일드카드 or 애스터리스크같이 범위가 큰 라우터들이 아래에 위치
- 에러처리 코드
에러처리 미들웨어 작성시 주의할 점
인자 4개를 모두 작성해줘야함.
다른 라우터에선 next
세번째 인자를 안쓰면 작성을 안 했는데, res
도 안쓰면 생략을 했는데 에러 미들웨어는 반드시 4개의 인자를 모두 작성해줘야함.
인자 값을 4개를 작성 안해주는 순간 해당 미들웨어는 에러 미들웨어가 아니고 일반 미들웨어.
4개 인자를 모두 입력해줘야 에러 미들웨어임.
const a = (a, b, c) => {
}
const b = (a, b, c, d) => {
}
console.log(a.length, b.length); // 3 4
위와 같이 인자 갯수를 달리 입력하면 해당 빌트인 오브젝트에 설정되는 값이 달라집니다.
즉, 완전 다른 함수가 되어버립니다.
여튼 위와 같은식으로 처리를 할 수 있습니다.
그리고 위와 같이 에러난 이유를 서버 콘솔창에서만 기록하게하면 보안적인 이슈가 없을겁니다.
- 없는 라우터 요청보내기 -
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// 아래 app.use는 모든 요청이 들어오는 코드에서 실행이 됩니다.
app.use((req, res, next) => {
console.log("1 요청에 실행하고 싶어요.");
// 이렇게 3번째 인자인 next를 실행해줘야 다음 라우터 함수들 중에 일치하는 곳을 찾아갑니다.
next();
})
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
app.get("/category/javascript", (req, res) => {
res.send("hello javascript");
})
app.get("/category/:name", (req, res) => {
res.send("hello wildcard");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
app.use((err, req, res, next) => {
console.error(err);
res.send("에러났지롱. 근데 안알려주지롱");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
위와 같이 애스터리스크 라우터를 지운 상태에서 /abc
요청을 보내면
위와 같이 404
에러가 뜹니다.
이런 404
에러도 저희가 커스터마이징 해줄 수가 있습니다.
- 404 에러 커스터마이징 -
const express = require("express");
const path = require("path");
const app = express();
app.set("port", process.env.PORT || 3000);
// 아래 app.use는 모든 요청이 들어오는 코드에서 실행이 됩니다.
app.use((req, res, next) => {
console.log("1 요청에 실행하고 싶어요.");
// 이렇게 3번째 인자인 next를 실행해줘야 다음 라우터 함수들 중에 일치하는 곳을 찾아갑니다.
next();
})
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.post("/", (req, res) => {
res.send("hello express");
})
app.get("/category/javascript", (req, res) => {
res.send("hello javascript");
})
app.get("/category/:name", (req, res) => {
res.send("hello wildcard");
})
app.get("/about", (req, res) => {
res.send("hello express");
})
// 아래 코드가 404처리 미들웨어가 됩니다.
// 그런데 이 코드는 에러는 아닙니다. 404 처리 미들웨어라고 생각하시면 됩니다.
// 위의 라우터들 모두 해당사항이 없다. 그러면 404니깐 아래 미들웨어가 실행되는 것일 뿐입니다.
app.use((req, res, next) => {
res.send("404지롱");
})
// 아래 코드가 에러 처리 코드입니다.
app.use((err, req, res, next) => {
console.error(err);
res.send("에러났지롱. 근데 안알려주지롱");
})
app.listen(app.get("port"), () => {
console.log("익스프레스 서버 실행");
});
- HTTP 상태코드 -
지금까지의 요청들은 전부 HTTP 상태코드 200
을 돌려주고 있습니다.
app.use((req, res, next) => {
res.status(200).send("404지롱");
})
지금은 위의 res.status(200)
이란 코드가 생략되어있는겁니다.
기본적으로 200이기 때문에.
그럼 404이면
app.use((req, res, next) => {
res.status(404).send("404지롱");
})
이렇게 바꿔주면
404로 바뀝니다.
서버쪽에서 404
여도 브라우저에서 200
이라고 뻥칠수가 있습니다.
왜냐하면,
app.use((err, req, res, next) => {
console.error(err);
res.status(200).send("에러났지롱. 근데 안알려주지롱");
})
.status(200)
이게 기본값이니깐. 위의 200을 500으로 바꾸면,
위와 같이 500
에러가 뜹니다.
그래서 서버가 클라이언트한테 사기를 칠 수가 있습니다.
그래서 이런걸해서 오히려 보안 위협을 없애는 겁니다.
해커들이 404
에러났다, 403
에러났다, 401
에러 났다, 이런 코드들 있죠?
예를 들어 로그인 관련 에러면 401
이고, 클라이언트가 금지된 행동을 한다하면 403
, 이렇게 해주거든요?
그런데 이게 그런 상태코드가 해커들에겐 오히려 힌트가 될 수 있습니다.
401
, 403
이런게 뜨면 "아 여기 뭔가 보안 관련된 무언가가 있나보군" 이라고 해서 해커들이 그 주소로 더 파고들 수도 있고, 예를 들어 500
에러가 나는 지점을 알게되면, "어 내가 서버를 일부로 에러날 수 있게도 할 수 있겠구나" 라고 해서 해커들이 오히려 이 500
을 보고 그 부분만 집중 공략해서 서버에 에러를 내는 공격을 할 수 있습니다.
그래서 실무에선 보통 200
번대만 잘 써주고 400
번대와 500
번대 쓰시는 것은 좀 조심을 하셔야됩니다.
특히 500
번대는 확실히 보안 위험을 없앤 후에 쓰시고 400
번대는 401, 403, 409 이렇게 세세하게 전달해주기보다는 404
로 퉁치는 사람들도 많습니다.
401, 403, 404, 409 어떤 것이든 404
에러를 띄워 해커들이 뭐가 잘못되었는지를 잘 모르게하는 겁니다.
각 번호마다 의미가 담겨있기 때문에 그런걸 다 숨겨서 200으로 해버리는 케이스도 있습니다.
이런 사소한 것에서 해커들은 힌트를 얻어서 보안 공격을 하기 때문에 그렇습니다.
- 라우터 작성시 자주하는 실수 1 -
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
res.send("안녕하세요.");
res.json({ hello: "hyungju-lee" });
})
한 라우터 안에서 위 코드처럼 send
를 두 번 이상 하는 분들이 있습니다.
sendFile
이나 send
나 비슷한겁니다.
그런데 한 라우터에서 응답을 위처럼 여러번하면 에러가 납니다.
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
위 에러는 노드 처음 하시면 반드시 한번쯤은 저지르는 실수이니깐 반드시 기억해두십시오.
한 라우터, 또는 미들웨어 넘어오면서 send
, sendFile
, json
이런게 여러번 나오면 에러가 납니다.
예를 들어
app.use((req, res, next) => {
console.log("1 요청에 실행하고 싶어요.");
res.send("안녕하세요.");
next();
})
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
이렇게 되어있어도 에러가 납니다.
app.use()
의 미들웨어에서 send
를 하고 next()
로 넘어가서 다음 app.get()
의 미들웨어에서 sendFile
이렇게 두 번 이상 했기 때문에 에러가 나는겁니다.
이미 첫번째 send
또는 sendFile
또는 json
과 같은 응답을 보내서 끝난건데, 그 뒤에 또 응답을 보내려고 하는거에요.
원래 요청 한번에 응답 한번 보내줘야하거든요?
그런데 저희는 지금 위 예시코드에서 요청 한번에 응답을 2~3번 보내려고 하고있잖아요?
그래서 이렇게 에러가 뜨는겁니다.
- 라우터 작성시 자주하는 실수 2 -
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
또 위와 같은 에러가 나는 경우가
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
res.writeHead();
})
writeHead
http 공부했을 때 사용했던 메소드 있죠?
그거를 응답 보낸 다음에 writeHead
하면 위와 같은 에러가 납니다.
이미 응답을 보냈는데 왜 또 응답하느냐, 헤더는 왜 쓰냐, 라는 의미의 에러메시지입니다.
그런데 여기서 생각이 드시는게
app.get("/", (req, res) => {
res.status(200).sendFile(path.join(__dirname, "index.html"));
})
express
에서는 위와 같이 .status(200)
를 통해 HTTP 상태코드를 설정하는데 http
에선
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("안녕하세요");
이런식으로 작성했었죠?
express
에서도 위와 같이 작성 가능합니다.
그런데 express
에선 위 두 코드를 편리하게 하나로 합쳐놓은 겁니다.
app.get("/", (req, res) => {
res.status(200).send("안녕하세요.");
})
위의 http
서버 코드가 위의 express
코드로 한줄로 줄어든겁니다. .status(200)
생략 가능.
그래서 express
에서 writeHead
나 end
사용해도 되거든요?
왜냐면 위의 미들웨어의 req
, res
가 http
서버의 req
, res
와 같지는 않지만 걔네들을 상속받았기 때문에..
그래서 기본적으로 http
의 req
, res
를 쓸 수 있는데 express
에선 사용하지 않는 걸 권합니다.
그냥 express
형식으로 작성하시는 걸 권합니다.
왜냐하면,
app.get("/", (req, res) => {
res.setHeader("Content-Type", "text/html");
res.status(200).sendFile(path.join(__dirname, "index.html"));
})
헤더도 express
에선 위와 같은식으로 만들어주기 때문입니다.
이게 훨씬 더 편하기 때문에 기본 http
는 쓰지 마시고 express
형식으로 작성하시는 걸 권합니다.
6.5 next 활용법
- 초보들이 많이하는 실수 -
초보들이 가장 많이 하는 실수가
app.get("/", (req, res) => {
res.json({ hello: "hyungju-lee" });
res.json({ hello: "hyungju-lee" });
res.json({ hello: "hyungju-lee" });
})
위와 같이 한 번에 여러번 응답하는 거랑
app.get("/", (req, res) => {
res.json({ hello: "hyungju-lee" });
console.log("hello hyungju-lee");
})
res.json()
다음에 위와 같이 console.log("hello hyungju-lee");
코드가 실행이 안되는줄 아는 분들도 많더라구요.
res.json()
은 return이 아닙니다.
즉, 함수는 return이 되어야 종료되는데, return이 아니기 때문에 console.log("hello hyungju-lee");
코드가 실행이 되겠죠?
즉, res.json()
은 응답을 보낼뿐이지 함수 자체를 종료하는 것이 아닙니다.
이것은 자바스크립트입니다.
자바스크립트 함수는 return해야 함수가 종료되죠?
이게 노드나 express
를 하다가 내가 자바스크립트를 하고있는 것을 까먹는 초보자분들이 많아서 한번 짚어드린겁니다.
- Content-Type, application/json -
app.get("/", (req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ hello: "hyungju-lee" }));
})
json
을 쓸 때는 위와 같이 res.writeHead(200, { "Content-Type": "application/json" });
를 해줘야됩니다.
그리고 res.end(JSON.stringify({ hello: "hyungju-lee" }));
이렇게 해줘야 json
으로 응답하는 거거든요?
그런데 위와 같이하면 코드가 복잡하죠?
그거를 express
에서
app.get("/", (req, res) => {
res.json({ hello: "hyungju-lee" });
})
이렇게 줄여준겁니다.
길게 쓸 필요 없이 위와 같이 작성하면 "Content-Type": "application/json"
이걸 알아서 설정해주니까, 기본 노드의 http
모듈의 메소드들은 사용할일이 거의 없다고 보시면 됩니다.
저도 실무보면서 거의 사용한적이 없습니다.
쓰는 경우도 있기는한데, 거의 없으니까 모르셔도됩니다.
- api 서버, 웹서버 -
api 서버를 만들 땐
app.get("/", (req, res) => {
res.json({ hello: "hyungju-lee" });
})
이거를 보통 많이 쓰고 웹서버를 만드실 땐
app.get("/", (req, res) => {
res.sendFile(path.join(__dirname, "index.html"));
})
sendFile
을 많이씁니다.
그리고 그 다음에 또 많이 사용하는 것이
app.get("/", (req, res) => {
res.render();
})
res.render();
입니다.
res.render();
도 .json()
, .send()
, .sendFile()
처럼 응답을 보내는거라고 미리 알아두시면됩니다.
- throw new Error -
app.use((req, res, next) => {
console.log("1 요청에 실행하고 싶어요.");
next();
}, (req, res, next) => {
throw new Error();
})
실제로 위와 같이 throw new Error
를 내는 경우가 있기는 한데, 이거 너무 대놓고 에러를 내는거잖아요?
실제 코드에서 이렇게 대놓고 에러가 날 일은 거의 없고 에러는 보통 은근히 나거든요?
app.use((req, res, next) => {
console.log("1 요청에 실행하고 싶어요.");
next();
}, (req, res, next) => {
try {
console.log("에러야~~");
} catch (error) {
next(error);
}
})
위와 같은식으로 try-catch
문으로 에러를 처리하는 경우가 많습니다.
catch
에서 error가 잡히잖아요?
그럼 catch
구문에서 next(error)
처리를 보통 많이 합니다.
- 어 next는 다음으로 넘기는거 아닌가요? -
네. next()
는 다음으로 넘기는게 맞거든요?
단, next()
에 인자가 없을 때만.
app.use((req, res, next) => {
console.log("1 요청에 실행하고 싶어요.");
next();
}, (req, res, next) => {
try {
console.log("에러야~~");
} catch (error) {
next(error);
}
})
이렇게 next(error)
에 인자가 들어가는 순간 에러로 처리돼서, 위 코드에서 다음 미들웨어로 넘어가는 것이 아니라 바로
app.use((err, req, res, next) => {
console.error(err);
res.status(500).send("에러났지롱. 근데 안알려주지롱");
})
에러처리 미들웨어로 넘어갑니다.
그래서 보통 에러를 처리할 땐 next()
에 에러 인자를 넣어서 바로 에러처리 미들웨어로 넘겨줍니다.
6.5.1 next 정리
-
next
를 호출해야 다음 코드로 넘어감next
를 주석 처리하면 응답이 전송되지 않음- 다음 미들웨어(라우터 미들웨어)로 넘어가지 않기 때문
-
next
에 인수로 값을 넣으면 에러 핸들러로 넘어감(route
인 경우 다음 라우터로)
- 라우터 설명, next("route"); -
app.get("/", (req, res, next) => {
res.sendFile(path.join(__dirname, "index.html"));
next("route");
}, (req, res) => {
console.log("실행되나요?");
})
app.get("/", (req, res) => {
console.log("실행되지롱");
})
예를 들어 같은 라우터가 위와 같이 2개가 있다면, 그리고 next("route")
를 하면, 같은 라우터의 다른 미들웨어가 실행되는 것이 아니라 다음 라우터로 일단 넘어갑니다.
즉, console.log("실행되나요?");
를 건너뛰고 다음 라우터인 console.log("실행되지롱");
이게 실행됩니다.
어 그럼 다음 라우터의 미들웨어가 실행되는데, 그럼 같은 라우터에 미들웨어를 왜 2개이상 적어요?
실행되지 않을 미들웨어를 왜 작성했을까요?
app.get("/", (req, res, next) => {
res.sendFile(path.join(__dirname, "index.html"));
if (true) {
next("route");
} else {
next();
}
}, (req, res) => {
console.log("실행되나요?");
})
app.get("/", (req, res) => {
console.log("실행되지롱");
})
위와 같이 if
문을 사용하는 경우가 있는데 if
문에 따라 어떤 미들웨어를 다음에 실행할건지 제어할 수 있습니다.
예를 들어 위 if
문 조건이 false면 같은 라우터의 다음 미들웨어가 실행되겠죠? (console.log("실행되나요?");
)
그런데 true면 다음 라우터의 미들웨어가 실행되겠죠. (console.log("실행되지롱");
)
이런식으로 next
분기처리를해 다음 미들웨어로 어떻게 넘어갈지를 자유자재로 컨트롤할 수가 있습니다.
이런 특성을 알아야 어떤 라우터로 보낼지 제어를 잘 할 수가 있겠죠.
이런건 다 중복을 줄이기 위한 코드입니다.
정리
- next() : 다음 미들웨어로 넘김
- next(인자) : 인자가 들어있는 경우 다음 라우터로 넘김
- try - catch 문 안에서의 next(인자) : 에러처리 미들웨어(
(a, b, c ,d) => {}
) 인자 4개 갖고있는 애. 여기로 넘김.
6.6 morgan, bodyParser, cookieParser
미들웨어의 진가는 남이 미리 만들어둔 거, 저희가 HTTP 서버 만들 때 특히 어려웠던게 쿠키, 세션하는 것도 어려웠었고 그 다음에 어려웠던게 바디, 요청 본문할 때 스트림 사용해서 데이터 청크 모아서 받았었잖아요?
저도 이건 개인적으로 매우 안 좋게 생각하는 부분인데
POST/user
요청을 위와 같이 받고 있는 겁니다.
청크 데이터를 위와 같이 모아줘야 된다는 것.
스트림이기 때문에.
위와 같은 코드 너무 지저분하죠? 코드 자체가.
그리고 세션 처리할 때도 위와 같이 세션이 있는지 없는지 검사하고, if
문으로..
이런게 너무 지저분하기 때문에 이런 것들을 미들웨어로 다른 사람들이 다 만들어 놨습니다.
그래서 그거를 사용하면 진짜 편리하게 할 수 있습니다.
6.6.1 자주쓰는 미들웨어
-
morgan
,cookie-parser
,express-session
설치app.use
로 장착- 내부에서 알아서
next
를 호출해서 다음 미들웨어로 넘어감 -
dotenv
는 다음 장에 설명npm i morgan cookie-parser express-session
body-parser
도 있는데 요즘 잘 안씁니다.
이게 노드 할 때 옛날 사람인지 아닌지 구분하는 거, express 사용할 때 body-parser
를 쓴다. 그럼 옛날 사람입니다.
express 쓸 때 body-parser
를 안 쓴다. 그럼 아 어디서 최신 코드를 배웠구나. 최신 사람입니다.
body-parser
유무로 옛날 사람 최신 사람을 구분할 수 있습니다.
혹시나 여러분들이 body-parser
를 사용하는 옛날사람이라면 얼릉 최신사람으로 넘어오세요.
- morgan -
-
서버로 들어온 요청과 응답을 기록해주는 미들웨어
- 로그의 자세한 정도도 선택 가능 (
dev
,tiny
,short
,common
,combined
) - 예시) GET / 200 51.267ms - 1539
- 순서대로 HTTP 요청, 요청주소, 상태코드, 응답속도, - 응답바이트
- 개발 환경에서는
dev
, 배포 환경에서는combined
를 애용함
- 로그의 자세한 정도도 선택 가능 (
-
더 자세한 로그를 위해 winston 패키지 사용(15장에서)
-
코드에 아래 내용 추가
const morgan = require("morgan"); app.use(morgan('dev'));
-
서버 실행
npm start
package.json의 scripts 부분에 start: nodemon app으로 설정되어있음.
-
기존과 다르게 아래와 같이 한 줄이 추가됨
클라이언트에서 어떤 요청을 했는지 서버에 기록이됨.
위에 보시면GET
,/
요청이 왔다는 걸 알 수 있음.
그 다음에 응답하는데5.732ms
가 걸렸다.
그리고 응답은35byte
를 응답했다.
그리고500
- 서버쪽 에러가 났다.
이러한 정보들을 보여줍니다.즉, 요청을 보냈을 때 서버에서 어떻게 응답했는지를 알 수 있고(저희가 작성한 코드가 어떻게 응답했는지 알 수 있고), 위와 같이
500
에러가 났다고 하면 위와 같이 에러 로그가 바로 뜹니다.
그럼 위 에러 로그를 보면서 어떤 라우터에서 에러가 났는지도 바로 알 수 가 있습니다.이렇게 요청과 응답을 기록하는 라우터가
morgan
입니다.
const morgan = require("morgan");
// 그리고 아래가 dev가 아니라 combined도 있는데 저는 보통 개발시에는 dev를 쓰고 배포시에는 combined를 사용합니다.
// combined를 하면 좋은점이 좀 더 자세하게 뜹니다.
app.use(morgan('combined'));
위와 같은 식으로 좀 더 자세하게 뜹니다.
::1
: 이렇게 사람 IP도 뜨고13/Mar/2021:15~~~
: 시간도 뜨고GET
: 요청 정보도 뜨고500
: 응답도 드고35
: 바이트 수도 뜨고- 브라우저 정보도 뜨고
위와 같이 정보들이 정확하게 뜨기 때문에 저는 배포할 때는 보통 combined
를 사용하고 개발할 때는 보통 dev
로 많이 합니다.
- cookie-parser -
-
요청 헤더의 쿠키를 해석해주는 미들웨어
- HTTP 서버 4장에서 공부할 때 썼던, 직접 만들었던
parseCookies
함수와 기능 비슷 -
req.cookies
안에 쿠키들이 들어있음app.use(cookieParser(비밀키));
- 비밀키로 쿠키 뒤에 서명을 붙여 내 서버가 만든 쿠키임을 검증할 수 있음
- HTTP 서버 4장에서 공부할 때 썼던, 직접 만들었던
-
실제 쿠키 옵션들을 넣을 수 있음
expires
,domain
,httpOnly
,maxAge
,path
,secure
,sameSite
등-
지울 때는
clearCookie
로(expires
와maxAge
를 제외한 옵션들이 일치해야 함)res.cookie('name', 'hyungju-lee', { expires: new Date(Date.now() + 900000), httpOnly: true, secure: true }) res.clearCookie('name', 'hyungju-lee', {httpOnly: true, secure: true});
const cookieParser = require('cookie-parser');
app.use(cookieParser(process.env.COOKIE_SECRET));
cookie-parser
는 뭐냐면, 저희가 4장에서 쿠키를 파싱하기 위해서 엄청난 코드를 작성했었죠?
그런데 이를 cookie-parser
를 사용하면 알아서.. 쿠키가 있으면 아래 코드처럼 req.cookies
이걸로 알아서 쿠키({ mycookie: 'test' }
)를 보낼 수 있습니다.
이미 알아서 파싱이 되어있습니다.
const cookieParser = require('cookie-parser');
app.use(cookieParser(process.env.COOKIE_SECRET));
app.get('/', (req, res, next) => {
req.cookies // { mycookie: 'test' }
res.sendFile(path.join(__dirname, 'index.html'));
})
그 다음에 express
에서 쿠키 관련 조작을 할 수 있게 되었는데
if (req.url.startsWith('/login')) {
const { query } = url.parse(req.url);
const { name } = qs.parse(query);
const expires = new Date();
// 쿠키 유효 시간을 현재시간 + 5분으로 설정
expires.setMinutes(expires.getMinutes() + 5);
res.writeHead(302, {
Location: '/',
'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
});
res.end();
}
저희가 Set-Cookie
할 때 위와 같이 복잡하게 처리를 해서 보내줬어야 됐잖아요?
위와 같은 것을 똑같이 cookie-parser
로 처리하해보면 아래와 같이 처리할 수 있습니다.
const cookieParser = require('cookie-parser');
app.use(cookieParser(process.env.COOKIE_SECRET));
app.get('/', (req, res, next) => {
req.cookies // { mycookie: 'test' }
res.cookie('name', encodeURIComponent(name), {
expires: new Date(),
httpOnly: true,
path: '/'
})
res.sendFile(path.join(__dirname, 'index.html'));
})
아래와 같은 식으로 writeHead
메소드로 문자열로 쭉 쓰는 것보단
'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`
cookie-parser
형태로 하는 것이 훨씬 깔끔합니다.
- 쿠키 지울 때 -
const cookieParser = require('cookie-parser');
app.use(cookieParser(process.env.COOKIE_SECRET));
app.get('/', (req, res, next) => {
req.cookies // { mycookie: 'test' }
res.cookie('name', encodeURIComponent(name), {
expires: new Date(),
httpOnly: true,
path: '/'
})
// 쿠키 지우고 싶을 때 clearCookie를 사용해서.. expires 옵션은 필요 없고, 나머지 옵션들은 위의 res.cookie와 똑같애햐됩니다.
// 아래와 같이 작성하면 방금 위에서 res.cookie로 세팅했던 쿠키가 아래 clearCookie에서 지워집니다.
res.clearCookie('name', encodeURIComponent(name), {
httpOnly: true,
path: '/'
})
res.sendFile(path.join(__dirname, 'index.html'));
})
이러한 식으로 쿠키를 좀 더 편리하게 다룰 수 있습니다.
- signedCookies -
const cookieParser = require('cookie-parser');
app.use(cookieParser(process.env.COOKIE_SECRET));
app.get('/', (req, res, next) => {
req.cookies // { mycookie: 'test' }
// signedCookies가 뭐냐면, 위의 app.use(cookieParser("여기 부분"));
// 여기 부분에 암호를 넣는다고 칩시다.
// 예를 들어, app.use(cookieParser("hyungjuleepassword")); 이렇게 암호를 넣었다면, 쿠키를 암호화할 수 있거든요?
// 모든 쿠키를 암호화하진 않는데, 어떤 쿠키는 암호화를 해서 브라우저에 가도 쿠키가 암호화돼서 해커들이 읽지 못하도록. 그런식으로 암호화할 수가 있습니다.
// 암호화하면 req.cookies 대신에 req.signedCookies;(암호화된 쿠키)를 사용합니다.
// 암호학 아니라 서명이 좀 더 정확한 표현이겠네요.
req.signedCookies;
res.cookie('name', encodeURIComponent(name), {
expires: new Date(),
httpOnly: true,
path: '/'
})
// 쿠키 지우고 싶을 때 clearCookie를 사용해서.. expires 옵션은 필요 없고, 나머지 옵션들은 위의 res.cookie와 똑같애햐됩니다.
// 아래와 같이 작성하면 방금 위에서 res.cookie로 세팅했던 쿠키가 아래 clearCookie에서 지워집니다.
res.clearCookie('name', encodeURIComponent(name), {
httpOnly: true,
path: '/'
})
res.sendFile(path.join(__dirname, 'index.html'));
})
- body-parser 1 -
-
요청의 본문을 해석해주는 미들웨어
- 폼 데이터나 AJAX 요청의 데이터 처리
- json 미들웨어는 요청 본문이 json인 경우 해석, urlencoded 미들웨어는 폼 요청 해석
-
put이나 patch, post 요청 시에 req.body에 프런트에서 온 데이터를 넣어줌
app.use(express.json()); app.use(express.urlencoded({ extended: false }));
-
버퍼 데이터나 text 데이터일 때는 body-parser를 직접 설치해야 함
npm i body-parser
const bodyParser = require('body-parser'); app.use(bodyParser.raw()); app.use(bodyParser.text());
-
Multipart 데이터(이미지, 동영상 등)인 경우는 다른 미들웨어를 사용해야 함
- multer 패키지(9장에서)
제가 body-parser
쓰면 조금 옛날 사람이라고 그랬죠?
왜냐하면 예전엔 body-parser
를 설치해서 사용했었는데 요즘엔 body-parser
의 기능이 express
로 들어갔습니다.
요즘도 아닙니다. 3~4년 전에 들어갔습니다.
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
보통 위 두 코드를 많이 사용합니다.
위 두 코드를 넣으면 뭐가 좋으냐면, 항상 지난번 HTTP 서버 만들었을 때와 비교를 해봐야됩니다.
워낙 HTTP쪽이 지저분했기 때문에.
req.on('data', 콜백)
, req.on('end', 콜백)
이런거 할 필요 없이 위 코드 두 줄을 넣으면 알아서 데이터가 파싱이 되어서
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res, next) => {
req.body.name
res.sendFile(path.join(__dirname, 'index.html'));
})
위와 같은식으로 req.body.name
이렇게 바로 쓸 수가 있습니다.
이전 코드를 보시면 name
꺼내려고 별의별 짓을 다 했었죠?
그런거를 할 필요 없이 위와 같이 편하게 req.body.name
을 꺼낼 수가 있습니다.
여기서 name
은 클라이언트에서 보내는거. 클라이언트에서 name
을 보냈으면 서버쪽에서도 name
이고 클라이언트에서 hello
프로퍼티키를 보냈으면 여기서도 hello
이고.
여튼 req.body
안에 다 들어가있다는 거.
app.use(express.json()); // 클라이언트에서 json 데이터를 보냈을 때 json 데이터를 파싱해서 req.body로 넣어줍니다.
app.use(express.urlencoded({ extended: true })); // 클라이언트에서 form 데이터를 submit 할 때, 기본적으로 urlencoded 이거든요?
// 결론은 form 데이터를 파싱해주는 겁니다.
// extended: true는 form 데이터를 파싱할 때 쿼리스트링을 어떻게 처리할거냐인데 이건 true를 해두시는걸 추천드릴게요.
// true로 하면 qs 라는 모듈을 사용하거든요?
// false면 3장에서 배웠던 노드 내장 모듈인 querystring 모듈을 사용합니다.
// 그런데 qs가 querystring보다 훨씬 더 강력하기 때문에 true를 사용하시는걸 추천드립니다.
그래서 위 두 코드는 필수로 장착을 해두신다고 보시면 됩니다.
- body-parser 2 -
사실 body-parser
에 express
에 안 들어있는 두 가지가 더 있거든요?
const bodyParser = require("body-parser");
app.use(bodyParser.raw()); // 바이너리 데이터
app.use(bodyParser.text()); // 문자열
그런데 위에 2개는 굳이 잘 안쓰여서 express
에서 뺀거 같습니다.
그래서
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
얘네 둘이면 충분하고, 다만 form 데이터 보낼 때, image 같은걸 보내는 경우가 있거든요?
image나 file 보내는 경우.
얘네들은 urlencoded
가 처리를 못해서 multer
같은 다른 패키지를 사용해야됩니다. (busboy
등등…)
저는 주로 multer
를 사용하는 편입니다.
6.7 static 미들웨어
이번에는.. express
에서 제공하는 미들웨어가 하나 더 있거든요?
-
정적인 파일들을 제공하는 미들웨어
- 인수로 정적 파일의 경로를 제공
- 파일이 있을 때
fs.readFile
로 직접 읽을 필요 없음 - 요청하는 파일이 없으면 알아서 next를 호출해 다음 미들웨어로 넘어감
-
파일을 발견했다면 다음 미들웨어는 실행되지 않음
// app.use(요청경로, express.static(실제경로)); app.use('/', express.static(path.join(__dirname, 'public')));
-
콘텐츠 요청주소와 실제 콘텐츠의 경로를 다르게 만들 수 있음
- 요청 주소
localhost:3000/stylesheets/style.css
- 실제 콘텐츠 경로
/public/stylesheets/style.css
- 서버의 구조를 파악하기 어려워져서 보안에 도움이 됨
- 요청 주소
static
은 뭐냐면, 저희가 REST API할 때,
fs
모듈 사용했던거 기억나시나요?
readFile
로 html
, js
, css
같은 정적 파일 보내주는거.
그게 위 부분이었잖아요?
이것이 static
으로 빠졌다고 생각하시면 됩니다.
// app.use(요청경로, express.static(실제경로));
app.use('/', express.static(path.join(__dirname, 'public')));
- 첫번째 인자가 요청경로이고
- 두번째 인자에 실제경로를 적어줍니다.
저는 보통 public
폴더에 많이 넣어놓는데 이게 요청경로와 실제경로가 다르잖아요?
어떤 사람이 localhost:3000/hyungju-lee.html
을 요청을 했어요.
그런데 실제로 서버에선 public/hyungju-lee.html
이렇게 파일이 들어있는겁니다.
- 요청경로:
localhost:3000/hyungju-lee.html
// 실제경로:public/hyungju-lee.html
- 요청경로:
localhost:3000/hello.css
// 실제경로:public/hello.css
그런데 public
이란 이름은 너무 유명하니깐 실제 하실 때는 다른 폴더 이름으로 하시는게 좋아요.
public-3030
이런식으로?
- 요청경로:
localhost:3000/hyungju-lee.html
// 실제경로:public-3030/hyungju-lee.html
- 요청경로:
localhost:3000/hello.css
// 실제경로:public-3030/hello.css
이렇게하면 좋은점이 보안에 좋습니다.
정적 파일을 제공하는 의미도 있지만 사람들이 hyungju-lee.html
을 요청하거나 hello.css
를 요청할 때, 저희 서버 구조를 전혀 예측을 하지 못합니다.
저희 서버에 public-3030
이라는 폴더가 존재한다는 것을 클라이언트는 알 수가 없죠?
보안에 좋습니다.
// app.use(요청경로, express.static(실제경로));
app.use('/', express.static(path.join(__dirname, 'public')));
여튼 static
을 사용하면 정적파일들, image, 동영상, pdf 파일 등을 전부 다 제공을 해줄 수 있으면서도 요청경로와 실제경로가 다르기 때문에 보안에도 많은 도움이 됩니다.
- 미들웨어 주의할점 -
-
req, res, next를 매개변수로 가지는 함수
app.use((req, res, next) => { console.log("모든 요청에 다 실행됩니다."); next(); })
-
익스프레스 미들웨어들도 다음과 같이 축약 가능
- 순서가 중요
-
static 미들웨어에서 파일을 찾으면 next를 호출 안하므로 json, urlencoded, cookieParser는 실행되지 않음
app.use( margan('dev'), express.static('/', path.join(__dirname, 'public')), express.json(), express.urlencoded({extended: false}), cookieParser(process.env.COOKIE_SECRET) )
1. - 미들웨어간 순서 -
미들웨어 간에도 순서가 중요합니다.
app.use(morgan('combined'));
app.use('/', express.static(path.join(__dirname, 'public'))); // static은 이 위치에 많이 넣습니다.
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.get('/about', (req, res) => {
res.send('hello express');
})
미들웨어 간에 순서가 중요한 이유
app.use(morgan('combined'));
:morgan
미들웨어는 거의 무조건 실행을 합니다.-
app.use('/', express.static(path.join(__dirname, 'public')));
:morgan
을 실행한 후 이 부분을 실행해 파일을 찾겠죠.
예를 들어 어떤 사람이localhost:3000/hyungju-lee.png
사진파일을 요청을 했습니다.
그러면morgan
미들웨어를 통해서 서버에GET / hyungju-lee.png
이런 요청 정보들이 찍히겠죠?
그리고 그 다음에app.use('/', express.static(path.join(__dirname, 'public')));
이 부분에서hyungju-lee.png
가 진짜 있다면, 제공을 하겠죠?
제공을 하고 여기서 끝납니다.
static
은 해당 요청 파일이 실제 경로에 존재한다면, 여기서 더이상 다음 미들웨어로 넘어가지 않습니다.
즉,next
를 호출하지 않는다는 겁니다.morgan
다음에 여기가 실행되는 이유는morgan
미들웨어 내부에서next
를 하기 때문에 여기가 실행되는 겁니다.
거의 모든 미들웨어는 내부적으로next
를 실행한다고 보시면 됩니다.
그런데static
은 파일을 찾으면 내부적으로next
를 안해버립니다.
그럼 그 아래에 위치한 미들웨어들은 실행을 안하겠죠?그런데 요청이
localhost:3000/about
이렇게 왔습니다.
그러면public
안에about
이란 페이지가 없다면 여기서 끝나는게 아니라next
를 합니다. app.get('/about', 콜백)
: 그러면 이 부분이 실행되게 되는겁니다.
즉, 요청 주소에 따라서 어디까지 실행되느냐.
미들웨어가 어디까지 실행되느냐가 많이 달라지거든요?
app.get('/about', 콜백)
얘가 실행될 때는 그 위에 위치한 미들웨어..
app.use(morgan('combined'));
app.use('/', express.static(path.join(__dirname, 'public'))); // static은 이 위치에 많이 넣습니다.
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',
}));
얘네들은 전부 실행되는거죠?
그런데 hyungju-lee.png
가 있어서 app.use('/', express.static(path.join(__dirname, 'public')));
여기서 끝나면 그 밑에께 안 실행됩니다.
2. - 혹시 static 미들웨어 위치가 다르다면 1 -
그런데 혹시 static
을 아래 위치에 넣어놨다면,
app.use(morgan('combined'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use('/', express.static(path.join(__dirname, 'public'))); // static은 이 위치에 많이 넣습니다.
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
name: 'session-cookie',
}));
app.get('/about', (req, res) => {
res.send('hello express');
})
hyungju-lee.png
사진을 요청하는데
app.use(express.json());
: json 파싱하고app.use(express.urlencoded({ extended: false }));
: 바디 파싱하고app.use(cookieParser(process.env.COOKIE_SECRET));
: 쿠키 파싱하고
이러고 있으면 엄청난 손실이죠?
3. - 혹시 static 미들웨어 위치가 다르다면 2 -
const session = require("express-session");
const multer = require("multer");
app.use(morgan('combined'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session());
app.use(multer().array());
app.use('/', express.static(path.join(__dirname, 'public'))); // static은 이 위치에 많이 넣습니다.
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
name: 'session-cookie',
}));
app.get('/about', (req, res) => {
res.send('hello express');
})
또 위와 같이 사진만 전송하면 되는데
app.use(express.json());
: json 파싱app.use(express.urlencoded({ extended: false }));
: 바디 파싱app.use(cookieParser(process.env.COOKIE_SECRET));
: 쿠키 파싱app.use(session());
: 세션도 파싱app.use(multer().array());
: 이미지 등 파싱
이러면 매우 비효율적이죠?
쓰이지도 않는 것들을 거쳐가고 있잖아요.
app.use(morgan('combined'));
app.use('/', express.static(path.join(__dirname, 'public'))); // static은 이 위치에 많이 넣습니다.
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',
}));
그래서 static
은 저는 보통 위 위치에다 많이 넣습니다.
그래야 정적 파일들은 다른 파싱을 안 거치고 바로 제공되고 다른 요청이면 파싱을 거친 후에 실행되고..
그래서 미들웨어들간의 순서도 매우 중요합니다. 성능 측면에서.
4. - 혹시 static 미들웨어 위치가 다르다면 3 -
app.use(morgan('combined'));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session());
app.use('/', express.static(path.join(__dirname, 'public'))); // static은 이 위치에 많이 넣습니다.
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(session({
resave: false,
saveUninitialized: false,
secret: process.env.COOKIE_SECRET,
cookie: {
httpOnly: true,
secure: false,
},
name: 'session-cookie',
}));
그런데 위와 같은 위치는 종종 봤습니다.
쿠키와 세션을 정적파일 제공보다 위에 놨다? 이는 어떤 경우일까요?
쿠키랑 세션 보통 로그인할 때 쓰잖아요?
즉, 로그인 한 사람한테만 특정정적파일
을 주고 로그인 안한 사람들한테는 안 주겠다고 하면 위와 같은 미들웨어 배치가 필요하겠죠?
쿠키, 세션이 있어야 로그인 했는지 안했는지 판단을 할 수 있으니까요.
물론 위 경우엔 static
에 미들웨어가 하나 더 있어야겠지만..
여튼 순서에 따라 동작하는 것이 완전히 달라지기 때문에..
그렇다고 정해진 미들웨어 순서는 없습니다.
서비스에 맞게 순서를 조정하셔야됩니다.
나는 정적 파일 제공할 때는 morgan
도 안 거치고 싶다. 그러면 static
미들웨어를 morgan
미들웨어보다 위로 올리면 됩니다.
그러면 정적 파일 제공은 서버 콘솔창에 기록이 안되겠죠?
그런식으로 원하는대로 조정을 하실 수 있습니다.
5. - next -
-
next를 호출해야 다음 코드로 넘어감
- next를 주석 처리하면 응답이 전송되지 않음
- 다음 미들웨어(라우터 미들웨어)로 넘어가지 않기 때문
-
next에 인수로 값을 넣으면 에러 핸들러로 넘어감
("route"인 경우 다음 라우터로 - 반드시 "route"라기보단 어떤 인자가 담기면 다음 라우터로 넘어가는 것 같음)
(그 인자가 catch 문 안에서 err를 담는다던지 그러면 에러처리 미들웨어로 넘어가는거인거같음)
6. - 미들웨어간 데이터 전달하기 -
-
req나 res 객체 안에 값을 넣어 데이터 전달 가능
- app.set과의 차이점: app.set은 서버 내내 유지, req, res는 요청 하나 동안만 유지
-
req.body나 req.cookies같은 미들웨어의 데이터와 겹치지 않게 조심
app.use((req, res, next) => { req.data = "데이터 넣기"; next(); }, (req, res, next) => { console.log(req.data); // 데이터 받기 next(); })
위와 같이 첫번째 미들웨어가 있고 그 다음에 미들웨어가 있다..
또는 라우터가 다른 거여도 됩니다.let hello; app.use((req, res, next) => { // 여기에서 // 데이터를 보내고 싶을 때 실수를 제일 많이하시는 것이 변수를 사용하는 것 hello = "asdf"; }) app.get('/', (req, res, next) => { // 여기로 데이터를 보내고 싶을 때 console.log(hello); res.sendFile(path.join(__dirname, "index.html")); })
위와 같이하면 큰일납니다. 위의 hello는 전역변수이기 때문.
어떤 사람이 요청할 때 "비밀번호"를 넣어 요청을 했다면,let hello; app.use((req, res, next) => { // 여기에서 // 데이터를 보내고 싶을 때 실수를 제일 많이하시는 것이 변수를 사용하는 것 hello = "비밀번호"; }) app.get('/', (req, res, next) => { // 여기로 데이터를 보내고 싶을 때 console.log(hello); res.sendFile(path.join(__dirname, "index.html")); })
그럼 다른 사람이 해당
hello
변수를 통해 "비밀번호"를 볼 수도 있습니다.
위와 같이 하시면 큰일납니다.app.use((req, res, next) => { // 여기에서 app.set('hello', '비밀번호'); // set을 통해 데이터를 공유하는 것도 절대 안됩니다. }) app.get('/', (req, res, next) => { // 여기로 데이터를 보내고 싶을 때 app.get('hello'); // 위의 set('hello', '비밀번호')를 가져옴 res.sendFile(path.join(__dirname, "index.html")); })
데이터 공유를 위해
app.set()
이걸 쓰면 진짜로 큰일납니다.
물론 공유돼도 되는 데이터면 문제없겠지만..
여튼 위와 같은변수
또는set
을 사용하면 전체가 다 공유된다는 것.그래서 제가 실무에서 한 실수 중에 제일 큰 실수가 로그인하는 거를 실수로 전체 공유를 해가지고..
req.session
을 전체 공유를 해가지고 제가 로그인을 했는데, 다른 사람이 접속했는데 그 사람이 저로 로그인이 되어있는겁니다.
이런 실수를 한 적이 있습니다. 실제 서비스에서.
그럼 어떻게 해결해야되느냐
-
방법 1
app.use((req, res, next) => { // 여기에서 req.session.data = "비밀번호"; }) app.get('/', (req, res, next) => { // 여기로 데이터를 보내고 싶을 때 req.session.data // 비밀번호 res.sendFile(path.join(__dirname, "index.html")); })
방법 1의 단점. session에 저장하는 거니깐 다음 요청 때도 이 데이터가 남아있습니다.
이렇게 영구적으로 남게하고 싶은 것이 아니라 1회성. 이 요청 한번에서만 데이터가 남게하고 싶다.
그럼 방법 2 -
방법 2
app.use((req, res, next) => { // 여기에서 req.data = "비밀번호"; }) app.get('/', (req, res, next) => { // 여기로 데이터를 보내고 싶을 때 req.data // 비밀번호 res.sendFile(path.join(__dirname, "index.html")); })
위와 같이 넣는 걸 추천.
첫번째 라우터의 req와 두번째 라우터의 req는 같은 req거든요?
그래서 위와 같이req.data
로 받아올 수 있음.위의 라우터에서
next()
가 없기 때문에 해당 라우터가 실행되고 끝나면 거기서 종료되기 때문에 그러면req.data
가 메모리에서 정리되기 때문에 안전하게 사용하실 수 있습니다.
요청 한번만 할 때는req.data
.
(next()
가 없기 때문에 해당 라우터가 실행되고 끝나면 거기서 종료되기 때문에 그러면req.data
가 메모리에서 정리되기 때문에 안전..? 이게 무슨말이지…? 이해가 안된다… 음..)반대로 나에 한해서만 요청을 자꾸 많이 보내도 ‘나'라는 걸 기억하게 하고싶다면, 그렇게 계속 유지하고 싶은 데이터는
req.session.어쩌구
에 넣으면됨.
7. - 미들웨어 확장하기 -
-
미들웨어 안에 미들웨어를 넣는 방법
-
아래 두 코드는 동일한 역할
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') { margan("combined")(req, res, next); } else { morgan('dev')(req, res, next); } })
app.use(morgan('combined'));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session());
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) => {
express.static(path.join(__dirname, 'public'))(req, res, next)
});
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.get('/', (req, res, next) => {
res.sendFile(path.join(__dirname, "index.html"));
})
app.use('/', express.static(path.join(__dirname, 'public')));
을 위 위치에 넣는다고 하면, 아까 로그인한 사람한테는 static
을 실행하고 싶고, 로그인 하지 않은 사람한테는 static
을 안 실행하고 싶다면..
그런 경우가 있다고 그랬죠?
로그인 한 사람한테만 그 사진을 보여줘야될거아니에요?
예를 들어, 구글 드라이브, 구글 포토, 아이클라우드 이런 것들.
// 이럴 땐 아래와 같은 패턴이 아니라
app.use('/', express.static(path.join(__dirname, 'public')));
// 이런 패턴으로 넣으면됨. 미들웨어 안에 미들웨어.
app.use('/', (req, res, next) => {
express.static(path.join(__dirname, 'public'))(req, res, next)
});
이 패턴은 잘 익혀두세요.
이 패턴은 알아두면 express
개발이 매우 편해집니다.
이게 미들웨어 확장법입니다. (어우.. 이것도 뭔소리인지 잘 모르겠다.. 일단 하다보면 이해가되겠지..?)
app.use(morgan('combined'));
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session());
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) => {
// 그럼 이렇게 if 문으로.. 로그인 했으면 id가 있거나 그러겠죠? 나중에 로그인 구현할 때 해보겠지만.
// 로그인을 해서 세션에 id가 있다면
if (req.session.id) {
// 그러면 static 미들웨어 실행
express.static(path.join(__dirname, 'public'))(req, res, next)
} else { // 만약 session.id가 없다면
next(); // 그러면 그냥 next하면 됩니다.
}
});
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.get('/', (req, res, next) => {
res.sendFile(path.join(__dirname, "index.html"));
})
위와 같이 작성하면 로그인을 했다면 express static
으로 사진, 파일, 또는 데이터 이런거를 프론트로 전달을 해줄 것이고 내가 로그인을 안했다면 그냥 next()
해서 다음걸로 넘어가겠죠.
이게 미들웨어 확장법인데 이건 알아두시면 아주 편리합니다.
콜스
라는 미들웨어가 있거든요?
나중에 콜스
라는 미들웨어 쓸 때도 이 미들웨어 확장법을 쓰고 그 다음에 패스포트
라는 것을 쓸 때도 이 미들웨어 확장법을 씁니다.
저는 미들웨어 확장법을 즐겨쓰기 때문에 제가 만든 미들웨어 안에 어떤 다른 미들웨어, 남의 미들웨어를 넣고 뒤에 (req, res, next)
이것만 붙여주시면됩니다.
그러면 확장이 됩니다.
그러면 바깥에 다른 로직을 쓸 수가 있습니다. (if
문..)
6.8 express-session 미들웨어
-
세션 관리용 미들웨어
app.use(session({ resave: false, saveUninitialized: false, secret: process.env.COOKIE_SECRET, cookie: { httpOnly: true, secure: false, }, name: "session-cookie", })) req.session.name = 'zerocho'; // 세션 등록 req.sessionID; // 세션 아이디 확인 req.session.destroy(); // 세션 모두 제거
- 세션 쿠키에 대한 설정(secret: 쿠키 암호화, cookie: 세션 쿠키 옵션)
- 세션 쿠키는 앞에 s%3A가 붙은 후 암호화되어 프런트에 전송됨
- resave: 요청이 왔을 때 세션에 수정사항이 생기지 않아도 다시 저장할지 여부
- saveUninitialized: 세션에 저장할 내역이 없더라도 세션을 저장할지
- req.session.save로 수동 저장도 가능하지만 할 일 거의 없음
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');
app.set('port', process.env.PORT || 3000);
app.use('/', express.static(__dirname, 'public'));
app.use(morgan('dev'));
app.use(cookieParser('zerochopassword'));
app.use(session()); // 세션을 이렇게 넣으면
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res, next) => {
req.session // req.session이라는 것이 생깁니다.
res.sendFile(path.join(__dirname, 'index.html'));
})
위의 const session = {}
이 역할을 req.session
이 하신다고 보면 됩니다.
req.session
의 좋은 점은 session[uniqueInt]
이렇게 할 필요없이 단순하게 req.session
이렇게만 적어줘도 그 사용자에 대한 세션이 됩니다.
req.session
이 자체가 이미 그 사용자에 대한 고유한 세션이 됩니다.
app.get('/', (req, res, next) => {
req.session.id = 'hello';
res.sendFile(path.join(__dirname, 'index.html'));
})
만약, 위와 같이 req.session.id
에 hello
를 넣었다면, 이는 모든 사용자의 id가 hello가 되는 것이 아니라 방금 요청을 보낸 사용자만 hello가 됩니다.
이런 식으로 각 개개인의 저장공간을 만들 수 있습니다.
이렇게 개인의 요청마다 개인의 저장공간을 만들어주는게 express-session
이라고 보시면 됩니다.
- express-session 옵션 -
이 세션엔 다양한 옵션들을 넣을 수가 있습니다.
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');
app.set('port', process.env.PORT || 3000);
app.use('/', express.static(__dirname, 'public'));
app.use(morgan('dev'));
app.use(cookieParser('zerochopassword'));
app.use(session({
// 보통 resave와 saveUninitialized는 false로 많이 설정합니다.
resave: false,
saveUninitialized: false,
secret: 'zerochopassword', // secret은 쿠키와 똑같습니다.
// 저는 보통 cookieParser 의 쿠키랑 secret 이랑 같게 만들어두는 편입니다.
// 세션인데 왜 쿠키를 쓰지? 이렇게 말씀하시면 안됩니다. 그럼 벌써 4강 까먹으신거거든요.
// 세션일 때 항상 세션, 쿠키를 쓰죠?
// 세션 쿠키에 대한 설정을 하실 수 있는데 세션 쿠키 설정은 httpOnly는 항상 true로 하라고 그랬죠?
// 그래야지 자바스크립트로 공격을 안 당하기 때문에.
cookie: {
httpOnly: true,
},
name: 'connect.sid', // 보통 name은 connect.sid라고 되어있습니다.
// 나중에 보게되실텐데 sessionCookie 로 설정할 수도 있습니다.
// 기본값은 connect.sid 라는 것
// 그리고 서명(signed)되어있기 때문에 읽을 수 없는 문자열로 바뀌어있다는 것.
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res, next) => {
req.session // req.session이라는 것이 생깁니다.
res.sendFile(path.join(__dirname, 'index.html'));
})
6.9 multer 사용하기
6.9.1 멀티파트 데이터 형식
-
form
태그의enctype
이multipart/form-data
인 경우- body-parser로는 요청 본문을 해석할 수 없음
- multer 패키지 필요
제가 body-parser
, 또는 express.json()
이나 express.urlencoded()
로 form 요청 본문을 해석할 수가 있다고 그랬잖아요?
그런데 form
태그의 enctype
이 multipart/form-data
인 경우가 있습니다.
<!-- 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>
위와 같이 enctype
을 multipart/form-data
로 설정해놓는 경우가 있거든요?
이게 바로 이미지나 파일 동영상 같은 것을 업로드할 때입니다.
위의 input type=file
을 클릭하면 파일 선택창이 떠서 업로드할 파일을 선택할 수 있죠?
그렇게 파일을 업로드하는 경우엔 enctype
을 multipart/form-data
로 설정해놓습니다.
이 때는 body-parser
나 express.json()
이나 express.urlencoded()
로는 요청의 바디, 본문을 해석할 수가 없어서 multer
라는 패키지를 사용해야됩니다.
특히 form 데이터를 한번 서버로 전송을 해보시면 요청에 request fail load에 json이 들어가있는 게 아니라 위와 같은식의 데이터라 들어가있습니다.
String 같기도하고 중간중간 공백도 있고.. 뭔지 모르겠죠?
저런 것들을 서버쪽에서 해석하려면 multer
를 사용해야됩니다.
직접 해석하실 수도 있는데 워낙 복잡하기 때문에 multer
사용하시는 것을 추천드립니다.
npm i multer
multer
를 설치하신 후에는
const multer = require('multer');
이렇게 multer
를 불러옵니다.
multer
자체가 미들웨어라기 보다는 multer
함수를 호출하면 호출한 함수 안에 4가지의 미들웨어가 들어있습니다.
// multer 임포트
const multer = require('multer');
// uploads 폴더가 없으면 uploads 폴더를 생성합니다.
try {
// 서버 시작전에 실행될 코드라 readdirSync 메소드를 사용했습니다.
// 서버에선 sync 사용하지 말라고 했지만 서버 시작전에 uploads 폴더가 존재하는지 찾고 없으면 만들고.. 이러한 것들은 sync 쓰셔도됩니다.
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
fs.mkdirSync('uploads');
}
// multer 함수를 호출해 upload 변수에 담습니다.
const upload = multer({
// 대표적으로 storage와 limits라는 옵션 두가지가 있습니다.
// 옵션은 더 많기는 한데 자주 쓰이는 옵션은 이렇게 두개가 있습니다.
// storage는 업로드한 파일을 어디에 저장할 건지를 선택할 수 있습니다.
// 기본적으로는 diskStorage - 하드 disk에다 저장할 수도 있고
// 아니면 메모리에다 저장할 수도 있습니다.
// 메모리에 저장하면 서버를 껐다가 키면 이미지가 날아가겠죠?
// 그래서 메모리에 저장하는 경우는 극히 드물긴한데 가능은 합니다.
// 왜냐하면 잠깐 내 서버에 있다가 다른 서버로 옮겨줄 때, 잠깐 업로드용으로 쓰이기도 하기 때문에 memoryStorage도 존재하고
// 그 다음에 클라우드 저장소있죠? 거기다가 바로 보내는 패키지들도 존재합니다.
// 이와 관련된 내용들은 15, 16장에서 알아보고
// 기본적으로는 diskStorage를 사용할겁니다.
storage: multer.diskStorage({
// destination은 어디에다 저장할지
destination(req, file, done) {
// 현재 폴더의 uploads 폴더에 저장하겠다고 설정한것.
// 이렇게한 경우엔 uploads 폴더를 만들어 줘야겠죠? 위의 try - catch문으로.
// uploads 폴더가 없으면 에러가 나기 때문에 위의 try - catch 문으로 미리 uploads 폴더가 있는지 없는지 검사하고 없으면 만들어주는겁니다.
// try catch 문에서 uploads 폴더를 만드는 시점은 서버 시작전이기때문에 readdirSync 메소드를 써도됩니다.
done(null, 'uploads/');
},
// 어떤 이름으로 올릴지
filename(req, file, done) {
// extname으로 확장자 추출
const ext = path.extname(file.originalname);
// 파일이름 + 날짜 + 확장자 - 날짜를 파일이름과 확장자 사이에 껴넣었는데 이거를 껴놓는 이유는..
// hyungju-lee.png를 A라는 사람이 올리고 그 다음에 B라는 사람이 hyungju-lee.png 파일을 올리면
// B라는 사람이 올린 hyungju-lee.png 파일이 A라는 사람이 올린 hyungju-lee.png 파일을 덮어씌워버립니다.
// 그러면 A라는 사람은 나중에 자기가올린 hyungju-lee.png 파일을 찾아보려고했는데 자기가 올린 hyungju-lee.png 파일이 아니라
// B가 올린 hyungju-lee.png 파일이 보이겠죠?
// 이렇게 이름이 같으면 덮어씌워지기 때문에 그렇게 덮어씌워지는 것을 막고자 현재시간을 붙여준겁니다.
// 이게 Date.now()라고해서 무조건 안 겹친다. 그런건 아니죠. 동시에 같은 이름으로 올릴 가능성도 있긴한데, 실제로는 매우 드뭅니다.
// 왜냐하면 Date.now()는 밀리초 단위까지 기록을 하기 때문입니다. 설마 밀리초까지 같지는 않겠죠.
done(null, path.basename(file.originalname, ext) + Date.now() + ext);
},
// 그리고 위의 destination과 filename에 모두 done 이란 함수가 있는데,
// 첫번째 인자는 보통 null로하고 두번째 인자에 값을 넣어줍니다.
// 첫번째 자리에 넣는 경우는 에러가 났을 때.
// 에러가 났을 때 에러처리 미들웨어로 넘기려면 done 함수의 첫번째 인자에 에러를 넣어줍니다.
// 그게 아니라면 두번째 인자에 성공할 때 값을 넣어주시면 됩니다.
}),
// limits는 fileSize나 파일 갯수 등을 넣을 수 있습니다.
// 여기선 fileSize만 쓰고 있습니다.
// 5mb 이하의 파일만 업로드할 수 있게 한다는 뜻임.
// 5mb 이상의 파일을 올리면 400번대 에러가 날거에요.
// 동영상이면 5mb로는 너무 부족하니까 조금 더 늘려주시던가 그런 작업이 추가로 필요하겠죠?
limits: { fileSize: 5 * 1024 * 1024 },
});
app.post('/upload', upload.single('image'), (req, res) => {
console.log(req.file);
res.send('ok');
});
위와 같이 multer
를 호출하고 호출한 결과물을 upload
변수에 담고..
upload.single('image')
이렇게 upload
안에 single
이라는 미들웨어가 들어가 있습니다.
또 array
라는 미들웨어도 들어가있고 그렇습니다.
그럼 일단 위와 같이 multer
함수 호출하는 것부터 우선 보겠습니다.
위의 코드 설명을 읽어보세요.
위와 같이 multer
설정. 어디다가 어떻게 올린건지를 설정을하면 upload
변수에 객체가 담기거든요?
그럼 upload
에 담긴 객체를
app.post('/upload', upload.single('image'), (req, res) => {
console.log(req.file);
res.send('ok');
});
이렇게 라우터에 장착을 하시면 됩니다.
app.use(upload.single("image"));
왜 위와 같이 작성 안하는가.
위와 같이 작성해주셔도 됩니다.
app.use(upload.single("image"));
app.post('/upload', (req, res) => {
console.log(req.file);
res.send('ok');
});
이렇게 작성하셔도 되는데 보통 이미지 업로드 같은 경우는 모든 라우터에서 일어나는 것이 아니라 특정 라우터에서만 일어나잖아요?
특정 라우터에서만 일어나는 애들은
app.post('/upload', upload.single('image'), (req, res) => {
console.log(req.file);
res.send('ok');
});
이런식으로 해주는 경우가 더 많습니다.
한 라우터에서만 미들웨어 적용하기 위해서.
app.use(morgan('combined'));
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(express.json());
app.use(express.urlencoded({ extended: false }));
이런거 있죠?
업로드하는.. form 전송, 데이터 전송하는 라우터에만 연결해줘도 무방할 거 같긴 합니다.
그래서 위 미들웨어를 어디에다 위치를 시킬지 그런거는 여러분들의 자유고, 성능 같은걸 체크해보시면서 결정하시면 됩니다.
- single, multiple -
- single -
app.post('/upload', upload.single('image'), (req, res) => {
console.log(req.file);
res.send('ok');
});
- 기본적으로 알아볼건
upload.single('image')
인데, 이는 한 개의 파일만 업로드할 때. -
upload.single('image')
의 image는 아래에서 나온 것.
한 개의 파일만 업로드할 땐 아래와 같이 form 요소가 구성되어있겠죠?<form id="form" action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="image"/> <!-- 여기의 name과 upload.single() 여기의 인자가 일치해야됨 --> <button type="submit">업로드</button> </form> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> document.getElementById('form').addEventListener('submit', (e) => { e.preventDefault(); const formData = new FormData(); formData.append('image', e.target.image.files[0]) axios.post('/upload', formData); }); </script>
// 이렇게 하나만 업로드할 때는 upload.single('image')에서 업로드를 마무리 해주고 app.post('/upload', upload.single('image'), (req, res) => { // 업로드한거에 대한 정보를 req.file에 넣어줍니다. // 아래와 같이 console.log(req.file)하시면 업로드된 파일 정보가 뜰거임. console.log(req.file); res.send('ok'); });
이런식으로.
- single + title data -
-
참고로 파일만 전송되는 것이 아니라 동시에 데이터도 전송할 수 있음.
<form id="form" action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="image"/> <!-- 여기의 name과 upload.single() 여기의 인자가 일치해야됨 --> <input type="text" name="title"/> <!-- 아래 자바스크립트에서 title을 보내는 것처럼 html도 똑같이 만들 수 있음, 보통 똑같이 써줌. --> <button type="submit">업로드</button> </form> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> document.getElementById('form').addEventListener('submit', (e) => { e.preventDefault(); const formData = new FormData(); // 아래와 같은 식으로 데이터를 전송할 수 있음. formData.append('image', e.target.image.files[0]) // 이미지는 이런식으로 formData.append('title', e.target.title.value) // 타이틀은 req.file로 가는게 아니라 일반 String이기 때문에 req.body.title로 갑니다. // multer가 req.file도 파싱해주고 req.body도 동시에 파싱해주는 것. axios.post('/upload', formData); }); </script>
자바스크립트를 통해
formData
를 통해 multipart 데이터를 보낼 수 있음.
- multiple 1 -
-
아래와 같이 multiple인 경우가 있음.
<form id="form" action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="image" multiple/> <!-- 이렇게 속성이 multiple인 경우는 이거 하나로 여러개를 업로드할 수 있음 --> <!-- multiple인 경우 input type=file에다 파일을 여러개를 올릴 수 있는데 --> <input type="text" name="title"/> <button type="submit">업로드</button> </form> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> document.getElementById('form').addEventListener('submit', (e) => { e.preventDefault(); const formData = new FormData(); // 자바스크립트에서도 비슷하게 여러개를 올릴 수 있음 formData.append('image', e.target.image.files[0]) formData.append('image', e.target.image.files[1]) formData.append('image', e.target.image.files[2]) formData.append('image', e.target.image.files[3]) formData.append('title', e.target.title.value) axios.post('/upload', formData); }); </script>
// 위와 같을 때는 서버에서 아래와 같이.. single은 하나만 받기 때문에, array로 받아야됩니다. app.post('/upload', upload.array('image'), (req, res) => { // array로 받으면 여러개이기 때문에 req.file's' 안에 업로드된 정보들이 들어있습니다. console.log(req.files); res.send('ok'); });
이러한 정보들이 배열로 담기는 겁니다.
- multiple 2 -
-
multiple이긴 한데 약간 다른 거.
아래와 같이 여러개를 보내긴 하는데
multiple
속성이 아니라type=file
이 여러개인 경우.
이 또한 여러개를 보내는 것.
name
들이 달라질 뿐.<form id="form" action="/upload" method="post" enctype="multipart/form-data"> <input type="file" name="image1"/> <input type="file" name="image2"/> <input type="file" name="image3"/> <input type="text" name="title"/> <button type="submit">업로드</button> </form> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> document.getElementById('form').addEventListener('submit', (e) => { e.preventDefault(); const formData = new FormData(); formData.append('image1', e.target.image.files[0]) formData.append('image2', e.target.image.files[1]) formData.append('image3', e.target.image.files[2]) formData.append('title', e.target.title.value) axios.post('/upload', formData); }); </script>
이럴 때는 서버에서 받는 형식이 좀 달라집니다.
// array가 아닌 fields 메소드 사용 // 배열 안에 객체형태로 담고 // 각각 설정을 해줄 수 있음 - limits: 5 - image1은 이미지 5장까지 된다. image2는 무제한, image3도 무제한 app.post('/upload', upload.fields([{name: 'image1', limits: 5}, {name: 'image2'}, {name: 'image3'}]), (req, res) => { // 이렇게 각각 담김 console.log(req.files.image1); console.log(req.files.image2); console.log(req.files.image3); res.send('ok'); });
single
,array
,fields
순으로 점점 더 복잡해짐.
- upload.none() -
-
upload.none()
app.post('/upload', upload.none(), (req, res) => { res.send('ok'); });
이는 파일 업로드 안할 때.
파일 업로드 안하는데 왜 이걸 쓰지?
저도 거의 쓰진 않지만 쓸 때가 있긴 함.<form id="form" action="/upload" method="post" enctype="multipart/form-data"> <input type="text" name="title"/> <button type="submit">업로드</button> </form> <script src="https://unpkg.com/axios/dist/axios.min.js"></script> <script> document.getElementById('form').addEventListener('submit', (e) => { e.preventDefault(); const formData = new FormData(); formData.append('title', e.target.title.value) axios.post('/upload', formData); }); </script>
위와 같이 파일 올리는게 하나도 없는데, 일반 form 처럼해서 업로드를 하는데 form만
enctype="multipart/form-data"
이거일 때.
위와 같을 땐 이미지 업로드를 안해도 데이터 자체는multipart
데이터로 넘어감.
그리고new FormData();
를 썼을 때.이럴 때가 존재할 수 있기 때문에.. 이론적으로 존재할 수 있잖아요?
이미지를 업로드하지 않는데enctype
은multipart/form-data
로 했을 때.그럴 때도 데이터는 위와 같은식으로 감.
위와 같은식으로 가면 해석이 안되니깐 이런 것들도multer
로 해석을 해줘야돼고, 그런거는upload.none()
으로.app.post('/upload', upload.none(), (req, res) => { // 다만, upload.none()이기 때문에 req.file은 없음 // 대신 req.body.title만 들어가 있음. 위에 title은 보내니깐. res.send('ok'); });
s3
랑 같이 해보는거는 15장. 배포할 때s3
에 배포하면서 해보도록 하겠습니다.
- 정리 -
- multer 설정하기 -
-
multer 함수를 호출
- storage는 저장할 공간에 대한 정보
- distStorage는 하드디스크에 업로드 파일을 저장한다는 것
- destination은 저장할 경로
- filename은 저장할 파일명(파일명 + 날짜 + 확장자 형식)
-
limits는 파일 개수나 파일 사이즈를 제한할 수 있음
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} })
-
실제 서버 운영시에는 서버 디스크 대신에
S3
같은 스토리지 서비스에 저장하는 게 좋음- Storage 설정만 바꿔주면 됨
- multer 미들웨어들 -
-
single과 none, array, fields 미들웨어 존재
- single은 하나의 파일을 업로드할 때, none은 파일은 업로드하지 않을 때
-
req.file 안에 업로드 정보 저장
app.post('/upload', upload.single('image'), (req, res) => { console.log(req.file, req.body); res.send('ok'); }); app.post('/upload', upload.none(), (req, res) => { console.log(req.body); res.send('ok'); })
- array와 fields는 여러 개의 파일을 업로드할 때 사용
- array는 하나의 요청 body 이름 아래 여러 파일이 있는 경우
- fields는 여러 개의 요청 body 이름 아래 파일이 하나씩 있는 경우
-
두 경우 모두 업로드된 정보가 req.files 아래에 존재
app.post('/upload', upload.array('many'), (req, res) => { console.log(req.files, req.body); res.send('ok'); })
app.post('/upload', upload.fields([{name: 'image1'}, {name: 'image2'}]), (req, res) => { console.log(req.files, req.body); res.send('ok'); })
6.10 dotenv 사용하기
이번에는 dotenv 알려드리도록 하겠습니다.
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
저희가 미들웨어들을 설치를 하면서 사실 dotenv
도 설치를 했는데, dotenv
는 미들웨어는 아니고 저희의 비밀 키 같은 거를 관리할 수 있는..
엄격하게 말하면 꼭 비밀 키 뿐만은 아닙니다.
환경변수처럼 시스템에 따른 설정들 이런 것들도 다 조절을 할 수가 있습니다.
process.env.UV_THREADPOOL_SIZE
예를 들어 3강에서 배운 UV_THREADPOOL_SIZE
의 설정을 바꿨더니 노드 관련 설정이 바뀌었었죠?
- 이렇게 노드 관련 설정도 조절할 수 있고
- 다양한 값들도 저장하고
- 값들 중에는 비밀키도 있고
이런거 관리하는 것이 dotenv
라고 보시면 됨.
6.10.1 dotenv
-
.env
파일을 읽어서process.env
로 만듦- dot(점) + env
- process.env.COOKIE_SECRET에 cookiesecret 값이 할당됨(키=값 형식)
- 비밀 키들을 소스 코드에 그대로 적어두면 소스 코드가 유출되었을 때 비밀 키도 같이 유출됨
.env
파일에 비밀 키들을 모아두고.env
파일만 잘 관리하면 됨
- dotenv 설치 -
npm i dotenv
- 비밀 키 -
저희 코드 중에 비밀키가 하나 있죠?
위 부분들은 쿠키들을 암호화하는 키입니다.
암호화가 아니라 정확히는 서명.
쿠키를 서명하는 키인데 이 키를 만약 다른 사람들이 안다면?
그럼 이 키를 사용해서 마치 제가 서명한 것처럼 쿠키를 위조할 수가 있다.
그 쿠키를 갖고 마치 제가 로그인한 것처럼 속일 수가 있는겁니다.
이 키는 저의 사인과 마찬가지입니다.
어떤 공식문서에 사인 같은게 위조되면 마치 나인것처럼 행세할 수 있잖아요? 인감도장을 위조하듯이.
그래서 이러한 키는 중요하게 관리를 해야되는데 지금 문제는 이러한 비밀 키가 제 소스 코드에 그대로 묻어나있다는 것.
이게 왜 위험이 되냐면 소스코드에 제 키가 그대로 들어있으면 이 소스코드가 털릴 때 제 키도 같이 털리겠죠?
안 그래도 소스코드만 유출되도 보안적으로 어떤 문제가 생길까 걱정되는데, 그 안에 제 비밀 키까지 다 들어있다는거는 거의 프로젝트를 파기하고 폭파하고 아예 새로 시작해야되는..
그런 정도의 문제가 됩니다.
그래서 소스 코드는 유출되더라도 키라도 어떻게 관리할 수 없을까.
그래서 나온게 다양한 방법들이 있는데 그 중 하나가 이런 키들을 환경변수에다가 숨겨놓는 방법입니다.
app.use(morgan('combined'));
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',
}));
그래서 위와 같은 식으로.
그래서 실제로 위의 소스 코드가 털려도 비밀 키가 털리는게 아니므로 한가지 시름을 덜 수가 있겠죠.
소스 코드는 털렸지만 적어도 비밀 키들은 털리지 않았다.
어떤 비밀 키는 예를들어 구글 지도, 이런거 할 수 있는데, 그런거는 사용량에 따라서 요금이 부과되는데 제 비밀키가 털리면 해커가 그 키로 구글지도를 사용해서 나중에 엄청나게 돈이 청구된다던가..
이용은 그 해커가 하고 부담은 제가 내는 식으로.
이런 경우도 많습니다.
아마존에서 실제로 비밀키가 털리면 아마존 웹 서비스에 컴퓨터 같은거를 마구마구 생성할 수가 있는데 그래서 그걸로 비트코인 채굴하고, 돈은 해커가아니라 제가내고.
이런 악용 사례들이 실제로 있었습니다.
그래서 비밀키를 소스코드에 넣지 않는거. 그것도 일종의 보안책이 될 수 있습니다.
그럼 process.env.COOKIE_SECRET
이거를 어떻게 실제값으로 넣어주냐.
그것도 중요하잖아요?
- .env 파일 생성 -
COOKIE_SECRET=cookiesecret
DB_PASSWORD=nodejsbook
생성한 .env
파일에 위와 같이 작성해줍니다.
위와 같이 여러개 작성할 수도 있습니다.
위와 같이 작성하면
- COOKIE_SECRET은 process.env.COOKIE_SECRET
- DB_PASSWORD은 process.env.DB_PASSWORD
이렇게 대체되어서 값이 들어갑니다.
그리고 위 코드 작성하실 땐 뒤에 ;(세미콜론) 작성하시면 안됩니다.
자바스크립트 작성습관 때문에 가끔 세미콜론 붙이시는 분도 계신데 붙이시면 안됩니다.
이렇게하면 소스코드가 털려도 비밀 키는 안 털리니깐 일단 한시름 덜었고, .env
파일만 잘 관리하면 됩니다.
그리고 모든 비밀키들이 .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');
const dotenv = require('dotenv');
dotenv.config(); // 이 코드는 소스코드 상위에 작성하는 것이 좋습니다.
// 소스코드에 작성된 process.env.COOKIE_SECRET 이 코드들보다 위에 위치해야 제대로 값이 대체됨.
const app = express();
app.set('port', process.env.PORT || 3000);
app.use(morgan('combined'));
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',
}));
이런식으로 불러옵니다.
만약 morgan
이 process.env
에 따라 작동방식이 달라진다면?
아래와 같이 위치를 변경.
const express = require('express');
dotenv.config(); // 위치 주의!
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');
const path = require('path');
const dotenv = require('dotenv');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use(morgan('combined'));
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',
}));
- .env 파일이 털리면 끝아냐? -
맞습니다. .env
파일 털리면 끝입니다.
그리고 비밀키들을 다 모아두면 더 위험한거 아니냐.
맞습니다.
하지만 소스코드에 직접 넣어서 비밀키들이 흩어져있더라도 소스코드 털리면 끝입니다. 비밀키들이 흩어져있던 가운데 모아져있던.
그래서 모아져있냐 흩어져있냐 이런건 중요한 게 아니고
.env
파일 자체가 털리면 어떻게하냐.
이게 제일 문제입니다.
그래서 .env
는 실제로 파일로 만들어두긴 하지만 소스코드 관리하는 git
같은 곳에 올리진 않습니다.
드라이브나 클라우드에 .env
파일은 절대로 올리시면 안됩니다.
이거 올려서 똑같이 해커들에게 털려버리면 아무 의미가 없잖아요?
그래서 이런건 올리시면 안돼고, 비밀스럽게 멤버들간 공유를 하셔야됩니다.
그리고 권한마다 이 코드를 다르게 두셔야됩니다.
소스코드를 터는 사람들이 해커들도 있겠지만, 또 누가 있을까요?
내부 직원들이 퇴사할 때에도 소스코드를 들고갈 확률이 있거든요?
소스 코드를 들고갈 수도 있고 이런 비밀키들을 들고갈 수도 있으니까 권한별로 공유내용을 달리 해야됩니다.
- 일반 직원에게는 DB를 읽기권한밖에 없는 낮은권한 키를 공유한다던가
- 관리자에게는 모든 권한을 할 수 있는 키를 준다던가
- 그 사람이 퇴사하면 해당 비밀번호를 바꾸는 프로세스를 두셔야됩니다.
.env
를 활용해 소스 코드 내부에 비밀키를 두지 않는 것은 1차 보안이라고 생각하시면돼고
실제로 이런 비밀키들을 지키기 위해선 많은 연구가 필요합니다.
이것도 정말 머리를 썪히는 과제중 하나입니다.
그래서 이는 회사마다 다른데 일단은 소스코드에다 비밀키를 넣지마라. 적어도 .env
로 분리하고 2차적으로는 .env
이 파일은 서로 공유도하지말고 업로드도 하지말고 개개인별로 권한 다르게해서 다르게 나눠줘라.
왜냐하면 그 사람이 퇴사하면 비밀키까지 들고나갈 수도 있으니깐.
어떤 사람이 퇴사하면 .env
를 폐기하거나 바꾸거나.
즉, 회사에서 퇴사 프로세스도 잘 만들어두는게 좋습니다.
여튼 이상 비밀키 관리의 첫 역할을 하는 dotenv
배워봤습니다.
6.11 라우터 분리하기
app.get, app.post 처럼 메소드와 url이 있는 것들을 라우터라고 부르고 있는데 그 라우터들을 현재 app.js
파일에 아래와 같이
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('combined'));
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'), '번 포트에서 대기 중');
});
이렇게 작성하고 있는데, 만약에 라우터들이 수백개가 된다면 어떻게 해야될까요?
대규모 프로젝트에서 그러면 app.js
파일 길이가 엄청 길어지겠죠?
그러면 보기에도 별로고 관리도 어려우니깐 나중가면 위 코드를 분리를 해줍니다.
const indexRouter = require('./routes');
const userRouter = require('./routes/user');
app.use('/', indexRouter);
app.use('/user', userRouter);
이런식으로 분리하는데, 이를 라우팅 분리라고 부릅니다.
- routes 폴더 생성 -
|- 프로젝트폴더
|- routes
|- index.js
|- user.js
|- .env
|- app.js
|- package.json
|- package-lock.json
위와 같이 routes 폴더 안에 index.js, user.js 파일을 생성합니다.
// routes/index.js
const express = require('express');
const router = express.Router();
// GET / 라우터
router.get('/', (req, res) => {
res.send('Hello, Express');
});
module.exports = router;
// routes/user.js
const express = require('express');
const router = express.Router();
// GET /user 라우터
router.get('/', (req, res) => {
res.send('Hello, User');
});
module.exports = router;
위 파일들을
const indexRouter = require('./routes');
const userRouter = require('./routes/user');
app.use('/', indexRouter);
app.use('/user', userRouter);
이렇게 불러오는겁니다.
그럼 app.use('/')
여기의 슬래쉬랑 routes/index.js
파일의 router.get('/')
슬래쉬랑 합쳐져서 그냥 GET 슬래쉬
라우터가 되는 겁니다.
그럼 user.js 파일은 어떻게 되는걸까요? app.js
의 /user
와 routes/user.js
의 /
슬래쉬가 만나 /user/
가 되는 겁니다. 즉, 그래서 GET /user/
요청이 되는겁니다.
그래서 routes/user.js
에 /
만 있어도 app.js
의 /user
와 합쳐지기 때문에 /user/
요청으로 되는 겁니다.
이렇게 파일을 분리시킬 수 있습니다.
const router = express.Router();
이렇게 라우터 객체 생성 후 거기에다 .get()
메소드를 붙여주시면됩니다.
그리고 그걸 다시 module.exports
합니다.
즉, 라우터들을 분리하고 app.js
에서 다시 합쳐주는셈이죠?
이런식으로 라우터 분리를 할 수 있되, url(주소) 적는것만 주의를 해주시면 된다는 거.
- app.js가 길어질 때마다 라우터 분리를 이와 같이 해주면 되고
- 404 에러처리 미들웨어
- 500번대 에러처리 미들웨어
… 등등
이렇게 작성하시면 하나의 express 어플리케이션이 완성이 되는겁니다.
6.11.1 express.Router
-
app.js가 길어지는 것을 막을 수 있음
-
userRouter의 get은 /user와 /가 합쳐져서 GET /user/가 됨
// routes/index.js const express = require('express'); const router = express.Router(); // GET / 라우터 router.get('/', (req, res) => { res.send('Hello, Express'); }) module.exports = router;
// routes/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 // ... const path = require('path'); dotenv.config(); const indexRouter = require('./routes'); const userRouter = require('./routes/user'); // ... app.use('/', indexRouter); app.use('/user', userRouter); app.use((req, res, next) => { res.status(404).send('Not Found'); }) // ...
-
6.11.2 라우트 매개변수
-
:id
를 넣으면req.params.id
로 받을 수 있음-
동적으로 변하는 부분을 라우트 매개변수로 만듦
// 제가 와일드 카드라고 불렀던거 있잖아요? // 정확한 명칭은 :id가 라우트 매개변수이고 *이 와일드카드입니다. // :id는 매개변수(=파라미터) - 그래서 params라고 보시면 됩니다. router.get('/user/:id', function (req, res) { console.log(req.params, req.query); })
-
일반 라우터보다 뒤에 위치해야함
라우트 매개변수 or 와일드 카드가 위에 있으면 아래 라우터가 실행이 안됨.
항상 순서를 신경써주셔야 됨.router.get('/user/:id', function (req, res) { console.log('얘만 실행됩니다.'); }) router.get('/user/like', function (req, res) { console.log('전혀 실행되지 않습니다.'); })
-
/user/123?limit=5&skip=10 주소 요청인 경우
router.get('/user/:id', function (req, res) { console.log(req.params, req.query); // { id: '123' }, { limit: '5', skip: '10' } })
-
6.11.3 404 미들웨어
-
요청과 일치하는 라우터가 없는 경우를 대비해 404 라우터를 만들기
모든 라우터들 뒤에 이 코드를 위치시켜야함.
모든 라우터에 요청이 걸리지 않으면 이 404 라우터가 실행됨.app.use((req, res, next) => { res.status(404).send('Not Found'); })
-
이게 없으면 단순히
Cannot GET 주소
라는 문자열이 표시됨
6.11.4 라우터 그룹화하기
-
주소는 같지만 메소드가 다른 코드가 있을 때
아래와 같은 경우를 자주 볼 수 있습니다.
예를 들어/user
에 정보를 가져올 수도 있고(GET) 등록할 수도 있고(POST).router.get('/abc', (req, res) => { res.send('GET /abc'); }) router.post('/abc', (req, res) => { res.send('POST /abc'); })
-
router.route로 묶음
위와 같은 경우를 아래와 같이 그룹화할 수 있음.
router.route('/abc') .get((req, res) => { res.send('GET /abc'); }) .post((req, res) => { res.send('POST /abc'); })
그런데 저는 사실 router.route
그룹화 잘 안 쓰고, 다 따로 쓰는 방식을 더 선호합니다.
6.11.5 req, res 객체 살펴보기
미들웨어의 req, res, next 인자.
6.11.5.1 req
-
req.app
: req 객체를 통해 app 객체에 접근할 수 있습니다.
req.app.get('port')
와 같은 식으로 사용할 수 있습니다.app.set("port", process.env.PORT || 3000);
app.set()
을 활용하여 app에다가 변수나 속성을 저장할 수 있다고 했죠?
라우터에서는req.app
으로 접근을 할 수 있습니다.
그래서req.app.set()
또는req.app.get()
을 하면app.set()
과app.get()
이랑 똑같습니다. req.body
: body-parser 미들웨어가 만드는 요청의 본문을 해석한 객체입니다.req.cookies
: cookie-parser 미들웨어가 만드는 요청의 쿠키를 해석한 객체입니다.req.ip
: 요청의 ip 주소가 담겨있습니다.
그런데 이 ip는 정확하지 않을 수가 있습니다.
ip가 정확하지 않을 수가 있는게 proxy 서버를 사용하면 정확하지 않을 수도 있어서 더 정확한 ip를 받아내는 방법은 제가 나중에 알려드리도록 하겠습니다.
req.ip
가 완전히 정확한지 아닌지 헷갈리네요. 이는 나중에 직접 테스트하면서 보여드리도록 하겠습니다.req.params
: 라우트 매개변수(예시,/:id
)에 대한 정보가 담긴 객체입니다.req.params.id
req.query
: 쿼리스트링에 대한 정보가 담긴 객체입니다. (? 물음표 뒤에 붙어있는 애들)req.singedCookies
: 서명된 쿠키들은req.cookies
대신 여기에 담겨 있습니다.
쿠키를 서명했을 때(못알아보게 바꿔놨을 때)req.cookies
대신req.singedCookies
에 들어있다고 했죠?req.get(헤더 이름)
: 헤더의 값을 가져오고 싶을 때 사용하는 메서드입니다.
예를 들어content-type
같은거 있죠?
req.get(content-type)
하면content-type
값을 가져옵니다.
더 많지만 자주 쓰이는 것만 추렸다고 생각하시면 됩니다.
6.11.5.2 res
res.app
: req.app처럼 res 객체를 통해 app 객체에 접근할 수 있습니다.res.cookie(키, 값, 옵션)
: 쿠키를 설정하는 메소드입니다.res.clearCookie(키, 값, 옵션)
: 쿠키를 제거하는 메소드입니다.res.end()
: 데이터 없이 응답을 보냅니다.
이건express
메소드가 아니라http
메소드이긴한데, 제가http
꺼는 쓰지 말라고 하긴 했는데,
데이터 없이res
하는 것은res.end()
로 하는 것도 나쁘지 않을 것 같습니다.res.json(JSON)
: JSON 형식의 응답을 보냅니다.-
res.redirect(주소)
: 리다이렉트할 주소와 함께 응답을 보냅니다.res.writeHead(302, { Location: '/', 'Set-Cookie': `session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`, }); res.end();
위와 같이 작성되어있는 것을
express
에선res.status(302).redirect('/')
이렇게만 적어주시면 됩니다.
물론 쿠키는 따로 설정을 해주셔야됩니다. ('Set-Cookie': session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/
)
이것이res.redirect
입니다.302
로 다른 페이지로 리다이렉트 하는 거. res.render(뷰, 데이터)
: 다음 절에서 다룰 템플릿 엔진을 렌더링해서 응답할 때 사용하는 메소드입니다.res.send(데이터)
: 데이터와 함께 응답을 보냅니다.
데이터는 문자열일 수도 있고 HTML일 수도 있으며, 버퍼일 수도 있고 객체나 배열일 수도 있습니다.res.sendFile(경로)
: 경로에 위치한 파일을 응답합니다.res.setHeader(헤더,값)
: 응답의 헤더를 설정합니다.
req.get()
이 요청의 헤더를 가져오는 거였잖아요?
res.set()
은 응답의 헤더를 설정.res.status(코드)
: 응답 시의 HTTP 상태 코드를 지정합니다.
제가 말씀드렸죠?
- res.end()
- res.json()
- res.redirect()
- res.render()
- res.send()
- res.sendFile()
이것들은 각 라우터 안에서 딱 한번만 써야됩니다.
전체 요청에 대해서(=위의 각 라우터랑 같은말) 딱 한번만 써야되고, 두번 이상 쓰게되면..
즉, 하나의 요청에 응답을 2번이상 보내는걸 의미
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
응답은 두번보낼 수 없다. 응답은 한번만 보내야된다. 라는 에러 메시지가 뜬다고 했습니다.
즉, 위 코드는 한번만 쓴다.
그리고 얘네들이 return 역할을 하는 것도 아니라고 말씀드렸습니다.
6.11.6 기타
-
메소드 체이닝을 지원함
res
같은 거는 메소드 체이닝을 지원을 해서 아래와 같이 연달아서 쓸 수가 있음.
위의 예시 코드들에선 한줄한줄 따로 쓰긴 했었는데, 아래와 같이 쓸 수도 있음.res .status(201) .cookie('test', 'test') .redirect('/admin')
아래와 같이 써도 됨.
res.status() res.cookie() res.redirect()
-
응답은 한 번만 보내야 함
6.12 Pug 템플릿 엔진
템플릿 엔진은 HTML을 좀 개선한거라고 보시면 되는데 요즘은 사용성이 많이 줄어들었습니다.
요즘엔 리액트, 뷰, 앵귤러 등이 쓰이고 있어서 템플릿 엔진의 사용성이 많이 줄어들긴 했지만, 그렇다고 리액트랑 템플릿 엔진이랑 같이 안 쓴다거나 리액트가 완전히 템플릿 엔진을 대체한다던가 그런건 아니고 같이 쓰는 경우도 있습니다.
저도 실무에서 리액트랑 Pug랑 같이 쓰고 있고..
이거는 HTML의 기능을 확장해주는 거기 때문에 어느정도 알아두면 좋겠죠?
6.12.1 템플릿 엔진
-
HTML의 정적인 단점을 개선
-
반복문, 조건문, 변수 등을 사용할 수 있음
- 반목문: 리스트 나열할 때.
- 조건문: 로그인 한 화면과 안한 화면을 어떻게 처리해야되지? 고민들 경우.
- 변수: 로그인 했는지 안했는지.
위와 같은 것을 못해서 HTML 페이지를 여러개를 만든다던가 HTML 안에 중복이 많이 들어가있다던가 그런 문제들이 있거든요?
물론 이런 것들은 리액트하면서 어느정도 극복이 되지만, 서버사이드 랜더링을 할 때..
리액트는 서버사이드 랜더링을 해야만 "변수"같은게 최종적으로 극복이 되거든요?그런데 서버사이드 랜더링을 리액트에서 어떻게 하는지 모르겠다, 그럴 땐 간단하게 템플릿 엔진을 쓸 수가 있습니다.
- 동적인 페이지 작성 가능
- PHP, JSP와 유사함
-
템플릿 엔진 중에서 유명한거 두개를 알려드릴 겁니다.
Pug와 넌적스.
ejs도 유명한데 ejs는 너무 오래되었고 기능이 너무 부족해서 추천드리진 않습니다.
예전에는 express
내부에 ejs
템플릿 엔진이 내장되어있던 시절도 있었는데 express
도 ejs
를 빼버렸습니다.
- Pug
- nunjucks
- handlebars
이렇게 3개가 가장 좋습니다. 나머지는 별로..
저는 그 중에서도 Pug를 제일 좋아하긴 합니다.
6.12.2 Pug(구 Jade)
-
문법이 Ruby와 비슷해 코드 양이 많이 줄어듦
-
HTML과 많이 달라 호불호가 갈림
그래서 제 책 예제들은 전부 nunjucks로 되어있습니다. -
익스프레스에 app.set으로 퍼그 연결
// app.js // ... app.set('port', process.env.PORT || 3000); app.set('views', path.join(__dirname, 'views')); // 폴더 지정 app.set('view engine', 'pug'); // 위에서 지정한 views 폴더 안에 pug라는 확장자로 된 파일들을 선택하겠다 라는 뜻 // 이러면 템플릿 엔진에 대한 설정 끝 app.use(morgan('dev')); // ...
-
6.12.3 Pug - HTML 표현 1
<!doctype html>
<html lang="ko">
<head>
<title>익스프레스</title>
<link rel="stylesheet" href="/style.css">
</head>
</html>
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
- title= title: 이 부분은 조금 뒤에 말씀드리도록 하겠습니다.
이 부분이 변수를 쓰는 부분입니다.
<div id="login-botton"></div>
<div class="post-image"></div>
<span id="highlight"></span>
<p class="hidden full"></p>
#login-button
.post-image
span#highlight
p.hidden.full
<p>Welcome to Express</p>
<button type="submit">전송</button>
p Welcome to Express
button(type='submit') 전송
- Pug는 위와 같이 꺽쇠들이 없습니다.
- 닫는 태그도 없습니다.
- 그리고 줄바꿈, 들여쓰기.
줄바꿈, 들여쓰기는 스페이스나 탭으로 합니다. - div같이 많이 쓰는 태그는 생략 가능
Pug는 보통 타이핑 많이 하기 싫어하시는 분들이 주로 씁니다.
id나 class도 속성이므로
button(id='login-button') 로그인버튼
이런식으로 쓸 수 있긴 함.
6.12.4 Pug - HTML 표현 2
<p>
안녕하세요. 여러 줄을 입력합니다.
<br>
태그도 중간에 넣을 수 있습니다.
</p>
p
| 안녕하세요.
| 여러 줄을 입력합니다.
br
| 태그도 중간에 넣을 수 있습니다.
<style>
h1 {
font-size: 30px;
}
</style>
<script>
const message = 'Pug';
alert(message);
</script>
style.
h1 {
font-size: 30px;
}
script.
const message = 'Pug';
alert(message)
style
이나 script
뒤에 .
을 안 붙이면 태그로 인식하기 때문에 .
을 붙여주셔야합니다.
6.12.6 Pug - 변수
-
res.render
에서 두번째 인수 객체에 Pug 변수를 넣음app.set('port', process.env.PORT || 3000); app.set('views', path.join(__dirname, 'views')); // 폴더 지정 app.set('view engine', 'pug'); // 위에서 지정한 views 폴더 안에 pug라는 확장자로 된 파일들을 선택하겠다 라는 뜻 // 이러면 템플릿 엔진에 대한 설정 끝 app.use(morgan('dev')); router.get('/', function (req, res, next) { // 보통 여기다 res.sendFile('index.html') 이런식으로 해줬다면 // 이번에는 res.render('index', 변수전달) 하면 알아서 views 폴더 안에있는 index.pug.. 아까 위에서 views와 view engine 설정한거 기억나죠? // 여튼 이렇게 변수전달까지 해줍니다. res.render('index', {title: 'Express'}); })
-
res.locals
객체에 넣는 것도 가능(미들웨어간 공유됨)router.get('/', function (req, res, next) { // 위와 같이 res.render에 두번째 인자로 직접 전달하는 것이 아니라 아래와 같이 분리할 수도 있음 // 아래와 같이 설정하면, 미들웨어가 여러개 있을 때 이 미들웨어가 맨 위에 있다면, 자동으로 그 아래에 있는 미들웨어 render할 때 자동으로 파악해서 적용이됨. // 이렇게하면 변수 넣는 공간과 렌더링하는 것을 나눌 수 있음. res.locals.title = 'Express'; res.render('index'); })
-
=
이나#{}
으로 변수 렌더링 가능(= 뒤에는 자바스크립트 문법 사용 가능)<h1>Express</h1> <p>Welcome to Express</p> <button type="submit" class="Express">전송</button> <input type="text" placeholder="Express 연습">
h1= title p Welcome to #{title} button(class=title, type='submit') 전송 input(type='text', placeholder=title + ' 연습')
-
6.12.7 Pug - 파일 내 변수
-
퍼그 파일 안에서 변수 선언 가능
-
-
뒤에 자바스크립트 사용<p>Node.js와 JavaScript</p>
- const node = 'Node.js' - const js = 'JavaScript' p #{node}와 #{js}
-
변수 값을 이스케이프 하지 않을 수도 있음(자동 이스케이프)
<p><strong>이스케이프</strong></p> <p><strong>이스케이프하지 않음</strong></p>
p= '<strong>이스케이프</strong>' p!= '<strong>이스케이프하지 않음</strong>'
-
6.12.8 Pug - 반복문
-
for in
이나each in
으로 반복문 돌릴 수 있음<ul> <li>사과</li> <li>배</li> <li>오렌지</li> <li>바나나</li> <li>복숭아</li> </ul>
ul each fruit in ['사과', '배', '오렌지', '바나나', '복숭아'] li= fruit
-
값과 인덱스 가져올 수 있음
<ul> <li>1번째 사과</li> <li>2번째 배</li> <li>3번째 오렌지</li> <li>4번째 바나나</li> <li>5번째 복숭아</li> </ul>
ul each fruit, index in ['사과', '배', '오렌지', '바나나', '복숭아'] li= (index + 1) + '번째 ' + fruit
6.12.9 Pug - 조건문
-
if else
문,case when
문 사용가능<!-- isLoggedIn이 true일 때 --> <div>로그인 되었습니다.</div> <!-- isLoggedIn이 false일 때 --> <div>로그인이 필요합니다.</div>
if isLoggedIn div 로그인 되었습니다. else div 로그인이 필요합니다.
<!-- fruit이 apple일 때 --> <p>사과입니다.</p> <!-- fruit이 banana일 때 --> <p>바나나입니다.</p> <!-- fruit이 orange일 때 --> <p>오렌지입니다.</p> <!-- 기본값 --> <p>사과도 바나나도 오렌지도 아닙니다.</p>
case fruit when 'apple' p 사과입니다. when 'banana' p 바나나입니다. when 'orange' p 오렌지입니다. default p 사과도 바나나도 오렌지도 아닙니다.
이 조건문을 활용하여 로그인된 화면과 로그인 안된 화면을 모두 적어주실 수 있겠죠.
6.12.10 Pug - include
-
퍼그 파일에 다른 퍼그 파일을 넣을 수 있음
- 헤더, 푸터, 네비게이션 등의 공통 부분을 따로 관리할 수 있어 편리
-
include로 파일 경로 지정
header.pug
header a(href='/') Home a(href='/about') About
footer.pug
footer div 푸터입니다.
include header main h1 메인 파일 p 다른 파일을 include할 수 있습니다. include footer
<header> <a href="/">Home</a> <a href="/about">About</a> </header> <main> <h1>메인 파일</h1> <p>다른 파일을 include할 수 있습니다.</p> </main> <footer> <div>푸터입니다.</div> </footer>
6.12.11 Pug - extends와 block
제가 ejs
를 안 쓰는 이유가 바로 이 레이아웃, ejs
에선 이 레이아웃 기능을 못 쓰거든요?
이게 정말 편리한 기능입니다.
레이아웃은 여러 페이지간 공통된 틀을 레이아웃이라고 하죠?
대부분의 페이지들은 여러 페이지들간에 서로 공통된 부분이 있습니다.
공통되는 부분을 이렇게 레이아웃으로 만들어놓고 바뀌는 부분만 block
으로 만들어 놓는 겁니다.
바뀌는 block
부분은 아래와 같이 정의합니다.
ejs
가 이게 안돼서 비추천 드리는 겁니다.
이게 정말 필수 기능입니다.
-
레이아웃을 정할 수 있음
-
공통되는 레이아웃을 따로 관리할 수 있어 좋음, include와도 같이 사용
layout.pug
doctype html html head title= title link(rel='stylesheet', href='/style.css') block style body header 헤더입니다. block content footer 푸터입니다. block script
extends layout block content main p 내용입니다. block script script(src="/main.js")
<!doctype html> <html lang="ko"> <head> <title>Express</title> <link rel="stylesheet" href="/style.css"> </head> <body> <header>헤더입니다.</header> <main> <p>내용입니다.</p> </main> <footer>푸터입니다.</footer> <script src="/main.js"></script> </body> </html>
-
6.13 넌적스 템플릿 엔진
Pug 문법이 싫다 or 줄바꿈을 철저하게 지키기 싫다(Pug는 줄바꿈을 반드시 지켜야됨) 그러신 분들은 넌적스를 추천드립니다.
-
Pug의 문법에 적응되지 않는다면 넌적스를 사용하면 좋음
- Pug를 지우고 Nunjucks 설치
-
확장자는 html 또는 njk(view engine을 njk로)
npm i nunjucks
view engine을 퍼그 대신 넌적스로 교체합니다.
// app.js // ... const path = require('path'); const nunjucks = require('nunjucks'); dotenv.config(); const indexRouter = require('./routes'); const userRouter = require('./routes/user'); const app = express(); app.set('port', process.env.PORT || 3000); // 보통 확장자를 njk라고 할 수도 있는데 nunjucks는 html로도 많이 합니다. app.set('view engine', 'html'); // app.set('views') 이걸로 하는게 아니라 아래와 같이 // 이렇게하면 views 폴더가 넌적스 파일들의 위치가 됩니다. nunjucks.configure('views', { express: app, watch: true, }) app.use(morgan('dev')); // ...
6.13.1 넌적스 - 변수
변수를 넣는 것은 똑같습니다. res.render()
의 두번째 인자로 넣거나 res.locals
로 넣거나.
넌적스는 html 문법을 그대로 따르고있기 때문에 아래와 같이 중괄호 두번으로 감싼 부분이 바뀝니다.
html에 익숙하신 분들은 넌적스가 조금 더 낫죠?
-
중괄호중괄호 변수 중괄호중괄호
<h1>{{title}}</h1> <p>Welcome to {{title}}</p> <button type="submit" class="{{title}}">전송</button> <input type="text" placeholder="{{title}} 연습">
-
내부 변수 선언 가능
중괄호 %set 자바스크립트 구문 중괄호
내부에서 변수를 쓰는 것은 가능은 한데 그렇게 추천드리진 않습니다.{% set node = 'Node.js' %} {% set js = 'JavaScript' %} <p>{{node}}와 {{js}}</p>
<p>Node.js와 JavaScript</p>
<p>{{'<strong>이스케이프</strong>'}}</p> <p>{{'<strong>이스케이프하지 않음</strong>' |safe}}</p>
<p><strong>이스케이프</strong></p> <p><strong>이스케이프하지 않음</strong></p>
이스케이프 하냐 안하냐는 중요합니다.
예를 들어 엑세스 공격. 예를 들어 위의 strong 부분에 위험한 자바스크립트 코드가 들어있다면 그게 바로 실행되냐 마냐의 문제도 있어서 보안에 따라 이를 허용할지 안할지를 구분을 하셔야되는데, 그래서 이스케이프 부분은 잘 알아두셔야합니다.
6.13.2 넌적스 - 반복문
-
중괄호% %중괄호
안에for in
작성(인덱스는 loop 키워드)
넌적스에선loop.index
를 자체 제공<ul> {% set fruits = ['사과', '배', '오렌지', '바나나', '복숭아'] %} {% for item in fruits %} <li>{{item}}}</li> {% endfor %} </ul>
<ul> <li>사과</li> <li>배</li> <li>오렌지</li> <li>바나나</li> <li>복숭아</li> </ul>
<ul> {% set fruits = ['사과', '배', '오렌지', '바나나', '복숭아'] %} {% for item in fruits %} <li>{{loop.index}}번째 {{item}}</li> {% endfor %} </ul>
<ul> <li>1번째 사과</li> <li>2번째 배</li> <li>3번째 오렌지</li> <li>4번째 바나나</li> <li>5번째 복숭아</li> </ul>
6.13.3 넌적스 - 조건문
-
중괄호% if %중괄호
안에 조건문 작성{% if isLoggedIn %} <div>로그인 되었습니다.</div> {% else %} <div>로그인이 필요합니다.</div> {% endif %}
<!-- isLoggedIn이 true일 때 --> <div>로그인 되었습니다.</div> <!-- isLoggedIn이 false일 때 --> <div>로그인이 필요합니다.</div>
넌적스에
else if
는 없습니다.
아래와 같이elif
라고 적습니다. 파이썬에 주로 쓰이는 문법입니다.
파이썬 문법을 따왔기 때문에elif
라고 씁니다.{% if fruit === 'apple' %} <p>사과입니다.</p> {% elif fruit === 'banana' %} <p>바나나입니다.</p> {% elif fruit === 'orange' %} <p>오렌지입니다.</p> {% else %} <p>사과도 바나나도 오렌지도 아닙니다.</p> {% endif %}
<!-- fruit이 apple일 때 --> <p>사과입니다.</p> <!-- fuit이 banana일 때 --> <p>바나나입니다.</p> <!-- fruit이 orange일 때 --> <p>오렌지입니다.</p> <!-- 기본값 --> <p>사과도 바나나도 오렌지도 아닙니다.</p>
6.13.4 넌적스 - include
-
파일이 다른 파일을 불러올 수 있음
-
include에 파일 경로 넣어줄 수 있음
<!-- header.html --> <header> <a href="/">Home</a> <a href="/about">About</a> </header>
<!-- footer.html --> <footer> <div>푸터입니다.</div> </footer>
{% include "header.html" %} <main> <h1>메인 파일</h1> <p>다른 파일을 include할 수 있습니다.</p> </main> {% include "footer.html" %}
<header> <a href="/">Home</a> <a href="/about">About</a> </header> <main> <h1>메인 파일</h1> <p>다른 파일을 include할 수 있습니다.</p> </main> <footer> <div>푸터입니다.</div> </footer>
-
6.13.5 넌적스 - 레이아웃
-
레이아웃을 정할 수 있음
-
공통되는 레이아웃을 따로 관리할 수 있어 좋음, include와도 같이 사용
layout.html
<!doctype html> <html lang="ko"> <head> <title>{{title}}</title> <link rel="stylesheet" href="/style.css"> {% block style %} {% endblock %} </head> <body> <header>헤더입니다.</header> {% block content %} {% endblock%} <footer>푸터입니다.</footer> {% block script %} {% endblock %} </body> </html>
body.html
{% extends 'layout.html' %} {% block content %} <main> <p>내용입니다.</p> </main> {% endblock %} {% block script %} <script src="/main.js"></script> {% endblock %}
<!doctype html> <html lang="ko"> <head> <title>Express</title> <link rel="stylesheet" href="/style.css"> </head> <body> <header>헤더입니다.</header> <main> <p>내용입니다.</p> </main> <footer>푸터입니다.</footer> <script src="/main.js"></script> </body> </html>
-
6.13.6 에러 처리 미들웨어
에러 처리 미들웨어도 제가 설명은 드렸는데, 좀 더 활용해서 사용하는 방법이 있습니다.
-
에러 발생 시 템플릿 엔진과 상관없이 템플릿 엔진 변수를 설정하고 error 템플릿을 렌더링함
res.locals.변수명
으로도 템플릿 엔진 변수 생성 가능-
process.env.NODE_ENV
는 개발환경인지 배포환경인지 구분해주는 속성// app.js // ... // 보통 아래에서도 res.end()해주고 // 404도 사실상 에러니까 아래처럼.. error.status = 404 // 이런식으로하면 404에 대한 메시지가 완성이되고 app.use((req, res, next) => { const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`); error.status = 404; // next(error)를 하면 바로 에러처리 미들웨어로 보내지겠죠? // 404 에러도 에러처리 미들웨어에서 같이 처리하자는 겁니다. next(error); }) // 아래가 에러처리 미들웨어 // 아래에서도 res.end()해주고 그랬었는데 app.use((err, req, res, next) => { // res.locals.message와 res.locals.error는 템플릿 엔진의 변수입니다. // 거기에다가 메시지들을 넣는겁니다. res.locals.message = err.message; // 에러 메시지를 넣고 res.locals.error = process.env.NODE_ENV !== 'production' ? err : {}; // 에러 자체도 넣고 // 배표용이 아닐 땐 err를 넣어주고, 배포용이면 에러를 빈객체로 넣어줍니다. // 즉, 실제 서비스되는 페이지면 아래 스샷에서 404와 그 아레 메시지들이 안뜹니다. 빈객체이니깐. // 그렇게 하는 이유가 이 두개의 부분이 노출되면 보안에 위협이 될 수도 있다고 그랬죠? // 에러메시지도 너무 자세히 나오면 보안에 위협이 될 수 있습니다. // 그래서 배포시에는 빈객체, 개발시에는 디버깅 편하게 해야되니까 다 보여주는 식으로. res.status(err.status || 500); // 그리고 404 에러인지 500 에러인지 구분해주고 res.render('error'); // 마지막에 에러 렌더링, 넌적스면 error.확장자(html로 설정했으면 html, njx로 설정했으면 njx, pug사용했으면 pug) // 즉, error.html or error.njx or error.pug 페이지 찾아 렌더링 })
위 코드를 다 거치고나면 위와 같은 화면이 뜹니다.
-
스크린샷과 같은 화면이 뜨는 이유는 아래와 같이 설정되어있기 때문
error.html
{% extends 'layout.html' %} {% block content %} <h1>{{message}}</h1> <h2>{{error.status}}</h2> <pre>{{error.stack}}</pre> {% endblock %}
또는 퍼그이면 error.pug
extends layout block content h1= message h2= error.status pre #{error.stack}