본문 바로가기

BackEnd/Node

[노드교과서] 섹션5. 익스프레스 웹 서버 만들기

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');
});