express와 미들웨어

YOONIVERSE·2023년 10월 17일
0

미들웨어란?

: 미들웨어는 중간 작업을 해주는 역할을 한다. 즉, 요청과 응답 사이에 express 자체에있는 기능 외에 추가적인 기능을 넣어줄 수 있다. express의 사용법에서 핵심은 '미들웨어'에 있다고 할 정도로 미들웨어는 express에서 중요한 역할을 한다. 미들웨어는 인증 수행, 예외처리, 세선처리, 라우터 등 많은 종류가 있다.

app.use()

미들웨어는 app.use() 메서드를 통해 사용한다. app.set() 과의 차이점은 app.set() 은 전역으로 사용된다는 점이다.


src/express/express_study3.js

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

app.get('/', function (req, res) {
  res.send('Hello Wolrd!');
});

const myLogger = function (req, res) {
  console.log('LOGGED');
};

app.use(myLogger);

app.listen(8080);

line 8: 위 코드는 서버가 요청을 받을 때마다 'LOGGED' 라는 메시지를 콘솔에 출력하는 것이다. myLogger라는 미들웨어를 만들어 주었고
line 12: app.use() 를 사용해 미들웨어를 붙여주었다.
이렇게 되면 요청이 들어올 때마다 myLogger 를 반드시 거치게 된다.
여기서 요청은 '/' 주소를 get했을 때, 즉, localhost:8080/에 접속했을 때를 말한다.


src/express/express_study4.js

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

app.get('/' , function (req, res, next) {
  res.send('Hello World!');
  next();
});

const myLogger = function (req, res, next) {
  console.log('LOGGED');
  next();
};

app.use(myLogger);

app.listen(8080);

line4, line9: 함수 파라미터에 next 를 추가해주고
line6, line11: next 함수를 호출해주었다.

localhost:8080/ 에 접속하는 요청이 오면 콘솔에 'LOGGED'를 출력하는 myLogger가 실행되는 것을 확인할 수 있다. 미들웨어는 위에서 아래로 실행되기 때문에 순서가 중요하다.
먼저 app.get('/')이 수행되고 res.send()가 끝나고 응답을 종료해버리기 때문에 myLogger 까지 도달하지 않는다. next() 는 다음 미들웨어로 넘어가는 역할을 하기 때문에 순서를 잘 배치를 해주고 next() 를 통해 흐름을 잘 제어해주어야 한다.

next() 는 다음 미들웨어로 가는 역할을 하지만 몇가지 인자를 넣어 다른 기능을 하게 할 수도 있다.

종류내용
next()다음 미들웨어로 가는 역할을 한다
next(error)오류 처리 미들웨어로 가는 역할을 한다
next('route')많이 사용하지는 않지만 next()로 같은 라우터에서 분기처리를 할 때 사용한다


오류 처리를 위한 미들웨어 함수는 총 네 개의 파라미터 err, req, res, next 를 가진다. 오류는 오류 처리 미들웨어에서 따로 다루어야 한다.


오류 처리 미들웨어

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

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

app.use(~...)			// 공통 미들웨어 morgan, cookie-parser, express.json
        				// express.urlencoded, session.. 등
        
app.get(~...)			// 라우터

app.get((req, res, next) => res.status(404)...)			// 404 처리 미들웨어
// 단 res.status 상태 코드의 400 500번 대를 너무 자세히 보여주면 해킹의 위험이 있음

app.use((err, req, res, next) => ...)			// 오류 처리 미들웨어
        
app.listen(app.get('port'));

보통 express 로 서버를 만들 때 다음과 같은 순서로 구조를 짠다.

  1. express 를 불러온다.
  2. 포트를 설정해준다.
  3. 공통적으로 사용하는 미들웨어를 장착해준다.
  4. 라우터를 구성한다.
  5. 404 처리 미들웨어를 구성한다.
  6. 오류 처리 미들웨어를 구성한다.
  7. 생성된 서버가 포트를 리스닝한다.

자주 사용하는 미들웨어

express.static

css, js 등 static 파일은 static 끼리 모아 따로 폴더를 지정해 놓는 것이 좋다.

static 은 express 안에 기본적으로 포함되어 있기 때문에 별도의 설치 없이 꺼내서 사용만 해주면 된다. static() 안에 static 폴더로 지정해 줄 파일의 경로를 입력한다. static 폴더를 따로 지정해주면 지정한 파일이 바로 클라이언트로 가는 것이 아닌 static 미들웨어를 거친 후 도착하게 된다. 이렇게 지정했으면 이제부터 static 파일들을 불러 html에 띄울 수 있다.

router

router 도 일종의 미들웨어이며, 클라이언트로부터 요청(request) 이 왔을 때 서버에서 어떤 응답(reponse)을 보내주어야 할 지 결정해 준다. router는 URL 스키마 같은 존재로 "localhost:8080/ 요청이 들어왔으면 index.html을 보내!" "localhost:8080/join.html이라는 요청이 들어왔으면 join.html을 보내!" 라고 알려주는 역할을 한다.
express 에서 router 는 'app.get', 'app.post', 'app.use' 등 처럼 express 모듈을 담은 변수 뒤에 http 메서드가 붙은 것인데, 이를 이용해 요청이 들어오는 경로와 응답을 만들 수 있다.

종류주소응답
GET/Index.html 파일을 송신한다.
GET/joinJoin.html 파일을 송신한다.
POST/user사용자를 등록한다.
PUT/user/user_iduser_id를 가진 사용자의 정보를 수정한다.
DELETE/user/user_iduser_id를 가진 사용자의 정보를 삭제한다.

app.use('/경로', 미들웨어);
app.get('/경로', 미들웨어);
app.post('/경로', 미들웨어);
app.put('/경로', 미들웨어);
app.delete('/경로', 미들웨어);

첫 번째 파라미터로 넣어준 경로로 요청이 들어오면 두 번째 파라미터의 미들웨어를 실행한다. 첫 번째 파라미터의 경로는 서버 자원을 가리키는 URI 문자열인데, 회원가입 경로이면 /join.html 처럼 만들어준다.
두 번째 파라미터는 라우팅 로직 함수를 콜백 형태로 구현한 것이고 해당 주소의 요청을 받으면 미들웨어가 어떤 작업을 수행하게 된다.


app. get("/user/:id", function (req, res) {
  res.send("user id: " + req.params.id);
});

하나의 클라이언트에서 GET 메서드로 /user/1 이라는 URI 요청이 들어왔다고 생각해보면 이 요청에 대한 응답으로 req.params.id 에 접근하여 얻은 고객의 아이디를 문자열로 보내게 된다.


src/express/express_study7.js

const express = require('express');

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

app.use(express.static(__dirname + '/public'));

app.get('/', (req, res) => {
  const output =  `
      <h2>express로 웹 만들기</h2><br>
      <p>메인 페이지 입니다.</p><br>
      <img src ="./sample.HEIC" width="400px" height="200px" />
      `
  res.send(output);
});

app.get('/user/:id', (req, res) => {
  res.send(req.params.id + "님의 개인 페이지 입니다.");
});

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

📍 응답을 위한 함수
app.get, app.use 는 요청을 위한 함수이고 응답을 위한 함수도 몇 가지가 있는데, 지금까지 send()와 sendFile()을 이용해서 직접 html 요소를 넣기도 하고 html 파일을 통째로 보내주기도 했다. 그 외에도 render().json() 등도 있다.

  • res.send() : 문자열로 응답한다.
  • res.json() : json 객체로 응답한다.
  • res.render() : Jade, Pug 와 같은 템플릿을 렌더링하여 응답한다.
  • res.sendFile(): 파일로 응답한다.

morgan

: morgan 은 Logger API 이다.
morgan 은 request 와 response 를 깔끔하게 포매팅해주어 콘솔에 로그를 찍는 역할을 한다. 호출된 router 가 어떤 상태이고, 어떤 결과 값인지를 보여준다. 그리고 이렇게 찍힌 로그를 콘솔로만 확인해도 되지만 json 형태로 dump 파일에 기록하게 해주는 winston 이라는 모듈도 있다.

const logger = require('morgan');

...

app.use(logger('combine'));
// [:remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"]
app.use(logger('common'));
// [:remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length]]
app.use(logger('dev'));
// [:method :url :status :response-time ms - :res[content-length]]
app.use(logger('short'));
// [:remote-adr :remote-user :method :url HTTP/:http-version :status :res[content-length] - :response-time ms]
app.use(logger('tiny'));
// [:method :url :status :res[content-length] :response-time ms]

morgan 에는 인자로 여러 옵션을 넣어줄 수 있고 옵션마다 보여주는 정도도 다르다. 주로 개발 시에는 'dev' 옵션을 사용하는데, 요청 메서드, url, 상태, 응답시간 등을 보여주고 배포 시에는 'combine' 옵션을 사용해 사용자의 주소, 브라우저 등 더 세부적인 정보를 로깅할 수 있다.

express.json, express.urlencoded

: 클라이언트에서 post, put 요청 시 들어온 정보를 가진 req.body에 접근하기 위해 필요한 미들웨어 이다. 요청 정보가 url에 들어온 것이 아니라 request body에 들어있는데, 이 값을 읽을 수 있는 구문으로 파싱하고 req.body로 옮겨주는 역할을 하는 것이 express.json, express.urlencoded 이다.

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

express.json 은 req.body가 json 형태일 때, express.urlencoded 는 폼에 대한 요청일 때 사용한다.
express.urlencoded 의 extended 옵션 옵션은 false 로 설정하면 node.js 에 내장된 queryString 을 사용하고 true로 설정하면 npm의 qs 모듈을 사용한다. 둘다 url 쿼리 스트링을 파싱해주지만 qs는 보안이 추가적으로 필요할 때 사용한다. 그리고 req.body 가 이미지, 동영상 등의 multipart 형식의 데이터라면 multer 등 다른 미들웨어를 추가적으로 설치해서 처리해주어야 한다.

: 쿠키는 출입증 같은 역할을 한다.

res.writeHead({'Set-Cookie': 'name=gildong'});

처음에 한번만 서버에서 res.writeHead() 메서드를 통해 'Set-Cookie'에 값을 넣어준다. 그러면 브라우저에 키-쌍으로 이루어진 쿠키가 헤더에 저장된다. 이렇게 한번 설정되고 난 후에는 브라우저에서 자동으로 쿠키를 매번 요청할 때마다 서버에 보낸다.

src/express/cookie.js

const http = require('http');

http.createServer((req, res) => {
  res.writeHead(200, { 'Set-Cookie' : 'name=roadbook' });
  console.log(req.headers.cookie);
  res.end('Cookie --> Header');
})
    .listen(8080, () => {
      console.log('8080포트에서 서버 연결 중 ..');
    });

localhost:8080 으로 접속하고, 개발자 도구 [Network] 탭을 열고 [새로고침] 을 한 뒤, [localhost]를 누르고 [Headers] 탭을 클릭하면 클라이언트에 Response Header에 해당 쿠키가 잘 전달 된 것을 확인 할 수 있다.

브라우저를 끄기 전까지만 쿠키가 살아있는 이유는 쿠키의 expire를 지정해주지 않았기 때문에 기본 값으로 브라우저를 끄게 되면 쿠키 값이 없어진다. 이렇게 쿠키 값을 통해 쿠키가 있을 때는 사용자 관련 화면을, 없을 때는 로그임 화면을 보여주는 식으로 로그인을 구현 할 수 있게 된다.

클라이언트가 쿠키와 함께 요청을 보내면 req.headers.cookie를 통해 쿠키 값에 접근을 할 수 있게 된다. req.headers.cookie 에 저장된 값은 문자열인데, 이를 자바스크립트에서 사용하기 위해서는 객체로 파싱하는 과정이 필요하다. Cookie-parser 없이 파싱하려면 따로 조금 복잡한 함수를 만들어 주어야 한다.

❓파싱 : 데이터를 원하는 형태로 가공하는 과정이다.

Cookie-parser 를 사용하면 따로 파싱하는 함수를 만들어줄 필요 없이 express의 req 객체에 cookies 속성이 부여되므로 res.cookies.쿠키명을 통해 쿠키 값에 접근을 할 수 있게 된다.

express-session

: 쿠키만 사용하게 되면 개발자도구 [Applicatio] 탭의 Cookies 에서 쿠키 값을 확인 할 수도 있고 값을 바꿀 수도 있게 된다.
따라서 실제 정보는 서버에만 저장해두고 브라우저에는 암호화된 키 값만 보내고 그 키 값으로 실제 값에 접근할 수 있도록 하는 것을 세션이라고 한다.

src/express/express-session.js

const http = require('http');

const session = {};
const sessKey = new Date();
session[sessKey] = { name: 'roadbook' };

http.createServer((req, res) => {
  res.writeHead(200, { 'Set-cookie' : `session=${sessKey}` });
  res.end('Session-Cookie --> Header');
})

    .listen(8080, () => {
      console.log('8080포트에서 서버 연결 중..');
    });

line 5: 세션 저장용 객체 하나를 session 이라는 변수에 생성하고, 키 값을 sessKey 라는 변수에 생성한다.
express-session 미들웨어를 이용하면 임의의 키 값을 생성할 수 있게 된다. session 객체에 sesskey 라는 키 값을 지정해주고 값은 { name = 'roadbook' } 으로 할당한다. 그리고 쿠키 값을 바로 보내주는 것이 아니라 세션 값으로 생성한 키 값을 넣어준다.

쿠키 값이 바로 뜨는 것이 아니라 세션 값이 보여지게 된다. 이 세션도 마찬가지로 express-session 이 없다면 cookies.session 값을 이용해서 세션을 다루어야 하지만 express-session 을 이용한다면 req 객체에 session 속성이 부여되어 req.session 을 통해 접근할 수 있게 된다.

미들웨어 통합 테스트

npm install morgan cookie-parser express-session

한번에 여러 모듈을 설치할 수 있다.

const express = require('express');
const morgan = require('morgan');
const cookieParser = require('cookie-parser');
const session = require('express-session');
const app = express();

/* 포트 설정 */
app.set('port', process.env.PORT || 8080);

/* 공통 미들웨어 */
app.use(express.static(__dirname + '/public'));
app.use(morgan('dev'));
app.use(cookieParser('secret@1234'));     // 암호화된 쿠키를 사용하기 위한 임의의 문자 전송
app.use(session({
  secret: 'secret@1234',      // 암호화
  resave: false,      // 새로운 요청 시 세션에 변동 사항이 없어도 다시 저장할지 설정
  saveUninitialized: true,      // 세션에 저장할 내용이 없어도 저장할지 설정
  cookie: {     // 세션 쿠키 옵션들 설정 httpOnly, expires, domain, path, secure, sameSite
      httpOnly: true,     //  로그인 구현 시 필수 적용, 자바스크립트로 접근 할 수 없게 하는 기능
  },
  // name: 'connect.sid'      // 세션 쿠키의 Name 지정 default가 connect.sid
}));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

/* 라우팅 설정 */
app.get('/', (req, res) => {
  if (req.session.name) {
    const output = `
        <h2>로그인한 사용자님</h2><br>
        <p>${req.session.name}님 안녕하세요.</p><br>
      `
    res.send(output);
  } else {
      const output = `
          <h2>로그인하지 않은 사용자입니다.</h2><br>
          <p>로그인 해주세요.</p><br>
        `
      res.send(output);
  }
});

app.get('/login', (req, res) => {     // 실제 구현 시 post
  console.log(req.session);
  // 쿠키를 사용할 경우 쿠키에 값 설정
  // res.cookie(name, value, options)
  // 세션 큐키를 사용할 경우
  req.session.name = 'yooniverse';
  res.end('Login OK')
});

app.get('/logout', (req, res) => {
  res.clearCookie('connect.sid');     // 세션 쿠키 삭제
  res.end('Logout OK');
});

/* 서버와 포트 연결.. */
app.listen(app.get('port'), () => {
  console.log(app.get('port'), '번 포트에서 서버 실행 중..')
});

콘솔에 위와 같은 메세지가 뜨는 이유는 morgan 을 장착해주었기 때문에
line 2: 요청 메서드, url, 상태코드, 응답 속도 등의 로그가 표시되는 것이다.

line 3~4: cookie-parser와 express-session도 장착했다.
line 48: /login 페이지로 접속 시 쿠키 세션을 이용해 req.session.name 이라는 값을 넣어주었고, 브라우저를 닫을 때까지 req.session.name 값은 유효하다.

line 13,15: 안전하게 쿠키를 전송하기 위해서 쿠키를 서명해야 하기 때문에 secret 값이 필요하다. cookie-parser 의 인자 값과 쿠키 세션의 값을 동일하게 설정해준다.

express-session 을 이용하지 않고 세션을 구현하면 따로 const session = {}; 과 같이 세션 정보를 저장할 공간을 따로 마련해주어야 한다. 하지만 express-session 을 사용해서 req.session 을 가지게 되면, req.session 은 방금 요청을 보낸 사람의 고유한 저장 공간 같은 곳으로 사용할 수 있게 된다. req.session.name = '로드북' 이란 값을 넣어주었다면 요청을 보낸 사람의 req.session.name만 '로드북' 이라는 값을 가지게 되는 것이다.

/ 페이지로 접속했을 때 req.session.name 값이 있으면 즉, /login 페이지로 먼저 접속해 req.session.name 값이 생성되어 있을 경우에는 "로그인한 사용자님" 이라는 제목을 출력하고, req.session.name 값이 없다면 "로그인하지 않은 사용자입니다." 라는 제목을 출력해준다.

🔑 미들웨어 첫 부분에서 미들웨어는 next()를 넣어주어야 다음 미들웨어로 간다고 했는데 자주 사용하는 미들웨어(morgan, cookie-parser등)에서는 next()가 보이지 않는다. 이들은 모두 next()가 내부에 내장되어 있기 때문에 자동으로 다음 미들웨어로 넘어가게 된다.
단, static의 경우 next()가 없기 때문에 static을 거쳐야하는 router라면 공통 미들웨어의 순서를 잘 설정해주어야 한다.

profile
스텝이 꼬이면 그것이 바로 탱고 💃

0개의 댓글