8주차: Express 핵심 기능

김민지·2024년 7월 24일

미들웨어

미들웨어란?

  • request와 response 사이에 위치하며 응답을 어떻게 받을지, 요청을 어떻게 보낼지를 결정하는 함수
  • express를 미들웨어의 집합이라고 부르기도 함: 미들웨어가 모여서 express를 구성하므로
// app.js
import express from 'express'

const app = express();

app.get('/', (req, res) => {
  res.json({ message: 'Hello express!' });
}); // 이 때, 이 arow function () => {}을 미들웨어라고 부름
// also 리퀘스트 핸들러라고도 불림

app.listen(3000, () => {
  console.log('Server is listening on port 3000')
});
  • 파라미터가 4개일 때: (err, req, res, next)
    err 파라미터는 throw로 에러를 발생시키거나, next(err) 등으로 사용할 경우에만 사용됨
  • 파라미터가 3개일 때: (req, res, next)
    next 파라미터는 다음 미들웨어 함수를 가리키는 함수로, next() 등으로 사용됨

Request 보내서 테스트해보기:
1. npm run dev 명령어로 서버 실행
2. .http 파일에서 Send Request 버튼을 눌러서 요청 보내기
GET http://localhost:3000/hello

  • req, res 객체의 params, query, body 등 자주 사용되는 속성을 익히기

미들웨어의 사용법

  • next 사용: 이름 있는 함수로 미들웨어 2개 만들고 next로 연결하기
    .get의 argument로 전달한 순서대로 실행됨
import express from 'express'

const app = express();

function meeting(req, res, next) {
  console.log('Hello,');
  next();
}

function greeting(req, res, next) {
  console.log('Express!');
  res.json({ message: 'hey Codeit!' })
}

app.get('/hello', meeting, greeting); 

app.listen(3000, () => {
  console.log('Server is listening on port 3000')
});
  • .all 메소드를 사용해서 같은 미들웨어를 여러 http 메소드 (get, post, delete ... )에서 중복하여 사용하는 경우 코드를 간결히 작성하는 법
// 코드의 변경, 추가, 삭제가 복잡해짐,, 
app.get('/hello', greeting);
app.post('/hello', greeting);
app.delete('/hello', greeting);
// 개선된 코드: .all 메소드 사용
app.all('/hello', greeting);
  • .use 메소드를 활용하여 /hello 주소를 가진 모든 request 보내기
    만약 주소가 생략된 채로 작성된 경우 모든 주소에서 항상 실행하라는 의미!
// use
app.use('/hello', (req, res, next) => {
  console.log('hello, use!');
  next();
});

// get: '/hello/world' 주소
app.get('/hello/world', (req, res, next) => {
  console.log('hello world');
  res.json({ message: 'hey world' });
});

.use(), .all() 차이

  • app.use() 메소드는 해당 경로로 시작하는 모든 리퀘스트에 대해 미들웨어를 실행
  • app.all() 메소드는 모든 HTTP 메소드에 대해 해당 경로와 정확하게 일치하는 리퀘스트에 대해 미들웨어를 실행

.get()에서 실행 순서는 함수 호출 순서이나, use()all()로 불러와진 함수들이 가장 앞에 놓임

미들웨어로 req, res 다루기

function authentication(req, res, next) {
  req.user = 'Codeit';
  next();
}

app.get('/me', authentication, (req, res, next) => {
  console.log(req.user);
  res.json({ user: req.user });
});
  • 특정 상품의 id를 req로 받아와서 상품 조회. 단, 존재하지 않는 id일 경우 404 Not Found 상태 코드와 함께 경고메시지 출력
function getProduct(req, res, next) {
  req.product = products[req.params.id];
  if (req.product) {
    next();
  } else {
    res.status(404).json({ message: '존재하지 않는 상품입니다.' });
  }
}

에러 처리하기

function error(req, res, next) {
  throw new Error('에러 발생!');
  // next(new Error('에러 발생!')) 를 사용해도 동일한 결과!
}

function ok(req, res, next) {
  res.json({ message: 'OK!'})
}

app.get('/error', error, ok);

// 직접 ErrorHandler 정의해보기
app.use((error, req, res, next)=>{
  console.log(err);
  res.json({ message: "에러 핸들러!" });
});
  • GET http://localhost:3000/error로 request 보내서 확인하기
  • Express에는 기본적인 에러핸들러가 내장되어있음

내장 미들웨어

  • app.use(미들웨어)의 형태로 app.use의 argument로 미들웨어를 넘겨주는 형식을 사용함

미들웨어의 종류:
1. express.json()

import express from 'express';

const app = express();

app.use(express.json()); // express.json()

app.post('/products', (req, res) => {
  console.log(req.body);
  res.json({ message: 'Product 추가하기' });
});
POST http://localhost:3000/products
Content-Type: application/json

{
	"name": "상품1",
    "price": 100
}
  1. express.urlencoded()
// express.urlencoded()
app.use(express.urlencoded({ extended: true }));

app.post('/users', (req, res) => {
  console.log(req.body);
  res.json({ message: 'User 추가하기' });
})

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});
POST http://localhost:3000/users
Content-Type: application/x-www-form-urlencoded

name=codeit&age=22
  1. express.static()
app.use(express.static('public'));

public 디렉토리에 속한 파일 명으로 리퀘스트를 보내서 해당 파일들에 접근할 수 있게 됨

GET http://localhost:3000/codeit.png

###

GET http://localhost:3000/index.html

codeit.png, index.html 파일은 public 폴더에 저장된 파일들임

third-party 미들웨어

https://www.codeit.kr/topics/express-core-features/lessons/8924

라우터

경로 중복 제거하기

  • .route(중복된 경로) 메소드를 사용하면 중복되는 경로들을 묶어서 표현할 수 있음!
import express from 'express';

const app = express();

app.route('/products')
  .get((req, res) => {
    res.json({ message: 'Product 목록 보기' });
  })
  .post((req, res) => {
    res.json({ message: 'Product 추가하기' });
  });

app.route('/products/:id')
  .patch((req, res) => {
    res.json({ message: 'Product 수정하기' });
  })
  .delete((req, res) => {
    res.json({ message: 'Product 삭제하기' });
  });

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

라우터 만들기

import express from 'express';

const app = express();

const productRouter = express.Router();

productRouter.route('/')
  .get((req, res) => {
    res.json({ message: 'Product 목록 보기' });
  })
  .post((req, res) => {
    res.json({ message: 'Product 추가하기' });
  });

productRouter.route('/:id')
  .patch((req, res) => {
    res.json({ message: 'Product 수정하기' });
  })
  .delete((req, res) => {
    res.json({ message: 'Product 삭제하기' });
  });

app.use('/products', productRouter);

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

라우터 레벨 미들웨어

import express from 'express';

const app = express();

const productRouter = express.Router();

productRouter.use((req, res, next) => {
  console.log('productRouter에서 항상 실행!');
  // product로 시작하는 모든 경로에서 항상 실행됨!
  next();
});

productRouter.route('/')
  .get((req, res) => {
    res.json({ message: 'Product 목록 보기' });
  })
  .post((req, res) => {
    res.json({ message: 'Product 추가하기' });
  });

productRouter.route('/:id')
  .patch((req, res) => {
    res.json({ message: 'Product 수정하기' });
  })
  .delete((req, res) => {
    res.json({ message: 'Product 삭제하기' });
  });

app.use('/products', productRouter);

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

실습

  • 경로가 /products로 시작하는 라우트는 누구나 조회, 생성, 수정, 삭제가 가능하지만, 경로가 /users로 시작하는 라우트는 관리자만 조회, 생성, 수정, 삭제가 가능한 API 서버를 만드려고 합니다.

  • Authorization 헤더에 s3cret이라는 문자열 값으로 포함하여 리퀘스트를 보낸 경우 관리자로 인증되고, 키가 없거나 잘못된 값이 포함되어 있는 경우 401 Unauthorized 상태 코드와 함께 메시지 객체를 리스폰스로 반환해 주세요. 메시지 객체는 아래의 리스폰스 바디를 확인해 주세요.

import express from 'express';

const app = express();

// User Router
const userRouter = express.Router();

userRouter.route('/')
  .get((req, res, next) => {
    res.json({ message: '사용자 목록 보기' });
  })
  .post((req, res, next) => {
    res.json({ message: '사용자 추가하기' });
  });

userRouter.route('/:id')
  .patch((req, res, next) => {
    res.json({ message: '사용자 수정하기' });
  })
  .delete((req, res, next) => {
    res.json({ message: '사용자 삭제하기' });
  });

function adminOnly(req, res, next) {
  const authorization = req.get('Authorization');
  if (authorization === 's3cret') {
    next();
  } else {
    res.status(401).json({ message: '권한이 없습니다.' });
  }
}

app.use('/users', adminOnly, userRouter);

// Product Router
const productRouter = express.Router();

productRouter.route('/')
  .get((req, res, next) => {
    res.json({ message: '상품 목록 보기' });
  })
  .post((req, res, next) => {
    res.json({ message: '상품 추가하기' });
  });

productRouter.route('/:id')
  .patch((req, res, next) => {
    res.json({ message: '상품 수정하기' });
  })
  .delete((req, res, next) => {
    res.json({ message: '상품 삭제하기' });
  });

app.use('/products', productRouter);

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

Express 프로젝트 구조와 모듈화

  • router 분리
// src > routes > product.js
import express from 'express';

const productRouter = express.Router();

productRouter.use((req, res, next) => {
  console.log('Product Router에서 항상 실행!');
  next();
})

productRouter.route('/')
  .get((req, res) => {
    res.json({ message: 'Product 목록 보기' });
  })
  .post((req, res) => {
    res.json({ message: 'Product 추가하기' });
  });

productRouter.route('/:id')
  .patch((req, res) => {
    res.json({ message: 'Product 수정하기' });
  })
  .delete((req, res) => {
    res.json({ message: 'Product 삭제하기' });
  });

export default productRouter;
// src > routes > user.js
import express from 'express';

const userRouter = express.Router();

userRouter.get('/', (req, res, next) => {
  res.json({ message: 'User 목록 보기' });
});

export default userRouter;
  • 미들웨어 분리
// src > middlewares > always.js
export default function (req, res, next) => {
  console.log('나는 항상 실행!');
  next();
}
  • 기존 app.js 파일
// app.js
import express from 'express';
import productRouter from './routes/product.js';
import userRouter from './routes/user.js';
import always from '.middlewares/always.js';

const app = express();

app.use(always);

app.use('/products', productRouter);
app.use('/users', userRouter);

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

파일업로드

multer middleware 사용하기

// app.js
import express from 'express';
import productRouter from './routes/product.js'
import userRouter from './routes/user.js'
import always from './middlewares/always.js';
import multer from 'multer';

const app = express();

app.use(always);

app.use('/products', productRouter);
app.use('/users', userRouter);

const upload = multer({ dest: 'uploads/' });

app.post('/files', upload.single('attatchment'), (req, res) => {
  res.json({ message: '파일 업로드 완료!' });
});

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});
  • http
POST http://localhost:3000/files
Content-Type: multipart/form-data; boundary=Boundary01234567890123456789

--Boundary01234567890123456789
Content-Disposition: form-data; name="attachment"; filename="hello.txt"
Content-Type: text/plain

Hello!
--Boundary01234567890123456789--

서버의 파일 제공하기

  • 파일에 접근하도록 하는 미들웨어
// users.js
import express from 'express';

const userRouter = express.Router();

userRouter.get('/', (req, res, next) => {
  res.json({ message: 'User 목록 보기' });
});

export default userRouter;
// app.js
import express from 'express';
import productRouter from './routes/product.js'
import userRouter from './routes/user.js'
import always from './middlewares/always.js';
import multer from 'multer';

const app = express();

app.use(always);

app.use('/products', productRouter);
app.use('/users', userRouter);

const upload = multer({ dest: './uploads/' });

app.post('/files', upload.single('attachment'), (req, res) => {
  console.log(req.file);
  res.json({ message: '파일 업로드 완료!'});
});

app.listen(3000, () => {
  console.log('Server is listening on port 3000');
});

0개의 댓글