상세 컨텐츠

본문 제목

[노드 1팀] 6장. 익스프레스 웹 서버 만들기

24-25/Node.js 1

by gooroominuna 2024. 11. 22. 10:00

본문

728x90

 

익스프레스 (Express)  

  • 서버 제작 과정의 불편을 해소하고, 편의 기능을 제공하는 npm 웹 서버 프레임워크다.
  • http 모듈의 요청과 응답 객체 추가 기능들을 부여하고, 기존 메서드를 보완해 더욱 편리한 사용이 가능하다. 

 

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

1. 새 폴더 생성 후  package.json 생성

npm init 또는 npm init -y를 입력해 다음과 같이 package.json을 생성한다. 

{
    "name": "learn-express",
    "version": "0.0.1",
    "description": "익스프레스를 배우자",
    "main": "app.js",
    "scripts": {
      "start": "nodemon app"
    },
    "author": "ZeroCho",
    "license": "MIT"
  }


🍉 start 속성은 반드시 넣어야 하고, nodedemon app app.js nodemon으로 실행한다는 뜻이다.
nodemon 모듈은 수정 사항이 생길 때마다 서버를 자동으로 재시작한다.
🍎 express, nodemon 설치
$ npm i express
$ npm i -D nodemon

 

2. 서버 코드 작성

app.js

// express 모듈을 실행해 app 변수에 할당 
// 익스프레스 내부에 http 모듈 내장되어 있어 서버의 역할
const express = require('express'); 
const app = express();  

// app.set('port', 포트) : 서버가 실행될 포트 설정 
// process.env 객체에 PORT 속성이 있다면 사용하고, 없다면 기본값 3000번 포트 이용 
app.set('port', process.env.PORT || 3000);  

// app.get(주소, 라우터) : 주소에 대한 GET 요청이 들어올 때 어떤 동작을 할 지 
app.get('/', (req, res) => {  // req : 요청에 관한 정보 객체, res : 응담에 관한 정보 객체
  res.send('Hello, Express'); // 응답 내용, 익스프레스에서는 res.send 사용 
});

// listen을 하는 부분은 http 웹 서버와 동일
app.listen(app.get('port'), () => { // app.get('port')로 포트 가져옴
  console.log(app.get('port'), '번 포트에서 대기 중');
});
🍅 GET 요청 외에도 POST, PUT, PATCH, DELETE, OPTIONS에 대한 라우터를 위한 app.post, app.put, app.patch. app.delete, app.options 메서드가 존재한다.

 

3. 실행 

🍓 $ npm start로 실행 → http://localhost:3000 으로 접속

 

4. res.sendFile 메서드를 이용해 HTML로 응답할 수 있다. 

index.html 

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

 
app.js 

const express = require('express');
const app = express(); 

const path = require('path'); // path 모듈로 파일 경로 지정

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

app.get('/', (req, res) => { 
  res.sendFile(path.join(__dirname, '/index.html')); // res.sendfile 메서드 
});

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

 
 

6.2. 자주 사용하는 미들웨어

미들웨어

요청과 응답의 중간(middle)에 위치해 기능을 추가하거나 나쁜 요청을 걸러낸다. 
 
미들웨어는 app.use와 함께 사용한다. 

const express = require('express');
const app = express();
const path = require('path');

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

// app.use에 매개변수가 req, res, next인 함수를 넣는다. 
app.use((req, res, next) => {	// next : 다음 미들웨어로 넘어가는 함수
  console.log('모든 요청에 다 실행됩니다.');
  next();
});

// app.use, app.get 같은 라우터에 미들웨어를 여러 개 장착할 수 있다. next를 호출해야 다음 미들웨어로 넘어간다.
// 주소를 첫 번째 인수로 넣지 않으면 모든 요청에서 실행되고, 주소를 넣으면 해당 요청에서만 실행된다.
app.get('/', (req, res, next) => {
  console.log('GET / 요청에서만 실행됩니다.');
  next();
}, (req, res) => {	// 에러 발생 -> 에러 처리 미들웨어에 전달 
  throw new Error('에러는 에러 처리 미들웨어로 갑니다.')
});

// 에러 처리 미들웨어는 매개변수가 err, req, res, next로 네 개 
app.use((err, req, res, next) => {	// err : 에러에 관한 정보 
  console.error(err);
  res.status(500).send(err.message);	// res.status 메서드로 HTTP 상태 코드 지정, 기본값 200
});

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

 
실행 결과, 콘솔에 다음과 같은 메시지가 표시된다.

 

🍎기본적으로 익스프레스가 에러를 처리하지만, 실무에서는 직접 에러 처리 미들웨어를 연결해 주는 것이 좋다. 
     에러 처리 미들웨어는 특별한 경우가 아니면 가장 아래에 위치하도록 한다.
🍓 자주 사용되는 미들웨어 패키지 설치 
      $ npm i morgan cookie-parser express-session dotenv 
       → dotenv는 process.env를 관리하기 위해 설치
const express = require('express');

const path = require('path');

// 설치한 패키지 블러오기
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const dotenv = require('dotenv');

dotenv.config();
const app = express(); 
app.set('port', process.env.PORT || 3000);  

// 설치한 패키지 app.use에 연결, req, res, next는 미들웨어 내부에 있다.
app.use(morgan('dev'));
app.use('/', express.static(path.join(__dirname, 'public')));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

// process.env.COOKIE_SECRET에 cookiesecret 값이 할당. 
app.use(cookieParser(process.env.COOKIE_SECRET));
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
  name: 'session-cookie',
}));

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

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

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

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

 

🍅 .env
      COOKIE_SECRET=cookiesecret
      .env 파일을 읽어서 process.env로 만든다. 
🍉 process.env를 별도의 파일로 관리하는 이유 : 보안과 설정의 편의성
      .env 같은 별도의 파일에 비밀 키를 적어두고 dotenv 패키지로 비밀 키를 로딩하는 방식으로 관리하면,
      소스 코드가 유출되더라도 비밀 키를 지킬 수 있다.

 

1. morgan 

  • 요청과 응답에 대한 정보를 콘솔에 기록하는 미들웨어
  • 요청과 응답에 대한 정보를 한눈에 볼 수 있어 편리하다. 
  • 사용 방법 → 인수에 따라 로그가 달라진다
    app.use(morgan('dev'));      // (개발 환경) GET / 500 20.198 ms - 50  
    				// [메서드] [주소] [상태 코드] [응답 속도] - [응답 바이트]
    app.use(morgan('combined')); // (배포 환경) ::1 - - [18/Nov/2024:04:05:35 +0000] "GET / HTTP/1.1" 500 50 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"
    app.use(morgan('common'));   // ::1 - - [18/Nov/2024:04:06:32 +0000] "GET / HTTP/1.1" 500 50
    app.use(morgan('short'));    // ::1 - GET / HTTP/1.1 500 50 - 8.182 ms
    app.use(morgan('tiny'));     // GET / 500 50 - 9.405 ms

2. static 

  • 정적인 파일들을 제공하는 라우터 역할을 하는 미들웨어
  • 함수의 인수로 정적 파일들이 담겨 있는 폴더를 지정한다.
    app.use('요청 경로', express.static('실제 경로'));
    app.use('/', express.static(path.join(__dirname, 'public')));
  • 파일을 발견했다면 다음 미들웨어는 실행되지 않고, 경로에 해당하는 파일이 없으면 내부적으로 next를 호출한다.  
     
🍓public 폴더
     css, js, 이미지 파일들을 public 폴더에 넣으면 브라우저에서 접근할 수 있다. 
    실제 서버의 폴더 경로에는 public이 들어 있지만, 요청 주소에는 public이 들어 있지 않다. 
    ex) public/stylesheets/style.css → http://localhost:3000/stylesheets/style.css로 접근할 수 있다.

 

3. body-parser 

  • 요청의 본문에 있는 데이터를 해석해서 req.body 객체로 만들어주는 미들웨어
  • 보통 폼 데이터 AJAX 요청의 데이터를 처리한다. 
  • 사용 방법 
    app.use(express.json());	// JSON 형식 데이터 처리
    app.use(express.urlencoded({ extended: false }));	// URL-encoded 형식 데이터 처리
  • 요청 데이터 종류
    JSON : JSON 형식의 데이터 전달 방식
    ex) { name: 'zerocho', book: 'nodejs' } → req.body : { name: 'zerocho', book: 'nodejs' }
    URL-encoded : 주소 형식으로 데이터를 보내는 방식 
    ex) name=zerocho&book=nodejs → req.body : { name: 'zerocho', book: 'nodejs' }
 🍉 { extended: false } 옵션
       false → 노드의 querystring 모듈을 사용해 쿼리스트링을 해석
       true → qs 모듈을 사용해 쿼리스트링을 해석
       ** qs 모듈은 내장 모듈이 아니라 npm 패키지이며, querystring 모듈의 기능을 좀 더 확장한 모듈

 

🍅 body-parser를 직접 설치해야 하는 경우
     Raw는 요청의 본문이 버퍼 데이터일 때, Text는 텍스트 데이터일 때 해석하는 미들웨어다.
     버퍼나 텍스트 요청을 처리할 필요가 있다면 body-parser를 설치한 후 다음과 같이 추가한다.
     $ npm i body-parser
     const bodyParser = require('body-parser');
     app.use(bodyParser.raw());
     app.use(bodyParser.text());

 

4. cookie-parser 

  • 요청에 동봉된 쿠키를 해석 req.cookies 객체로 만들어주는 미들웨어 
  • 해석된 쿠키들은 req.cookies 객체에 들어간다.
    ex) name = zerocho  req.cookies : { name: 'zerocho' }
  • 사용 방법
    app.use(cookieParser(비밀 키));
  • 서명된 쿠키는 req.cookies 대신 req.signedCookies 객체에 저장된다. ex) name=zerocho.sign
  • 쿠키를 생성/삭제하려면 res.cookie, res.clearCookie 메서드를 사용한다.
    res.cookie('name', 'zerocho', {
    	expires: new Date(Date.now() + 900000), 
        httpOnly: true, 
        secure: true, 
    }); 
    
    res.clearCookie('name', 'zerocho', { httpOnly: true, secure: true });
    .

5. express-session 

  • 세션 관리용 미들웨어 → 로그인 등 세션을 구현하거나, 사용자를 데이터를 임시적으로 저장할 때 사용한다. 
  • 세션은 사용자별로 req.session 객체 안에 유지된다. 
  • 사용 방법 
    const session = require('express-session');	// express-session : 인수로 세션에 대한 설정을 받는다. 
    
    app.use(session({
      resave: false,		// resave : 세션에 수정 사항이 생기지 않더라도 다시 저장할지 설정하는 것
      saveUninitialized: false,	// saveUninitialized : 세션에 저장할 내역이 없더라도 처음부터 세션을 생성할지 설정하는 것
      
      // express-session은 세션 관리 시 클라이언트에 쿠키를 보낸다.
      secret: process.env.COOKIE_SECRET,	// secret : 쿠키 서명에 사용할 비밀키, cookie-parser의 secret과 같게 설정하는 것이 좋다. 
      cookie: {			// cookie : 세션 쿠키에 대한 설정, 일반적인 쿠키 옵션 모두 제공 
        httpOnly: true,		// 클라이언트에서 쿠키를 확인하지 못한다. 
        secure: false,		// https가 아닌 환경에서도 사용할 수 있다.
      },
      name: 'session-cookie',	// 세션 쿠키의 이름, 기본 이름은 connect.sid 
    }));
  • 세션 데이터 처리 
    req.session.name = 'zerocho'; // 세션 등록 
    req.sessionID; // 세션 아이디 확인 
    req.session.destroy(); // 세션 모두 제거
     
🍅  store 옵션
서버를 재시작하면 메모리가 초기화되어 세션이 모두 사라진다.
따라서 배포 시에는 store에 데이터베이스를 연결해 세션을 유지하는 것이 좋다. ex) Redis

 

🍉 express-session에서 서명한 쿠키 앞에는 s:이 붙는다.
실제로는 encodeURIComponent 함수가 실행되어 s%3A가 되는데
s%3A의 뒷부분이 실제 암호화된 쿠키 내용이다.
앞에 s%3A가 붙은 경우, 쿠키가 express-session 미들웨어에 의해 암호화된 것이다.

 

6. 미들웨어의 특성 정리 

  • 미들웨어는 req, res, next를 매개변수로 갖는 함수로,  app.use app.get, app.post 등으로 장착한다. 
    app.use((req, res, next) => {
      console.log('모든 요청에 다 실행됩니다.');
      next();
    });
     
  • 특정한 주소의 요청에만 미들웨어가 실행되게 하려면 첫 번째 인수로 주소를 넣는다.
  • 동시에 여러 개의 미들웨어를 장착할 수도 있고, 다음 미들웨어로 넘어가려면 next 함수를 호출해야 한다.
    app.use(
      morgan('dev'),
      express.static('/', path.join(__dirname, 'public')),
      express.json(),
      express.urlencoded({ extended: false }),
      cookieParser(process.env.COOKIE_SECRET),
    );
     
  • next를 호출하지 않는 미들웨어는 res.send나 res.sendFile 등의 메서드로 응답을 보내 종료해야 한다. 
  • next('route')  : 다음 라우터의 미들웨어로 바로 이동한다. 
    next(err) : 에러 처리 미들웨어로 이동하며, 인수 err은 에러 메시지를 전달한다. 
  • 미들웨어 간에 데이터를 전달하는 방법

    1) 세션을 사용하면 req.session 객체에 데이터를 넣어도 되지만, 세션이 유지되는 동안 데이터도 계속 유지된다는 단점이 있다.
    2) res.locals 객체를 이용하면  현재 요청에 한해서  데이터를 유지하고 공유한다. 요청이 끝나면 초기화된다.
    app.use((req, res, next) => {
      res.locals.data = '공유 데이터'; // 데이터 저장
      next();
    });
    
    app.use((req, res, next) => {
      console.log(res.locals.data); // 데이터 확인
      next();
    });
         
 🍎 app.set과의 차이
app.set : 익스프레스에서 전역적으로 사용되므로 하나의 요청 안에서만 유지되어야 하는 값을 넣기에는 부적절하다. app.set은 앱 전체의 설정을 공유할 때 사용한다.
res.locals : 하나의 요청 안에서만 유지되므로 res.locals 객체를 통해 요청에 종속되는 데이터를 전달하는 것이 좋다.
🍅 미들웨어를 사용할 때 유용한 패턴 : 미들웨어 안에 미들웨어를 넣는 방식 
기존 미들웨어의 기능을 확장할 수 있다. 
// ex) 조건문에 따라 다른 미들웨어를 적용하는 코드

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

 

7. multer 

multer
이미지, 동영상 등 여러 가지 파일을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어
멀티파트 형식이란 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>



multer 설치 : $ npm i multer
 
multer 기본 설정 

const multer = require('multer');

// multer 함수의 인수로 설정을 넣는다. 

const upload = multer({

  storage: multer.diskStorage({	   // storage : 어디에(destination) 어떤 이름으로(filename) 저장할지  
  	
    destination(req, file, done) { // 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);
    },					// uploads 폴더에 [파일명+현재시간.확장자] 파일명으로 업로드
  }),								
  
  limits: { fileSize: 5 * 1024 * 1024 }, // limits : 업로드에 대한 제한 사항 
});					// 파일 사이즈(fileSize, 바이트 단위)는 5MB로 제한

 

🍅 설정을 활용하려면 서버에 upload 폴더가 있어야 한다. → 직접 생성하거나 fs 모듈로 생성
       설정이 끝나면 upload 변수가 생기는데, 여기에 다양한 종류의 미들웨어가 들어 있다.

 
 
multer 사용 
 
1) 파일을 하나만 업로드하는 경우   single 미들웨어 사용 / 업로드된 파일 정보는 req.file 객체에, 기타 데이터는 req.body에 저장 

app.post('/upload', upload.single('image'), 
		(req, res) => { console.log(req.file, req.body); 
    				res.send('ok'); });


2) 여러 파일을 업로드하는 경우
 
 input 태그에 multiple을 쓰고, 미들웨어는 array로 교체 / 업로드 결과 req.files 배열
 
multipart.html

<form id="form" action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="many" multiple />
  <input type="text" name="title" />
  <button type="submit">업로드</button>
</form>
app.post('/upload', upload.array('many'), (req, res) => {
  console.log(req.files, req.body);
  res.send('ok');
});

 
3) 파일을 여러 개 업로드하지만 input 태그나 폼 데이터의 키가 다른 경우
 
 fields 미들웨어 사용, fields 미들웨어의 인수로 input 태그의 name을 적는다.
 업로드 결과는 req.files.image1, req.files.image2에 각각 들어 있다.
 
multipart.html

<form id="form" action="/upload" method="post" enctype="multipart/form-data">
  <input type="file" name="image1" />
  <input type="file" name="image2" />
  <input type="text" name="title" />
  <button type="submit">업로드</button>
</form>
app.post('/upload',
  upload.fields([{ name: 'image1' }, { name: 'image2' }]),
  (req, res) => {
    console.log(req.files, req.body);
    res.send('ok');
  },
);

 
4) 파일을 업로드하지 않고도 멀티파트 형식으로 업로드하는 경우
 
 none 미들웨어 사용 / 파일을 업로드하지 않았으므로 req.body만 존재한다. 
 
multipart.html

<form id="form" action="/upload" method="post" enctype="multipart/form-data">
  <input type="text" name="title" />
  <button type="submit">업로드</button>
</form>
app.post('/upload',  upload.none(),  (req, res) => {
  console.log(req.body);
  res.send('ok');
});`

 

6.3. Router 객체로 라우팅 분리하기 

app.js에서 app.get 같은 메서드가 라우터 부분이다. 
라우터를 많이 연결하면 코드가 복잡해지므로 익스프레스에서는 라우터를 분리할 수 있는 방법을 제공한다.
 
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;

 
 
index.js user.js app.use를 통해 app.js에 연결하고,
에러 처리 미들웨어 위에 404 상태 코드를 응답하는 미들웨어를 추가한다. 
 
app.js

...
const path = require('path');

dotenv.config();
const indexRouter = require('./routes');	// index.js는 생략 가능, require('./routes/index.js')
const userRouter = require('./routes/user');
...
  name: 'session-cookie',
}));

// app.use로 연결할 때 use와 get의 주소가 합쳐진다.
app.use('/', indexRouter);	// GET / 라우터 -> localhost:3000에서 응답
app.use('/user', userRouter);	// GET /user 라우터 -> localhost:3000/user에서 응답

app.use((req, res, next) => {
  res.status(404).send('Not Found');
});

app.use((err, req, res, next) => {
...

 
next('route')를 이용해 다음 라우터로 넘어갈 수 있다. 

// 주소의 라우터를 여러 개 만들어도 된다. 

// 첫 번째 라우터 
router.get('/', (req, res, next) => {	// 첫 번째 미들웨어
  next('route');		// next('route')를 호출 -> 다음 미들웨어 실행되지 않고 다음 라우터로 
}, (req, res, next) => {	// 두 번째 미들웨어 -> 실행 X 
  console.log('실행되지 않습니다');	
  next();
}, (req, res, next) => {	// 세 번째 미들웨어 -> 실행 X 
  console.log('실행되지 않습니다');
  next();
});

router.get('/', (req, res) => {
  console.log('실행됩니다');
  res.send('Hello, Express');
});

 
라우트 매개변수 

  •  :id에는 다른 값을 넣을 수 있고, 이는 req.params 객체에 저장된다. ex) /users/1이나 /users/123 등
    :id이면 req.params.id로, :type이면 req.params.type으로 조회할 수 있다.
    router.get('/user/:id', (req, res) => {
      console.log(req.params, req.query);
    });
  • 이 패턴을 사용할 때에는 일반 라우터보다 뒤에 위치해야 한다.
    router.get('/user/:id', (req, res) => {
      console.log('얘만 실행됩니다.');
    });
    router.get('/user/like', (req, res) => {
      console.log('전혀 실행되지 않습니다.');
    });
  •  주소에 쿼리스트링을 쓸 수 있다. 쿼리스트링의 키-값 정보는 req.query 객체 안에 들어 있다.
    ex) /users/123?limit=5&skip=10이라는 주소의 요청이 들어왔을 때 req.params와 req.query 객체
    { id: '123' } { limit: '5', skip: '10' }
  • app.js에서 에러 처리 미들웨어 위에 넣어둔 미들웨어는 일치하는 라우터가 없을 때 404 상태 코드를 응답한다.
    익스프레스가 자체적으로 404 에러를 처리해주기는 하지만, 404 응답 미들웨어와 에러 처리 미들웨어를 연결해 주는 것이 좋다. 
    app.use((req, res, next) => {
      res.status(404).send('Not Found');
    });
  • app.route나 router.route : 주소는 같지만 메서드는 다른 코드가 있을 때 이를 하나의 덩어리로 줄일 수 있다.
    router.get('/abc', (req, res) => {
      res.send('GET /abc');
    });
    router.post('/abc', (req, res) => {
      res.send('POST /abc');
    });

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

 

6.4. req, res 객체 살펴보기

익스프레스의 req, res 객체는 http 모듈의 req, res 객체를 확장한 것이다.
기존 http 모듈의 메서드에, 익스프레스가 추가한 메서드나 속성을 사용할 수 있다.

익스프레스 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(헤더 이름) : 헤더의 값을 가져오고 싶을 때 사용하는 메서드

익스프레스 res 객체의 속성과 메서드 

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

메서드 체이닝 (method chaining)  

req나 res 객체의 메서드는 메서드 체이닝(method chaining)을 지원하는 경우가 많다.
메서드 체이닝을 활용하면 코드 양을 줄일 수 있다.

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

 

6.5. 템플릿 엔진 

템플릿 엔진

  • HTML은 정적인 언어로, 자바스크립트 없이는 사용자가 기능을 직접 추가하거나 많은 데이터를 처리하기 어렵다.
  • 템플릿 엔진을 사용하면 자바스크립트로 HTML을 렌더링 할 수 있고, 반복문이나 동적 처리를 간단히 구현할 수 있다. 
  • 대표적인 템플릿 엔진으로 퍼그(Pug)와 넌적스(Nunjucks)가 있다. 
     
🍅 최근에는 템플릿 엔진 대신 리액트(React), 뷰(Vue) 같은 프런트엔드 프레임워크가 더 널리 사용되고,
프런트엔드에서 작업 후 노드 서버와 Restful 방식으로 데이터를 주고받는 것이 일반적이다. 

 

Pug(Jade) 

퍼그(Pug)는 문법이 간단해 코드 양이 줄어든다는 장점이 있어 꾸준한 인기를 얻고 있다.
 
퍼그 설치 $ npm i pug
 
익스프레스와 연결 

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

// views : 템플릿 파일들이 위치한 폴더 지정 -> res.render 메서드가 이 폴더 기준으로 템플릿 엔진을 찾아서 렌더링
app.set('views', path.join(__dirname, 'views')); 

// view engine : 어떠한 종류의 템플릿 엔진을 사용할지
app.set('view engine', 'pug'); 

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

 
퍼그 문법
 
1. HTML 표현 

  • 화살 괄호(< >)와 닫는 태그가 없다.
  • 탭 또는 스페이스로만 태그의 부모 자식 관계를 규명한다.
  • 태그의 속성은 태그명 뒤에 소괄호로 묶어 적는다.
    Pug
    doctype html
    html
      head
        title= title
        link(rel='stylesheet', href='/stylesheets/style.css')
  • 아이디와 클래스 표현  
    Pug
    #login-button
    .post-image
    span#highlight
    p.hidden.full
    
    
    HTML
    <div id="login-button"></div>
    <div class="post-image"></div>
    <span id="highlight"></span>
    <p class="hidden full"></p>
  • div 태그인 경우 div 문자는 생략할 수 있다. 
  • HTML 텍스트는 태그 또는 속성 뒤에 한 칸을 띄고 입력한다. 
    Pug
    p Welcome to Express
    button(type='submit') 전송
    
    
    HTML
    <p>Welcome to Express</p>
    <button type="submit">전송</button>
  • 에디터에서 텍스트를 여러 줄 입력하고 싶다면 파이프(|)를 넣는다. 
    Pug
    p
      | 안녕하세요.
      | 여러 줄을 입력합니다.
      br
      | 태그도 중간에 넣을 수 있습니다.
  • style이나 script 태그로 CSS 또는 자바스크립트 코드를 작성하고 싶다면 태그 뒤에 점(.)을 붙인다.
    Pug
    style.
      h1 {
        font-size: 30px;
      }
    script.
      const message = 'Pug';
      alert(message);

 
2. 변수 

  • HTML과 다르게 자바스크립트 변수를 템플릿에 렌더링 할 수 있다. 
  • res.render를 호출할 때 보내는 변수를 퍼그가 처리한다.

    routes/index.js

    router.get('/', (req, res, next) => {
      res.render('index', { title: 'Express' });
    });


  • res.locals 객체를 사용해서 변수를 넣을 수도 있다.
    router.get('/', (req, res, next) => {
      res.locals.title = 'Express';
      res.render('index');
    });
     
    템플릿 엔진이 res.locals 객체를 읽어서 변수를 집어넣는다.
    다른 미들웨어에서도 res.locals 객체에 접근할 수 있어, 다른 미들웨어에서 템플릿 엔진용 변수를 미리 넣을 수도 있다.
  • 퍼그에서 변수 사용하는 방법 
    태그, 속성 뒤에 =을 붙인 후 변수를 입력하면 텍스트로 사용할 수 있고 
    텍스트 중간에 변수를 넣으려면 #{변수}를 사용한다. 
    h1= title
    p Welcome to #{title}
    button(class=title, type='submit') 전송
    input(placeholder=title + ' 연습')
  • 내부에 직접 변수를 선언할 수도 있다. 
    - const node = 'Node.js'
    - const js = 'Javascript'
    p #{node}와 #{js}
    
    
    <p>Node.js와 Javascript</p>
  • 퍼그는 기본적으로 변수의 특수 문자를 HTML 엔티티(entity)로 이스케이프(escape)한다.
  • 이스케이프를 원하지 않는다면 = 대신 !=을 사용한다. 
    p= '<strong>이스케이프</strong>'
    p!= '<strong>이스케이프하지 않음</strong>'
    
    <p>&lt;strong&gt;이스케이프&lt;/strong&gt;</p>
    <p><strong>이스케이프하지 않음</strong></p>

 
3. 반복문

  • 반복 가능한 변수인 경우, 반복문을 사용할 수 있다.
  • 반복문 사용 시 인덱스도 가져올 수 있다.  
    ul
      each fruit in ['사과', '배', '오렌지', '바나나', '복숭아']
        li= fruit
    
    ul
      each fruit, index in ['사과', '배', '오렌지', '바나나', '복숭아']
        li= (index + 1) + '번째 '  + fruit

4. 조건문

  •  if, else if, else를 사용하여 편리하게 분기 처리를 할 수 있다.
    if isLoggedIn
      div 로그인 되었습니다.
    else
      div 로그인이 필요합니다.
  • case문도 사용 가능하다.
    case fruit
      when 'apple'
        p 사과입니다.
      when 'banana'
        p 바나나입니다.
      when 'orange'
        p 오렌지입니다.
      default
        p 사과도 바나나도 오렌지도 아닙니다.

5. include

  • "include 파일 경로"로 다른 퍼그나 HTML 파일을 넣을 수 있다.
  • header, footer, navigation처럼 웹을 제작할 때 공통되는 부분을 따로 관리할 수 있다. 
    header.pug
    header
      a(href='/') Home
      a(href='/about') About
      
    footer.pug
    footer
      div 푸터입니다
      
    main.pug
    include header
    main
      h1 메인 파일
      p 다른 파일을 include할 수 있습니다.
    include footer

6. extends와 block

  • 레이아웃을 정할 수 있고, 공통되는 레이아웃 부분을 따로 관리할 수 있다. 
  • 레이아웃이 될 파일에는 공통된 마크업을 넣고, 페이지마다 달라지는 부분을 block으로 비워둔다.
  • block [블록명]과 같은 형태로 block을 선언합니다.
  • block이 되는 파일에서는 extends 키워드로 레이아웃 파일을 지정하고 block 부분을 넣는다.
  • res.render('body')를 사용해 하나의 HTML로 합쳐 렌더링 할 수 있고 block 부분이 서로 합쳐진다.
    [layout.pug]
    doctype html
    html
      head
        title= title
        link(rel='stylesheet', href='/style.css')
        block style
      body
        header 헤더입니다.
        block content
        footer 푸터입니다.
        block script
        
        
    [body.pug]
    extends layout
    block content
      main
        p 내용입니다.
    block script
        script(src="/main.js")


넌적스 (Nunjucks) 

넌적스(Nunjucks)는 HTML 문법을 그대로 사용하되 추가로 자바스크립트 문법을 사용할 수 있으며,

파이썬의 템플릿 엔진인 Twig와 문법이 유사하다.

 

넌적스 설치 : $ 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); 
app.set('view engine', 'html');

// configure의 첫 번째 인수로 views 폴더의 경로, 두 번째 인수로 옵션
// express 속성에 app 객체를 연결, watch 옵션이 true이면 HTML 파일이 변경될 때 템플릿 엔진을 다시 렌더링
nunjucks.configure('views', { express: app, watch: true, });	
app.use(morgan('dev')); 
...

 
문법


1. 변수

  • res.render 호출 시 보내는 변수를 넌적스가 처리한다. 

    routes/index.js
    router.get('/', (req, res, next) => {
      res.render('index', { title: 'Express' });
    });
     
  • 넌적스에서 변수는 {{}}로 감싼다. 
    <h1>{{title}}</h1> 
    <p>Welcome to {{title}}</p> 
    <button class="{{title}}" type="submit">전송</button> 
    <input placeholder="{{title}} 연습">
  • 내부에서 변수를 선언할 때는 {% set 변수 = '값' %}를 사용한다. 
    {% set node = 'Node.js' %}
    {% set js = 'Javascript' %}
    <p>{{node}}와 {{js}}</p>
    
    <p>Node.js와 Javascript</p>
  • HTML을 이스케이프 하고 싶지 않다면 {{ 변수 | safe }}를 사용한다. 
    <p>{{'<strong>이스케이프</strong>'}}</p>
    <p>{{'<strong>이스케이프하지 않음</strong>' | safe }}</p>
    
    <p>&lt;strong&gt;이스케이프&lt;/strong&gt;</p>
    <p><strong>이스케이프하지 않음</strong></p>

2. 반복문

  • 넌적스에서는 특수한 문을 {% %} 안에 쓰므로 반복문도 이 안에 쓴다.
  • 반복문에서 인덱스를 사용하고 싶다면 loop.index라는 특수한 변수를 사용한다.   
    <ul>
      {% set fruits = ['사과', '배', '오렌지', ' 바나나', '복숭아'] %}
      {% for item in fruits %}
      <li>{{loop.index}}번째 {{item}}</li>
      {% endfor %}
    </ul>

3. 조건문 

  • {% if 변수 %} {% elif %} {% else %} {% endif %}로 이루어진다. 
    {% if isLoggedIn %}
    <div>로그인 되었습니다.</div>
    {% else %}
    <div>로그인이 필요합니다.</div>
    {% endif %}
  •  case 문은 없고 elif를 통해 분기 처리를 할 수 있다. 
    {% if fruit === 'apple' %}
    <p>사과입니다.</p>
    {% elif fruit === 'banana' %}
    <p>바나나입니다.</p>
    {% elif fruit === 'orange' %}
    <p>오렌지입니다.</p>
    {% else %}
    <p>사과도 바나나도 오렌지도 아닙니다.</p>
    {% endif %}
  • {{ }} 안에서는 다음과 같이 사용한다. 
    <div>{{'참' if isLoggedIn}}</div>
    <div>{{'참' if isLoggedIn else '거짓'}}</div>

4. include

  • include 파일 경로로 다른 HTML 파일을 넣을 수 있다.
  • 헤더나 푸터, 내비게이션처럼 웹 제작 시 공통되는 부분을 따로 관리할 수 있다. 
    header.html
    <header> 
      <a href="/">Home</a> 
      <a href="/about">About</a> 
    </header>
    
    footer.html
    <footer> 
      <div>푸터입니다.</div> 
    </footer>
    
    main.html
    {% include "header.html" %} 
    <main> 
      <h1>메인 파일</h1> 
      <p>다른 파일을 include할 수 있습니다.</p> 
    </main> 
    {% include "footer.html" %}

5. extends와 block 

  • 레이아웃을 정할 수 있으며, 공통되는 레이아웃 부분을 따로 관리할 수 있다.
  • 레이아웃이 될 파일에는 공통된 마크업을 넣되, 페이지마다 달라지는 부분을 block으로 비워둔다.
  • block은 {% block [블록명] %}으로 선언하고 {% endblock %}로 종료한다.
  • block이 되는 파일에서는 {% extends 경로 %} 키워드로 레이아웃 파일을 지정하고 block 부분을 넣는다.
  • res.render('body')를 이용해 하나의 HTML로 합쳐 렌더링 할 수 있고, 같은 이름의 block 부분이 서로 합쳐진다.  
    layout.html
    
    <!DOCTYPE html> 
    <html> 
      <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 %}

 

에러 처리 미들웨어

app.js

...

// 404에러가 발생하면, res.locals.message는 ‘${req.method} ${req.url} 라우터가 없습니다.’

app.use((req, res, next) => {
  const error =  new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
});

// next(error)에서 넘겨준 인수가 에러 처리 미들웨어의 err로 연결 

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

 
에러 처리 미들웨어는 error라는 템플릿 파일을 res.locals.message와 res.locals.error에 넣어준 값과 함께 렌더링 한다.
res.render에 변수를 대입하는 것 외에도, res.locals 속성에 값을 대입해 템플릿 엔진에 변수를 주입할 수 있다.
 
error 객체의 스택 트레이스(error.html의 error.stack)는 시스템 환경(process.env.NODE_ENV)이 production(배포 환경)이 아닌 경우에만 표시된다. 에러 스택 트레이스가 노출되면 보안에 취약할 수 있기 때문에 배포 환경인 경우에는 에러 메시지만 표시된다. 
 


Quiz

1. 미들웨어 (req), (res), (next)를 매개변수로 갖는 함수로,  app.use나 app.get, app.post 등으로 장착한다. 

    동시에 여러 개의 미들웨어를 장착할 수도 있고, 다음 미들웨어로 넘어가려면 (next) 함수를 호출해야 한다.


2. 요청과 응답에 대한 정보를 콘솔에 기록하는 미들웨어는 (morgan),

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


3. body-parser는 요청의 본문에 있는 데이터를 해석해서 (req.body) 객체로 만들어주는 미들웨어이고,

    cookie-parser는 요청에 동봉된 쿠키를 해석해 (req.cookies) 객체로 만들어주는 미들웨어이다. 


4. multer는 이미지, 동영상 등 여러 가지 파일을 (멀티파트) 형식으로 업로드할 때 사용하는 미들웨어이고, 

    파일을 하나만 업로드하는 경우에는 (single) 미들웨어를,
    여러 파일을 업로드하는 경우에는 (array) 미들웨어를 사용한다. 


5. (템플릿 엔진)을 사용하면 자바스크립트로 HTML을 렌더링 할 수 있고, 반복문이나 동적 처리를 간단히 구현할 수 있다. 
    대표적인 예시로 (Pug, 퍼그) (Nunjuks, 넌적스)가 있다. 


6. 넌적스(Nunjucks)에서 block {% block [블록명] %}으로 선언하고 {% endblock %}로 종료한다.

    block이 되는 파일에서는 {% extends 경로 %} 키워드로 레이아웃 파일을 지정하고 block 부분을 넣는다. 


7. error 객체의 스택 트레이스는 시스템 환경이 (production, 배포 환경)이 아닌 경우에만 표시된다. 


Programming Quiz

1.express 서버 코드 app.js를 작성하시오. 

// express 모듈을 실행해 app 변수에 할당 
________________________________________
________________________________________

// app.set('port', 포트) : 서버가 실행될 포트 설정 
// process.env 객체에 PORT 속성이 있다면 사용하고, 없다면 기본값 3000번 포트 이용 
________________________________________


// app.get(주소, 라우터) : 주소에 대한 GET 요청이 들어올 때 어떤 동작을 할 지 
// req : 요청에 관한 정보 객체, res : 응담에 관한 정보 객체
// 응답 내용은 'Hello, Express', res.send 사용 
________________________________________
________________________________________


// listen을 하는 부분은 http 웹 서버와 동일
app.listen(app.get('port'), () => { // app.get('port')로 포트 가져옴
  console.log(app.get('port'), '번 포트에서 대기 중');
});


2. HTML 코드를 퍼그 코드로 표현하시오. 

<div id="login-button"></div>
<div class="post-image"></div>
<span id="highlight"></span>
<p class="hidden full"></p>


 


Answer

1. 

// express 모듈을 실행해 app 변수에 할당 
// 익스프레스 내부에 http 모듈 내장되어 있어 서버의 역할
const express = require('express'); 
const app = express();  

// app.set('port', 포트) : 서버가 실행될 포트 설정 
// process.env 객체에 PORT 속성이 있다면 사용하고, 없다면 기본값 3000번 포트 이용 
app.set('port', process.env.PORT || 3000);  

// app.get(주소, 라우터) : 주소에 대한 GET 요청이 들어올 때 어떤 동작을 할 지 
app.get('/', (req, res) => {  // req : 요청에 관한 정보 객체, res : 응담에 관한 정보 객체
  res.send('Hello, Express'); // 응답 내용, 익스프레스에서는 res.send 사용 
});

// listen을 하는 부분은 http 웹 서버와 동일
app.listen(app.get('port'), () => { // app.get('port')로 포트 가져옴
  console.log(app.get('port'), '번 포트에서 대기 중');
});

 

2. 

#login-button
.post-image
span#highlight
p.hidden.full

 


출처 :  조현영,  Node.js 교과서 개정 3판, 길벗(2022)

Corner Node.js 1
Editor : Snoopy

728x90

관련글 더보기