이번 과제는 클라이언트에서 서버까지의 구조를 알아보는 여정의 일부입니다. 지난번에 만든 Chatterbox client-side app은 이미 만들어진 AWS 서버와 연결했다면, 오늘부터 진행할 스프린트에서는 이전의 서버를 떼어내고 여러분이 Node.js를 사용해 만들 로컬과 연결시킬 것입니다.
새로 만들 서버는 유저가 웹 브라우저에서 접속하고, username를 고르고, 메시지를 보내고, 같은 서버에 접속해있는 모든 유저의 메시지를 읽을 수 있게 해주어야 합니다.
이번 스프린트 역시 여러분이 주도적으로 문제를 해결할 수 있도록 설계되었습니다. 문제를 "어떻게" 해결할지에 관해 생각해보기도 전에 "무엇이" 문제인지를 파악하는 것부터가 시급한 과제가 되는 상황이 많이 발생합니다. 지시사항이 명확하지 않고 해결하기 어려운 문제를 마주해야 하는 상황에서, 생산적으로 고민하고 문제를 차근차근 해결해나갈 수 있도록 하는 도전 의식을 잃지 않기를 바랍니다. 익숙하지 않은 데다가 제대로 작성된 공식문서조차 없더라도 작업의 방향성을 찾고 과제를 완수할 수 있는 능력은 software engineer로서 성공하는 데에 크게 기여해줄 것입니다.
Advanced 콘텐츠는 여러분이 초보 개발자를 넘어 현업 엔지니어에게 기대할 법한 요구사항들로 이뤄져 있습니다. 문제를 해결하려는 시도에 앞서 충분한 컨텍스트가 필요할 수 있습니다. (예를 들어 socket.io를 이용한 요구사항을 충족시키기 위해 Websocket 프로토콜에 대한 지식을 요구할 수 있습니다) 그럼에도 불구하고, 도전해볼만 한 가치가 있으며, 여러분의 현재 수준을 넘어서는 기술적 성취를 도울 것입니다.
JavaScript 생태계에서, 인기있는 프레임워크의 앞글자를 따서 MERN stack으로 흔히 부르곤 합니다. (MongoDB, Express, React, Node)
이들 중 Express.js는 Node.js 환경에서 웹 어플리케이션 혹은 API를 제작하기 위해 사용되는 인기있는 프레임워크입니다.
이번 과제는 여러분이 기본 내장 node.js 모듈(http 모듈)로 작성했던 http 서버를 express 서버 프레임워크를 통해서 리팩토링 하는 것입니다.
express framework 는 npm을 통해 다운로드 받을 수 있습니다. express가 기존 http 모듈로 작성했던 서버와 갖는 큰 차이점은, 다음과 같습니다.
위 키워드를 이해할 수 있는 자료를 아래 덧붙입니다. 먼저 간단하게 웹 서버를 만들어보고, chatterbox server에 적용해봅시다.
express는 공식 문서를 따라하는 것으로도 간단한 웹 서버를 만들 수 있습니다. 위 Bare minimum requirements의 첫번째 요구사항을 달성하기 위해서는 다음과 같이 실습해보세요.
공식문서의 시작하기 -> 설치 를 참고하세요.
공식문서의 시작하기 -> Hello world 예제 를 참고하세요.
앞서 Mini node server 및 Chatterbox Server 스프린트를 통해 메소드(GET 또는 POST), 그리고 URL(/lower, /upper, /messages 등)로 분기점을 만드는 것을 경험해 보았을겁니다. 이것이 다름아닌 라우팅(routing)입니다.
라우팅은 URI(또는 경로) 및 특정한 HTTP 요청 메소드(GET, POST 등)인 특정 엔드포인트에 대한 클라이언트 요청에 애플리케이션이 응답하는 방법을 결정하는 것을 말합니다.
어떠한 라이브러리를 사용하지 않고, 순수 node.js 코드를 작성하면, 다음과 같이 작성해야 합니다.
const requestHandler = (req, res) => {
if(req.url === '/messages') {
if (req.method === 'GET') {
res.end(messages)
} else if (req.method === 'POST') {
req.on('data', (req, res) => {
// do something ...
})
}
}
}
반면에 express에는 자체 라우터 기능을 제공하므로, 라우터를 활용하면 아래와 같이 코드를 매우 직관적으로 처리할 수 있습니다.
const router = express.Router()
router.get('/messages', (req, res) =>{
res.send(messages)
})
router.post('/messages', (req, res) =>{
// do something
})
여기까지 익히고 나면, chatterbox server를 리팩토링 하는것이 크게 어렵게 느껴지지 않을 것입니다.
미들웨어는 간단하게 생각하시면 컨베이어 벨트 위에 올라가있는 request에 무언가 악세사리를 덕지 덕지 붙이거나, 혹은 불량품이라면 밖으로 걷어내는 역할을 한다고 보면 좋습니다. 미들웨어는 express의 가장 큰 장점 중 하나입니다.
미들웨어가 주로 쓰이는 상황을 먼저 알아봅시다. 다음은 미들웨어가 주로 쓰이는 상황입니다.
위와 같이, 우리가 node.js만을 이용해서 구현할 때에 다소 번거로운 작업을 미들웨어를 통해 적용하면, 보다 손쉽게 해결할 수 있습니다.
2,3번은 Express에서 흔히 사용되는 미들웨어입니다. 1,4번을 제외하고 각각의 경우를 한번 살펴봅시다. (1번, 4번은 직접 만들어볼 것입니다.)
앞서 순수 node.js 코드로 HTTP body(payload)를 받을 때에는 Buffer를 조합해서 다소 복잡한 방식으로 body를 얻어내야만 합니다.
let body = [];
request.on('data', (chunk) => {
body.push(chunk);
}).on('end', () => {
body = Buffer.concat(body).toString();
// body 변수에는 문자열 형태로 payload가 담겨져 있습니다.
});
이를 쉽게 만들어주는 역할을 하는 것이 바로 body-parser 미들웨어입니다.
const bodyParser = require('body-parser')
const jsonParser = bodyParser.json()
// 생략
app.post('/api/users', jsonParser, function (req, res) {
// req.body에는 JSON의 형태로 payload가 담겨져 있습니다.
})
앞서 순수 node.js 코드로 CORS 헤더를 붙이려면, 응답 객체의 writeHead
메소드 등을 이용해 일일이 Access-Control-Allow-*
헤더를 정의해줘야만 했습니다. OPTIONS
메소드에 대한 라우팅도 구현해줘야만 하죠.
const defaultCorsHeader = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Accept',
'Access-Control-Max-Age': 10
};
// 생략
if (req.method === 'OPTIONS') {
res.writeHead(201, defaultCorsHeader);
res.end()
}
이를 쉽게 만들어주는 역할을 하는 것이 바로 cors 미들웨어입니다.
const cors = require('cors')
// 생략
app.use(cors()) // 모든 요청에 대해 CORS 허용
const cors = require('cors')
// 생략
// 특정 요청에 대해 CORS 허용
app.get('/products/:id', cors(), function (req, res, next) {
res.json({msg: 'This is CORS-enabled for a Single Route'})
})
이번엔, 직접 미들웨어를 만들어보면서 작동원리를 이해합시다. 간단하게 아래에서 작동원리를 설명하지만, 분명 공식문서를 통해 더 깊은 이해를 하게 될 것입니다.
가장 단순한 미들웨어는 logger 입니다. 말 그대로 로거는 디버깅이나, 서버 관리에 도움이 되기 위해 console.log를 적절하게 찍어주는 역할을 합니다. 미들웨어는 다음 구성을 가집니다. 공식 문서에 나온 그림을 살펴봅시다.
위 그림은 endpoint가 /이면서, GET 요청을 받았을 때에, 사용하는 미들웨어입니다. 주목할 것은 파라미터 순서입니다. req, res는 우리가 잘 아는 요청/응답이며, next는 다음 컨베이어 벨트로 넘기는 작업을 합니다. (이 문서의 맨 위의 그림을 참고하세요.)
현재 미들웨어 내부에서는 아무런 일도 하고 있지 않습니다. 그저 next() 함수를 호출하여 다음 컨베이어 벨트로 넘길 뿐이죠.
이번엔 특정 endpoint가 아닌, 모든 요청에 대해서 미들웨어를 붙여봅시다. 이때에는 app.use라는 메소드를 이용합니다. 아래 코드를 직접 실행해보십시오. 모든 요청에 대해 LOGGED가 콘솔에 찍히는 것을 확인하세요.
const express = require('express');
const app = express();
const myLogger = function (req, res, next) {
console.log('LOGGED'); // 이 부분을 req, res 객체를 이용해 고치면,
//여러분들은 모든 요청에 대한 로그를 찍을 수 있습니다.
next();
};
app.use(myLogger);
app.get('/', function (req, res) {
res.send('Hello World!');
});
app.listen(3000);
목표는 다음과 같이, 모든 요청에 대해 메소드와 url을 찍는 것입니다. 직접 도전해보세요.
다음은 HTTP 요청에서 토큰(주로 사용자 인증에 쓰이며, 추후 Authentication 시간에 다룹니다)이 있는지 여부를 판단하여, 이미 로그인한 사용자일 경우 성공, 아닐 경우 에러를 보내는 미들웨어 예제입니다.
app.use((req, res, next) => {
// 토큰 있니? 없으면 받아줄 수 없어!
if(req.headers.token){
req.isLoggedIn = true;
next()
} else {
res.status(400).send('invalid user')
}
})
보통 접근 권한이 없는 웹사이트에 로그인 없이 접근을 시도하면 서버로부터 로그인 창 등으로 되돌려 보내는 경우를 종종 경험하였을 것입니다. 이런 식으로 컨베이어 벨트에 올라온 요청이 미들웨어가 요구하는 조건에 맞지 않으면 불량품으로 판단하고 돌려보내도록 구현할 수도 있습니다.
어느 회사를 들어가든지, 일단 무조건 API 문서부터 받아와야한다는 것을 알았다.
Express.js
로 리펙토링을 하고나니, Express.js
이전으로 되돌아가고 싶지 않다.