6.1 익스프레스 프로젝트 시작하기
1. 익스프레스 소개
⇒ http 모듈로 웹 서버를 만들 때 코드가 보기 좋지 않고, 확장성도 떨어진다.
- 프레임워크로 해결하며 대표적으로 Express(익스프레스), Koa(코아), Hapi(하피) 가 있다.
- 코드관리도 용이하고 편의성도 많이 좋아진다. Express점유율이 가장 높다.
2. package.json 만들기
⇒ npm init 명령어를 통한 package 생성
- 'npm init' 명령어로 기본골격을 만든 후 'npm i express', 'npm i nodemon -D' 패키지를 설치
3. app.js 작성하기
const express = require('express');
const path = require('path');
// 앱 객체를 생성
const app = express();
app.set('port', process.env.port || 3000); // 앱 관련 속성 설정
// use 공통 미들웨어를 삽입한다.
// 미들웨어는 next()를 해줘야만 다음 미들웨어나 라우터로 이동이 가능하다.
app.use(
// 미들웨어는 하나만 들어가는 게 아니라 여러개도 들어갈 수 있다.
(req, res, next) => {
console.log('1 모든 요청에 실행된다');
next();
},
(req, res, next) => {
// throws new Error('강제로 오류를 발생시킬 수 있다');
console.log('2 모든 요청에 실행된다');
next();
},
);
app.use('/about', (req, res, next) => {
console.log('나는 /about 링크에서만 실행이 됩니다!');
next();
});
app.listen(app.get('port'), () => {
console.log('익스프레스 서버 실행');
});
const express = require('express');
const path = require('path');
const app = express();
app.set('part', process.env.port || 3000);
app.use((req,res,next) => {
try {
// dkdkdkdkdkkdkdk // 에러 발생위한 코드
} catch(error) {
// next에 error를 넣었을 경우 에러 미들웨어가 실행된다.
next(error);
}
});
app.get('/',
(req, res, next) => {
res.sendFile(path.join(__dirname, 'index.html'));
// next인수에 route를 인수를 넣으면 현재 라우터에 속한 미들웨어 중
// 현재 미들웨어 하단에 다른 미들웨어가 있어도 무시하고 다음 라우터로 간다.
const nextStat = false;
if(nextStat) {
next('route');
} else {
next();
}
},
(req, res) => {
console.log('test');
},
);
app.get('/json', (res,req) => {
// json함수 사용 시 헤더정보도 자동으로 json에 맞춰 변경된다.
// res.writeHead(200,{'Content-Type' : 'application/json'});
// res.end(JSON.stringify({hello : 'zerocho'}));
res.json({hello : 'zerocho'});
});
// 범위가 넓은 라우터의 경우에도 밑에 둬야한다.
// 위에 둘경우 하단의 라우터들이 정상적으로 호출될 수 없기 때문이다.
// app.get('*', (req,res) => {
// res.send('hello everybody');
// });
app.get('/about'), (req, res) => {
// setHeader로 헤더정보를 수정할 수 있다.
res.setHeader('Content-Type', 'text/plain');
// status함수로 상태코드를 변경할 수 있다.
res.status(200).send('hello express!! about');
});
app.get('/', (req, res) => {
// 하나의 라우터에서 sendFile, send, json 이런 것들은 한번만 호출이 가능하다.
// 두번이상 호출 시 오류 발생(요청1번에 응답1번)
// Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
res.sendFile(path.join(__dirname, 'index.html'));
// res.send('안녕하세요');
// res.json({hello: 'zerocho'});
});
// 라우터가 모두 선언되어 있는 가장 하단에 use를 사용 해
// 404미들웨어를 체크할 수 있다.
app.use((req,res, next) => {
// res.status를 이용해 header에 status를 전달할 수 있다.
// 404이지만 header정보를 200(성공)으로 전달 해 보안적인 위협을 피할 수 있다.
// ex) 401~404까지는 404로 합치거나 500~503을 모두 500으로 합치거나
// 모두 200으로 처리하거나 한다.
res.status(200).send('404입니다.');
next();
});
// 에러미들웨어는 마지막으로 넣는다.
// 에러 미들웨어는 매개변수 4개를 필수적으로 넣어줘야 한다.
app.use((err, req, res, next) => {
console.error(err);
res.status(200).send('에러가 발생했으나 사용자에게는 상세에러정보를 숨겨줄 수 있다.');
});
app.listen(app.get('port', () => {
console.log('익스프레스 서버 실행');
});
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const app = express();
app.set('port', process.env.port || 3000);
// morgan은 요청 들어가는 정보를 로그에 넣어준다.
// morgan('dev') GET / 200 13.726 ms - 165
// morgan('combined') [31/Dec/2023:07:10:37 +0000] "GET / HTTP/1.1" 304 - "-"
// "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
// Chrome/120.0.0.0 Safari/537.36"
app.use(morgan('combined');
// 인수로 비밀키를 넣어줄 수 있다.
// 서명된 쿠키가 있는 경우, 제공한 비밀키를 통해 쿠키를 검정할 수 있다.
// 쿠키는 클라이언트에서 위변조가 쉬우므로, 비밀키를 통해 만든 서명을 쿠키 값 뒤에 붙인다.
app.use(cookieParser('zerochopassword'));
// 클라이언트에서 json 데이터를 보냈을 때 그 json데이터를 파싱하여 req.body로 넣어준다.
app.use(express.json());
// 클라이언트에서 form submit할때 기본적으로 urlencoded이며 form을 파싱해준다.
// extended에서 true면 qs모듈사용, false면 querystirng을 사용하나 qs모듈을 추천
app.use(express.urlencoded({ extended : true});
app.get('/', (req, res, next) => {
req.cookies; // 쿠키정보를 얻어온다.
req.signedCookies; // 쿠키정보를 서명화하여 식별하기 어렵도록한다(암호화)
// 쿠키정보를 설정한다.
res.cookie('name', encodeURLComponent(name), {
expires : new Date(),
httpOnly : true,
path : '/',
});
// 설정된 쿠키정보를 삭제한다.
res.clearCookie('name', encodeURIComponent(name), {
httpOnly : true,
path : '/',
});
});
6.2 자주 사용하는 미들웨어
3. 자주 쓰는 미들웨어
⇒ morgan, cookie-parser, express-session 설치
- app.use로 장착한다.
- 미들웨어는 내부에서 알아서 next를 호출해서 다음 미들웨어로 넘어간다.
const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const path = require('path');
const app = express();
app.set('port', process.env.PORT || 3000);
app.use(morgan('dev'));
app.use('/', express.static(__dirname, 'public'));
app.use(express.json());
app.use(express.urlencoded({ extended : false }));
app.use(cookieParser('cookiesecret'));
app.use(session({
resave : false, // 요청이 왔을 때 세션에 수정사항이 생기지 안하도 다시 저장할지 여부
saveUninitialized : false, // 세션에 저장할 내역이 없더라도 세션을 저장할지 여부
secret : 'cookiesecret', // 쿠키 암호화
cookie : { // 세션 쿠키 옵션
httpOnly : true,
secure : false,
},
name : 'session-cookie',
}));
5. morgan
⇒ 서버로 들어온 요청과 응답을 기록해주는 미들웨어이다.
- 로그의 자세한 정도 선택 가능(dev, tiny, short, common, combined)
- 더 자세한 로그를 위해 winston 패지를 사용할 것이다.
6. static
⇒ 정적인 파일들을 제공하는 미들웨어
- 인수로 정적 파일의 경로를 제공한다.
- 파일이 있을 때 fs.readFile로 직접 읽을 필요가 없다.
- 요청하는 파일이 없을 경우 readFile로 직접 읽을 필요가 없고 파일 존재 시 다음 미들웨어는 실행되지 않는다.
⇒ 컨텐츠 요청 주소와 실제 컨텐츠의 경로를 다르게 만들 수 있어 보안에 도움이 된다.
- 요청주소 localhost:3000/stylesheets/style.css
- 실제 컨텐츠 주소 /pulbic/stylesheets/style.css
const express = require('express');
const path = require('path');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const app = express();
// app.use('요청경로'), express.static(__dirname, '실제경로'));
// 정적파일을 제공하는 의미도 있지만 서버 구조를 예측할 수 없고 public에 있는 파일만
// 외부에 제공하기 때문에 보안적인 이점이 있다.
app.use('/', express.static(__dirname, 'public');
// 미들웨어를 배치하는 순서도 중요하다.
// 일반적으로 모건은 첫번째, 두번째로는 스태틱 미들웨어를 넣는다.
// 정적 파일제공이 쿠키나 세션의 인증이 필요할 경우에는 쿠키세션 다음으로 넣는다.
app.use(morgan('dev'));
app.use('/', express.static(__dirname, 'static'));
app.use(cookieParser('zerochopassword'));
app.use(express.json());
app.use(express.urlencoded({ extended : true }));
9. body-parser
⇒ 요청의 본문을 해석해주는 미들웨어이며 폼 데이터나 AJAX 요청의 데이터를 처리한다.
- json 미들웨어는 요청이 본문이 json일 경우 해석, urlencoded 미들웨어는 폼 요청을 해석한다.
- put이나 patch, post 요청 시에 req.body에 프런트에서 온 데이터를 넣어준다.
app.use(express.json());
app.use(express.urlencoded({ extended : false }));
⇒ 버퍼 데이터나 text 데이터일 때는 body-parser를 직접 설치해야 한다.
⇒ Multipart 데이터(이미지, 동영상 등)인 경우에는 다른 미들웨어를 사용해야 한다.(multer 패키지)
10. cookie-parser
⇒ 요청 헤더의 쿠키를 해석해주는 미들웨어이다.
- parseCookies 함수와 기능이 비슷하며 req.cookies 안에 쿠키들이 들어있다.
app.use(cookieParser(비밀키));
- 비밀 키로 쿠키 뒤에 서명을 붙여 내 서버가 만든 쿠키임을 검증할 수 있다.
⇒ 실제 쿠키 옵션들을 넣을 수 있다.
- expires, domain, httpOnly, maxAge, path, secure, sameSite 등
- 지울 때는 clearCookie로(expires와 maxAge를 제외한 옵션들이 일치해야 함)
res.cookie('name', 'zerocho', {
expires : new Date(Date.now() + 900000),
httpOnly : true,
secure : true,
});
res.clearCookie('name', 'zerocho', { httpOnly: true, secure: true}));
11. express-session
⇒ 세션 관리용 미들웨어이다.
app.use(session({
resave : false, // 요청이 왔을 때 세션에 수정사항이 생기지 않아도 다시 저장할지 여부
saveUninitialized : false,// 세션에 저장할 내역이 없더라도 세션을 저장할지 여부
secret : 'cookiesecret' // 쿠키 암호화
cookie : { // 세션 쿠키 설정
httpOnly : true,
secure : false,
},
});
app.get('/', (req,res) => {
req.session.name = 'zerocho'; // 세션 등록
req.sessionID; // 세션 아이디 확인
req.session.destroy;(); // 세션 모두 제거
});
12. 미들웨어의 특성
⇒ req, res, next를 매개변수로 가지는 함수
app.use((req, res, next) => {
console.log('모든 요청에 다 실행됩니다.');
next();
});
⇒ 익스프레스 미들웨어들도 다음과 같이 축약이 가능하다.
- 미들웨어의 순서가 중요하다.
- static 미들웨어에서 파일을 찾으면 next를 호출하지 않으므로 json, urlencoded, cookieParser는 실행되지 않는다.
app.use(
morgan('dev'),
express.static('/', path.join(__dirname, 'public')),
express.json(),
express.urlencoded({ extended : false }),
cookieParser('cookiesecret'),
);
13. next
⇒ next를 호출해야 다음 코드로 넘어간다.
- next를 주석으로 처리하면 응답이 전송되지 않는다.
- 다음 미들웨어(라우터 미들웨어)로 넘어가지 않기 때문이다.
- next에 인수로 값을 넣으면 에러 핸들러로 넘어간다('route'인 경우 다음 라우터로)
14. 미들웨어 간 데이터 전달하기
⇒ 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(erq.data); // 데이터 받기
next();
});
// 또는
app.use((req, res, next) => {
req.data = '데이터 삽입';
// 나를 한정적으로 데이터를 유지하고 싶을 경우 req.session을 사용
req.session.data = '데이터 삽입';
next();
});
app.get('/', (req,res,next) => {
req.data; // '데이터 삽입'
});
15. 미들웨어 확장하기
⇒ 미들웨어 안에 미들웨어를 넣는 방법
- 아래 두 코드는 동일한 역할
app.use(morgan('dev'));
// 또는
app.use((req, res, next) => {
morgan('dev')(req, res, next);
});
- 아래처럼 다양하게 활용이 가능하다.
app.use((req, res, next) => {
if(process.env.NODE_ENV == 'production') {
morgan('combined')(req,res,next);
} else {
morgan('dev')(req,res,next);
}
});
16. 멀티파트 데이터 형식
⇒ form 태그의 enctype이 multipart/form-data인 경우 body-parsor로는 요청 본문 해석이 안된다.
- multer 패키지를 이용하면 해석이 가능하다(npm i multer)
17. multer 설정하기
⇒ multer 함수를 호출
- storage : 저장할 공간에 대한 정보이다.
- diskStorage : 하드디스크에 업로드 파일을 저장한다는 것
- destination : 저장할 경로이다.
- filename : 저장할 파일명(파일+날짜+확장자 형식)
- Limits : 파일 개수나 파일 사이즈를 제한할 수 있다.
- 실제 서버 운영 시에는 서버 디스크 대신에 S3같은 스토리지 서비스에 저장하는게 좋다.
: Storage 설정만 바꿔주면 된다.
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const app = express();
// Sync는 최초 로딩시에는 사용해도 된다.
try {
fs.readdirSync('uploads'); // 폴더를 읽어 있는지 확인
} catch(error) {
console.log('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'));
});
// upload.singe : 파일을 하나만 업로드 할 때
app.post('/upload', upload.single('image'), (req,res) => {
console.log(req.file);
res.send('ok');
});
// upload.none : upload file은 없으나 encType이 multipart/form-data일경우
app.post('/upload', upload.none(), (req,res) => {
console.log(req.body.title);
res.send('ok');
});
// upload.array('images') input file의 multipart속성 사용 시
app.post('/upload', upload.array('images'), (req,res) => {
console.log(req.files);
res.send('ok');
});
// upload.fields([]) : 업로드한 input name이 모두 다를 경우
app.post('/upload', upload.fields([{ name : 'image1'},{name :'image2'},{name:'image3'}]), (req,res) => {
console.log(req.files.image2);
res.send('ok');
});
6.3 라우터 객체로 라우터 분리하기
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 express = require('express');
const morgan = require('morgan');
const indexRouter = require('./routes');
const userRouter = require('./routes/user');
const app = express();
app.use('/', indexRouter);
app.use('/user', userRouter);
app.use(req, res, next) => {
res.status(404).send('Not Found');
});
app.use((err,req,res,next) => {
...
});
2. 라우트 매개변수
⇒ :id를 넣으면 req.params.id로 파라미터를 사용할 수 있다.
- 동적으로 변하는 부분을 라우트 매개변수로 만듦(게시판의 게시물 번호 등)
router.get('/user/:id', (req, res) =>{
console.log(req.params.id, query);
});
- 일반 라우터들보다 뒤에 위치해야 한다. (자바스크립트는 위에서 아래로 순차실행하기 때문이다)
router.get('/user/:id', (req,res) => {
console.log('나는 무조건 실행된다.');
});
router.get('/user/kim', (req,res) => {
console.log('나는 절대 실행될 수 없다.');
});
- /users/123?limit=5&skip=10 주소 요청일 경우
{id:'123'}(req.params), {limit:'5', skip:'10'}(req.query)
3. 404 미들웨어
⇒ 요청과 일치하는 라우터가 없는 경우를 대비해 404 라우터를 만든다.
- 이게 없으면 단순히 Cannot GET 주소라는 문자열이 화면에 표시된다.
app.use(req, res, next) => {
res.status(404).send('Not Found');
});
4. 라우터 그룹화하기
⇒ 주소는 같지만 라우터의 메서드가 다를 때
router.get('/user', (req,res) => {
res.send('GET /user');
});
router.post('/user', (req,res) => {
res.send('POST /user');
});
⇒ router.route로 묶는다.
router.route('/user')
.get((req,res) => {
res.send('GET /user');
})
.post((req.res) => {
res.send('POST /user');
});
6.4. req, res 객체 살펴보기
1. req 객체
⇒ req.app : req 객체를 통해 app 객체에 접근한다. (ex) req.app.get('port'))
⇒ req.body : body-parser 미들웨어가 만드는 요청을 해석한 객체이다.
⇒ req.cookies : cookie-parser 미들웨어가 만드는 요청의 쿠키를 해석한 객체이다.
⇒ req.ip : 요청의 ip주소가 담겨있다.
⇒ req.params : 라우트 매개변수에 대한 정보가 담긴 객체이다.
⇒ req.query : 쿼리스트리에 대한정보가 담긴 객체이다.
⇒ req.signedCookies : 서명된 쿠키들은 req.cookies 대신 여기에 담겨있다.
⇒ req.get(헤더 이름) : 특정 헤더의 값을 얻어오고 싶을 때 사용한다.
2. res 객체
⇒ res.app : req.app 처럼 res 객체를 통해 app 객체에 접근한다.
⇒ res.cookies(키, 값, 옵션) : 쿠키를 설정한다.
⇒ res.clearcookies(키, 값, 옵션) : 쿠키를 제거한다.
⇒ res.end() : 데이터 값이 없이 응답을 보낼 때 사용한다.
⇒ res.json(JSON) : JSON형식의 응답을 보낸다.
⇒ res.redirect(주소) : 리다이렉트할 주소와 함께 응답을 보낸다.
⇒ res.render(뷰, 데이터) : 템플릿 엔진을 렌더링해서 응답할 때 사용하는 메서드이다.
⇒ res.send(데이터) : 데이터와 함께 응답을 보낸다. 데이터는 문자열, html, 버퍼, 객체, 배열 등이다.
⇒ res.sendFile(경로) : 경로에 위치한 파일을 응답한다.
⇒ res.setHeader(헤더, 값) : 응답의 헤더를 설정한다.
⇒ res.status(코드) : 응답 시의 HTTP 상태 코드를 설정한다.
3. 기타
⇒ 메서드 체이닝을 지원한다.
res.status(200).cookie('test','test').redirect('/admin');
⇒ 하나의 요청에 대한 응답은 한번만 보내야 한다. 두번 이상 보내면 에러가 발생한다.
Error : Can't set headers after they aer sent.
6.5 템플릿 엔진 사용하기
1. 템플릿 엔진
⇒ HTML 정적인 단점을 개선하여 다이나믹한 기능들을 사용할 수 있다.
- 반복문, 조건문, 변수 등을 사용하며 동적인 페이지 작성이 가능하다.
- PHP, JSP와 유사하다.
2. PUG(구 Jade)
⇒ 문법이 Ruby와 비슷해 코드의 양이 많이 줄어든다.
- HTML과 달라 호불호가 갈릴 수 있다.
- 익스프레스에 app.set으로 퍼그를 연결한다.
app.set('views', path.join(__dirname, 'views');
app.set('view engine', 'pug');
11. 넌적스
⇒ PUG의 문법이 적응이 어렵다면 넌적스를 사용하면 좋다.
- 확장자는 html 또는 njk(view engine을 njk로 한다).
const nunjucks = require('nunjucks');
app.set('view engine', 'njk');
nunjucks.configure('views', {
express : app,
watch : true,
});
17. 에러 처리 미들웨어
⇒ 에러 발생 시 템플릿 엔진과 상관없이 템플릿 엔진 변수를 설정하고 error 템플릿을 렌더링한다.
- res.locals.변수명으로도 템플릿 엔진 변수 생성이 가능하다.
- process.env.NODE_ENV는 개발환경인지 배포환경인지 구분해주는 속성이다.
app.use((req, res, next) => {
const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
error.status = 404;
next(error);
});
app.use((err,req, res, next) => {
res.locals.message = err.message;
res.locals.error = process.env.NODE_ENV == 'production' ? err : {};
res.status(err.status || 500);
res.render('error');
});
'BackEnd > Node' 카테고리의 다른 글
[노드교과서] 섹션 7. MongoDB (0) | 2024.01.07 |
---|---|
[노드교과서] 섹션 6. 데이터베이스 (0) | 2024.01.03 |
[노드교과서] 섹션 3. http 모듈로 서버 만들기. 섹션 4. 패키지 매니저 (0) | 2023.12.19 |
[노드교과서] 섹션 2. 노드 기본 기능 익히기 (0) | 2023.12.17 |
[노드교과서] 프런트엔드 자바 스크립트 (0) | 2023.12.13 |