앞서 Node.js의 기본 http 모듈을 사용하면서 다양한 종류의 Request를 파싱해 보았다.
지금까지 했던 것들을 전부 모아서 express에서는 어떻게 하는지, 간단하게 살펴보도록 하자.
http://localhost:3000/?a=1&b=2
URL 맨 뒤의 ?
이후에 붙는 key=value
형태의 문자열을 일컫는다.
아래와 같이 파싱했었다.
const qs = req.url.split('?')[1]; // a=1&b=2
const pairs = qs.split('&'); // ['a=1', 'b=2']
pairs.forEach(pair => {
const [k, v] = pair.split('=');
// a 1
// b 2
console.log(k, v);
});
문자열을 직접 ?
기준으로 나누고, 다시 &
기준으로 나누고, 다시 =
로 나눠야 비로소 Key와 Value를 얻을 수 있었다.
// 1. parse querystring
app.get('/querystring', (req, res) => {
const { value } = req.query;
res.setHeader('Content-Type', 'text/html');
res.status(200).send(`<h1>querystring value: ${value}</h1>`);
});
express는 이런 귀찮은 작업을 대신 다 해서, QueryString을 자동으로 파싱하여 req.query
에 넣어준다.
GET /querystring?value=111
로 요청을 보내면 req.query.value
는 111
이 될 것이다.
주의할 점은 파싱은 해주지만 무조건 문자열이기 때문에 number
, boolean
등으로의 변환은 직접 해야 한다는 것이다.
전체 코드를 실행한 뒤, 위 URL로 요청을 보내면 정상적으로 동작하는 것을 확인할 수 있다.
(전체 코드는 포스트 맨 아래에 첨부 되어있다.)
URI에 동적으로 변하는 인자를 전달하는 방법이다.
GET /:value
혹은 GET /{value}
로 동적으로 변하는 부분을 표기한다.
Node.js에서 GET /params/:value
로 요청을 보낼 때, value
부분을 파싱하는 방법이다.
// GET /params/VALUE
const splitted = req.url.split('/'); // ['', 'params', 'VALUE']
const path = splitted[1]; // 'params'
const param = splitted[2]; // 'VALUE'
위 코드는 URL이 짧아서 간단하지만, /a/b/c/d/e/:value
와 같이 URL이 길어지게 된다면 파싱 하는 것이 상당히 귀찮아 질 것이다.
express는 이런 귀찮음을 대신 떠안아준다.
// 2. parse route parameter
app.get('/params/:value', (req, res) => {
const { value } = req.params;
res.setHeader('Content-Type', 'text/html');
res.status(200).send(`<h1>route params value: ${value}</h1>`);
});
req.params
에 모든 Route Parameter를 자동으로 파싱하여 저장한다.
요청을 보내면 이번에도 역시 잘 동작하는 것을 볼 수 있다.
사용자가 <form>
의 다양한 <input>
에 입력한 데이터를 Request Body에 담아, 서버에서 수신하는 방식이었다.
여기서부터는 Node.js로 처리할 때, 코드가 약간 더 길어졌었다.
let buf = [];
req.on('data', chunk => {
buf.push(chunk);
});
req.on('end', () => {
console.log(Buffer.concat(buf).toString());
// need parsing process like QueryString
res.end('ok');
});
위와 같이 데이터를 스트림으로 수신하여 하나의 Buffer로 병합한 뒤, 문자열로 변경한 다음, QueryString과 마찬가지로 파싱을 해줘야 하기 때문이다.
파싱 과정은 생략하였지만, 그 부분이 추가되면 코드는 더 길어질 것이다.
express에서는 이 과정도 매우 대신 처리해주기 때문에 매우 간단해진다.
또한 간단하지만 추가적인 처리 과정이 하나 필요하다.
app.use(express.urlencoded({ extended: true }));
위 코드를 삽입하면 된다.
이 코드가 클라이언트로부터 수신한 form을 JS 객체로 파싱해주는 역할을 한다.
원래는 a=1&b=2&c=3
형태로 들어오는 x-www-form-urlencoded
방식의 데이터를 자동으로 { a: '1', b: '2', c: '3' }
처럼 JS 객체로 변환해 주는 것이다.
역시 숫자 데이터를 넣더라도 문자열로 받는다는 점을 주의하자.
extended: true
라는 옵션은 파싱에 사용할 모듈을 qs
로 할 것인지(true
), 내장 querystring
모듈로 할 것(false
)인지 정하는 것이다.
보통은 true
로 설정해서 qs
모듈을 사용할 것이 권장된다.
또 하나 주의할 점은 위 코드를 라우터 위쪽에 삽입해야 한다는 것이다.
아니면 오류가 발생한다.
번지 점프를 할 때 미리 생명줄을 걸고 점프를 해야지, 뛰고 나서 생명줄을 걸려고 하면 걸 대상이 없어서 불가능할 것이다.
비슷한 논리로 코드도 위에서 아래로 실행되기 때문에 파싱하기 전에 미리 파서를 넣어줘야 제대로 동작할 수 있는 것이다.
// form html page
app.get('/form', (req, res) => {
res.setHeader('Content-Type', 'text/html')
res.end(`<form method="POST">
Name: <input type="text" name="name" />
Age: <input type="text" name="age" />
<button>Submit</button>
</form>`);
});
// need for parsing form
app.use(express.urlencoded({ extended: true }));
// 2. parse form
app.post('/form', (req, res) => {
const { name, age } = req.body;
res.setHeader('Content-Type', 'text/html');
res.status(200).send(`<h1>name: ${name}, age: ${age}</h1>`);
});
윗부분은 클라이언트에서 form을 전송하기 위해 html을 렌더링 해주는 부분이다.
GET /form
으로 접근했을 때 실행된다.
form에 데이터를 넣어 Submit 버튼을 누르면 POST /form
으로 데이터가 전송되는데, 이 때 req.body
에 객체 형태로 데이터가 자동으로 파싱되어 저장된다.
app.use(express.urlencoded({ extended: true }));
가 반드시 라우터 위에 와야 한다는 사실을 다시 한번 상기하자.
실행 결과는 다음과 같다.
만약 순서를 옮긴다면?
// 2. parse form
app.post('/form', (req, res) => {
const { name, age } = req.body;
res.setHeader('Content-Type', 'text/html');
res.status(200).send(`<h1>name: ${name}, age: ${age}</h1>`);
});
app.use(express.urlencoded({ extended: true }));
위와 같이 파서가 누락되어 req.body
는 undefined
가 되고, undefined.name
에 접근하는 셈이 되어 오류가 발생하는 것을 볼 수 있다.
Node.js로 작성하는 부분이 form과 유사하므로 생략하겠다.
단, json 형식으로 받아온 데이터는
JSON.parse()
로 문자열 -> JS 객체로 변환,
JSON.stringify()
로 JS 객체 -> 문자열로 변환할 수 있으며
각 과정을 Deserialization/Serialization 혹은 Unmarshalling/Marshalling이라고 부른다는 사실을 기억하자.
특히 Deserialization/Serialization이라는 용어가 자주 사용된다.
문자열 -> JS 객체 변환이 Deserialization,
JS 객체 -> 문자열 변환이 Serialization이다.
헷갈리지 말자.
// need for parsing json
app.use(express.json());
// 3. parse json
app.get('/json', (req, res) => {
const { name, age } = req.body;
res.setHeader('Content-Type', 'text/html');
res.status(200).send(`<h1>name: ${name}, age: ${age}</h1>`);
});
코드의 패턴이 form과 유사하므로 쉽게 이해할 수 있을 것이다.
app.use(express.json());
를 삽입하여 json 파서를 먼저 실행한다.
역시 순서가 중요하다.
form과 마찬가지로 req.body
에 json으로 받아온 데이터를 객체 형태로 파싱해서 저장해준다.
재미있는 점은 form과 달리, ""
가 없는 숫자는 number
타입으로 변환된다.
"1"
과 1
을 double quotes의 유무를 통해 타입을 구분할 수 있기 때문이다.
다음은 쿠키를 생성/삭제하는 방법에 대해 알아보자.
res.writeHead(200, {
'Set-Cookie': 'id=master',
'Content-Type': 'text/html; charset=utf-8',
});
res.end('<h1>쿠키가 생성되었습니다.</h1>');
Node.js에서는 위와 같이 Response Header에 'Set-Cookie'라는 옵션을 Key로 주고 key=value
형태의 문자열을 Value로 지정하면 브라우저에 쿠키가 전송되었다.
쿠키에 옵션을 추가하기 위해서는 Set-Cookie: id=a3fWa; Expires=Thu, 21 Oct 2221 07:28:00 GMT; Secure; HttpOnly
와 같이 문자열이 상당히 길어져야 하고, 쿠키가 여러 개인 경우를 다루진 않았지만 아래와 같은 문자열이 된다.
cookie1=1; cookie2=2; cookie3=3
쿠키가 여러 개에 각 쿠키마다 기나긴 옵션이 붙는다면 파싱하기도 상당히 귀찮아 질 것이다.
게다가 이렇게 문자열을 그대로 다루게 되면, 실수가 발생할 가능성도 점점 높아진다.
express가 자체적으로 갖고 있지는 않지만 third-party 모듈을 설치하면 쿠키도 자동으로 파싱해주기 때문에 훨씬 편하게 사용할 수 있다.
작업 중인 디렉토리 내에서 터미널을 열고 npm i cookie-parser
를 입력하여 설치한다.
다음과 같이 사용할 수 있다.
const cookieParser = require('cookie-parser');
app.use(cookieParser());
app.get('/cookie', (req, res) => {
res.cookie('id', 1, {
// domain
// encode
// expires
// httpOnly
// maxAge
// path
// sameSite
// secure
// signed
});
res.setHeader('Content-Type', 'text/html');
res.status(200).send('<h1>Cookie Generated</h1>');
});
이번에도 패턴은 똑같다.
파싱을 담당하는 파서를 불러와서 app.use()
에 등록한다.
위에서 아래로 코드가 실행되기 때문에 등록한 이후에 사용 가능하다.
res.cookie()
에 인자로 쿠키의 Key, Value를 전달한다.
3번째 인자로 다양한 쿠키 옵션을 줄 수 있는데, 문자열이 아닌 JS 객체라 자동 완성이 되기 때문에 옵션을 외우지 않고 뭐가 있나 볼 수 있기 때문에 훨씬 편리하다.
GET /cookie
로 접속한 뒤, 크롬의 Application 탭에서 Cookies를 살펴보면 정상적으로 id
라는 쿠키가 등록된 것을 볼 수 있다.
app.get('/remove-cookie', (req, res) => {
res.clearCookie('id');
res.setHeader('Content-Type', 'text/html');
res.status(200).send('<h1>Cookie Cleared</h1>');
});
쿠키를 삭제하고 싶을 때는 res.clearCookie()
에 인자로 쿠키의 Key를 주면 된다.
GET /remove-cookie
에 접속하면 방금 생성한 쿠키가 사라질 것이다.
Node.js에서 서버가 갖고 있는 html 파일 자체의 내용을 클라이언트에 보낼 때, fs
모듈의 readFile
메소드를 통해 파일을 읽어온 데이터를 보내주는 방식으로 처리했었다.
const http = require('http');
const fs = require('fs').promises;
http.createServer((req, res) => {
fs.readFile('./index.html')
.then(data => {
res.end(data);
});
}).listen(3000);
express는 res.sendFile()
메소드 하나로 같은 기능을 구현할 수 있다.
const path = require('path');
app.get('/file', (req, res) => {
res.sendFile(path.join(__dirname, 'index.html'));
});
path
모듈을 불러와서 사용한 이유는, OS에 따라 path
의 delimeter(구분자)가 달라지기 때문이다.
Windows는 C:\dir1\dir2\dir3
과 같이 \
로 path가 나뉘는 반면, macOS 등의 리눅스 계열은 /dir1/dir2/dir3
처럼 /
로 path가 나뉜다.
그래서 실행 환경에 따라 파일을 찾지 못할 수도 있게 되는데, path.join()
은 OS에 따른 path delimeter 보정을 알아서 해주기 때문에 이런 것에 신경 쓸 필요가 없어진다.
또한 __dirname
은 현재 실행중인 JS 파일이 위치한 디렉토리의 path를 갖고 있다.
만약 현재 실행된 JS 파일의 path가 /Documents/dev/main.js
라면 __dirname
은 /Documents/dev
가 될 것이다.
클라이언트에 보내줄 파일 이름은 index.html
이므로 path.join()
에 이 둘을 인자로 전달하면 /Documents/dev/index.html
이 될 것이다.
경로에 따라 다르겠지만 윈도우라면 C:\Documents\dev\index.html
과 비슷한 형태가 될 것이다.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home</title>
</head>
<body>
Home Page
</body>
</html>
Serving 할 html 파일은 위와 같다.
이제 GET /file
로 접속해보자.
정상적으로 html 내용이 렌더링 되는 것을 확인할 수 있다.