참고: Building RESTful APIs Using Node.js and Express | Coursera
시작하기 | Axios Docs (axios-http.com)
Installing Express (expressjs.com)
Express routing (expressjs.com)
- 이 포스트에서는 coursera의 Building RESTful APIs Using Node.js and Express | Coursera 를 수강하며 배운 주요 개념과 과제를 진행하며 발생한 이슈 해결 방법을 공유합니다.
- 이는 저의 개인적인 이해를 바탕으로 작성되었으며 모든 정보가 최신이거나 완전한 것은 아니므로 항상 개선의 여지가 있습니다. 따라서 읽는 동안 어떤 피드백이나 추가적인 아이디어가 있으시다면 언제든지 댓글로 알려주시기 바랍니다.
2. Express framework로 node.js 구조화하기
기존의 Vanila JS로 작성한 코드는 다음과 같은 불편한 점이 있다.
- URL과 query, parameters를 얻기 위해 정규식, 문자열 파싱을 이용하는 점
- http.createServer에 무수히 많은 if-else 구문으로 http 메소드 콜을 정의하는 것
- response.end() 중복 사용이 방지되어 있지 않다는 점
express가 어떻게 효과적으로 이러한 점들을 해결해주고 있는지 알아보자!
2.1. express framework 특징
- 쉽고, 빠르고, 유연하고, 미니멀
- 미들웨어 기반
- 클라이언트에서 어떤 요청이 들어오면 서버가 처리하기 전, 중간에 어떤 로직을 수행하도록 하는 소프트웨어
- 응답, 요청 객체에 무언갈 함
- 인증, 권한 검사, 로깅, 쿠키 파싱, 에러 핸들링 등을 함.
- ★ 웹 앱의 핵심 로직에 집중하기 위해 필요한 공통적인 기능을 모아서 처리하는 것.
- express는 싱글 스레드, 비동기적이면서, node.js의 미들웨어 모듈인 connect에 기반 함
- routing을 지원한다.
2.2. basic express server config
/* basic express server config */
const express = require('express');
const app = express(); // express instance
const port = 9000;
// bind and listen to the connection on the host and port
app.listen(PORT, (err) => {
if (err) {
console.log('Error in server setup');
}
console.log('Server listening on port', PORT);
});
app.listen([port[, host[, backlog]]][, callback])
- host와 backlog를 명시하기 전에 port가 지정되어야 함
- backlog : 연결을 대기 큐의 최대 길이. port, host가 지정되어야 함.
- callback : 일단 app이 시작되면 실행 될 함수. port, host, backlog 지정 없어도 OK
2.3. Routing
- 특정 엔드포인트에 대한 클라이언트 요청에 어떻게 앱이 응답할 것인지 결정하는 것
app.METHOD(PATH, HANDLER);
- path: 서버 경로
- 정규식
.
,/
- 문자열, 문자열 패턴
- handler: 콜백 함수
route example
app.get('/', (req, res) => {
res.send('hello world')
})
// ab로 시작하고 cd로 끝나는 uri
app.get('/ab*cd', (req, res) => {
res.send('ab*cd')
})
res.send(데이터)
: 데이터를 브라우저로 보낸다. string, object, array 가능
app.all('/secret', (req, res, next) => {
console.log('Accessing the secret section...')
next() // next handler를 호출함
})
app.all()
: 모든 http 요청 메소드 path에 대한 미들웨어 함수를 load한다
query parameter
/todos?id=1
- URL의 “
?
” 뒤에 추가되는 것 - 클라이언트가 서버로 작은 정보를 보낼 때 사용
- database query, filtering out할 때 쓰임
- key=value pair
route parameter
/api/todos/:todoId
- 웹 페이지를 구조화하기 위해 사용
req.params
객체에 key, value pair로 담겨져 있음
app.get('/users/:userId/courses/:courseId', (req, res) => {
res.status(200).send(req.params)
})
route handler
- 콜백 함수, 함수의 배열, 혹은 둘 다 가능
2.4. Node.js REST App structure
- 일종의 MVC (Model-View-Controller) 패턴.
- 아래로 갈 수록 저수준 레이어이다.
- 모듈화
│ .env
│ .gitignore
│ app.js
│ config.js
│ package-lock.json
│ package.json
└─ src
index.js
UserController.js
UserDAO.js
UserRouter.js
Users.json
UserService.js
routes layer
express.router()
로 route를 정의한다- 클라이언트 요청의 URL 경로, HTTP 메소드에 따라 컨트롤러에게 넘겨주는 역할
- 미들웨어를 통해 인증, 권한 확인, 요청 데이터 유효성 검사 등을 수행하기도 함
controller layer
- 라우터 메소드로 넘겨진 콜백 함수. = “Controller”
- 클라이언트 요청의 처리 흐름을 관리
- 라우트로부터 받은 클라이언트 요청을 service layer의 필요한 서비스를 호출함으로서 처리하며, 처리 결과를 클라이언트에게 응답으로 전달
service layer
- 앱의 실제 비즈니스 로직을 핸들링하는 역할
- 컨트롤러로부터 데이터를 받아 처리하고, DAO나 Repo를 호출하여 데이터베이스와의 상호작용을 관리
- 재사용 가능하고, 여러 콘트롤러 간의 코드 중복을 최소화하게끔 한다.
DAO(Data Access Object) layer
- 데이터 리소스에 대한 연산을 수행하기 위해 사용. = “Model”
- 데이터베이스와의 상호작용 관리하는 역할(우리의 경우 user.json 파일에서 데이터를 읽어온다)
- CRUD 연산 수행하며, DB 스키마에 따라 특정 데이터를 어떻게 조작할 것인지에 대한 로직을 담음
- DB 조작 세부 구현을 캡슐화하여 DB가 변경되어도 다른 계층의 코드를 변경하지 않도록 함
2.5. middleware for express
// 로깅을 위한 미들웨어
const LoggerMiddleware = (req, res, next) => {
console.log(`Logges ${req.url} ${req.method}
-- ${new Date()}`)
next();
}
// app.use(미들웨어) : 미들웨어 로드
app.use(LoggerMiddleware);
app.use(express.json()); // 요청을 JSON으로 파싱하는 내장 미들웨어(body-parser 기반)
// route
app.use('/api/v1/users', usersRouter);
// 에러 핸들링을 위한 미들웨어 (특정 URI가 존재하지 않을 때)
app.use((req, res, next) => {
res.status(404).send('Error resource not found');
});
- 로깅은 route hit 하기 전에, 에러 핸들링은 route hit 이후에 호출됨
질문 & 배운 점
Q. 미들웨어 함수에서 next()의 역할
next()
는 Express.js에서 미들웨어 간의 컨트롤을 전달할 때 사용된다. 라우팅, 에러 핸들링에서 중요하다.next()
가 호출되지 않으면 다음 후속 미들웨어가 절대 실행되지 않는다.res.send()
를 호출하거나next()
를 호출하거나 해서 요청-응답 사이클을 끝내야 한다. (그렇지 않으면 요청이 계속 대기되고 있다)- 에러 핸들링의 경우, 일반적으로 404 에러는 라우팅 체인의 마지막에 일어나는 경우가 대부분이며
next()
를 넣어줄 필요가 없을 수 있다(이미 응답이 전송됐을 것이기 때문. 하지만 다음 추가적으로 호출해야 할 에러 핸들링 미들웨어가 있다면 써준다) - 요약하면
next()
를 쓰는 것은 좋은 습관이다.
Q. request.param.userId
를 숫자로 타입 변환하려고 한다. 어느 계층에서 처리하는 것이 바람직할까?
유저ID를 파라미터로 받는 API에서 반복적으로 int 타입 변환이 이루어진다.
처음에 생각한 것은 재사용 가능한 미들웨어 함수를 만들어 라우터 단계에서 처리하는 것이다.
베스트 프랙티스는 무엇인지 궁금해져 찾아보았다.
검색해본 바에 따르면 router에서 처리하는 방법이 젤 흔히 사용되나, 설계에 따라 달라질 수 있다고 한다. 아래와 같은 구현 선택지가 있다.
-
Router에서 처리하기:
- 라우터에서 요청 파라미터를 받아서 처리하고, 변환된 값을 컨트롤러에게 전달하는 방식
- 일반적으로 많이 사용되는 방법이라고 한다.
// UserRouter.js router.get('/:id', (req, res, next) => { const id = parseInt(req.params.id, 10); UserController.getUserById(id); });
-
Controller에서 처리하기:
- 만약 파라미터 변환 로직이 좀 더 복잡하거나, 동일한 로직이 여러 라우터에 적용되어야 하는 경우, 컨트롤러에서 처리하는 것이 좋을 수 있다고 함.
// UserController.js getUserById(req, res, next) { const id = parseInt(req.params.id, 10); // ... }
-
미들웨어에서 처리하기:
- 파라미터 변환 같은 로직이 모든 라우터에 공통적으로 적용되는 경우, 이를 미들웨어로 만들어서 처리할 수 있음. 중복 코드를 작성하지 않아도 되어 편함.
// app.js or separate middleware file app.use((req, res, next) => { if (req.params.id) { req.params.id = parseInt(req.params.id, 10); } next(); });
나의 경우는 미들웨어를 사용하는 방법을 구현해보고 싶었다.
처음에는 app.js 파일에 app.use('/api/v1/users', parseUserIdToInt, userRouter);
이런 식으로 미들웨어를 로드했으나 제대로 작동하지 않았다.
이유는? req.params
가 아직 설정되지 않았기 때문이다!
req.params
는 URL의 경로 파라미터를 포함하는 객체로, url에 userId
가 존재하는 라우트에서만 위 함수가 제대로 작동한다.
이 경우 userRouter
라는 유저 라우팅을 담당하는 라우팅 핸들러보다 이 미들웨어가 먼저 실행되면 안 된다.
// userRouter.js
// userId를 int로 변환하는 미들웨어
const parseUserIdToInt = (req, res, next) => {
if (req.params.userId) {
req.params.userId = parseInt(req.params.userId);
}
next();
};
...
// 아래처럼 userId를 사용하는 route에 위 미들웨어를 넘겨준다.
routes.get('/:userId', parseUserIdToInt, (req, res) => {
// 라우트 로직
})
routes.put('/:userId', parseUserIdToInt, (req, res) => {
// 라우트 로직
})
따라서 위 코드처럼 userId
를 포함하는 라우트 핸들러에 위 미들웨어를 로드해주어야 했다.
중요 교훈: 미들웨어는 순차적으로 실행되므로 순서가 매우 중요하다!