브라우저와 서버가 HTTP 통신을 할 때, Request를 보내고 Response를 받는다.
이 Request와 Response는 사실 양식이 정해진 문자열일 뿐이다.
그 중에서도 Request의 첫 번째 줄은 Request-Line이라고 부르며, 다음과 같은 모습을 하고 있다.
GET /path?q1=a HTTP/1.1\r\n
처음엔 GET, POST 등의 METHOD가 들어간다.
이어서 공백 한 칸과 PATH가 들어간다.
또 공백이 한 칸 들어가고 HTTP 버전이 명시되며, 윈도우인 경우 \r\n, 맥, 리눅스 계열은 \n이 들어간다.
\r
은 carriage return, \n
은 line feed를 의미하는데, 아주 오래 전, 타자기 시절에는 타자를 칠 때마다 잉크를 물리적으로 종이에 찍어서 글자를 적었기 때문에 좌에서 우로 펜촉 부분이 이동하면서 글씨가 적혀 나갔다.
그래서 종이의 맨 오른쪽 까지 펜이 이동한 뒤에 맨 앞으로 이동한 이후 한 줄을 내려야 비로소 정확히 다음 줄의 맨 앞 칸을 가리키게 되었는데, 이 맨 앞으로 이동하는 것을 carriage return이라고 불렀다.
타자기 시절에 살지 않았기 때문에 이 부분은 정확히 이해가 되지 않아서 영상을 찾아봤는데, 영상을 보니까 바로 이해가 되었다.
만약 궁금하다면 몇 분만 투자해서 영상을 찾아보라.
그러면 이 부분은 영원히 헷갈릴 일 없이 기억하게 될 것이다.
어쨌든 \r\n
이나 \n
이나 플랫폼에 따라 조금씩 다르지만 결국 다음 줄을 나타내는 의미이며, 그 뒤로 Request Header가 위치한다.
크롬에서 View Source를 누르면 Request Header 문자열 그대로의 모습을 볼 수 있다.
꽤 많은 정보들이 들어있는데, 이번에 할 것은 QueryString을 파싱하는 것이기 때문에 아까 본 Request 첫 줄의 \r\n
혹은 \n
부분까지만 보면 된다.
아까 봤듯, Request 첫 줄인 Request-Line은 [Method][Request-URI] [Protocol Version]으로 구성되어 있다.
이 중에서 PATH에 QueryString이 포함되는데, req.url
을 통해 이 [Request-URI]를 가져올 수 있다.
간단하게 Node.js로 [Request-URI]를 가져오는 서버를 작성해보자.
const http = require('http');
http.createServer((req, res) => {
console.log(req.url);
res.end('ok');
}).listen(3000);
http://localhost:3000/?a=1&b=2&c=true
로 접속해서 콘솔에 출력되는 값을 확인해보면, /?a=1&b=2&c=true
가 나온다.
파싱은 매우 간단하다.
?
을 기준으로 나누면 ['/', 'a=1&b=2&c=true']
가 될텐데, 2번째 요소를 다시 한 번 &
로 나누고, 각 요소를 다시 한 번 =
로 나누면 모든 QueryString이 Key, Value로 나눠지게 된다.
완성된 코드는 다음과 같다.
const http = require('http');
http.createServer((req, res) => {
if (req.url !== '/favicon.ico') {
const qs = req.url.split('?')[1];
const pairs = qs.split('&');
const map = new Map();
pairs.map(pair => {
const s = pair.split('=');
map.set(s[0], s[1]);
});
console.log(map);
}
res.end('ok');
}).listen(3000);
참고로 if (req.url !== '/favicon.ico')
부분은 브라우저에서 웹사이트에 접속할 때, 자동으로 해당 사이트의 파비콘을 가져오기 위해 /favicon.ico
으로 요청을 보내는데, 그 때는 QueryString이 없어서 undefined
를 split
하게 되어, 파싱 과정에서 오류가 나기 때문에 저 때는 파싱을 하지 않기 위해 추가한 구문이다.
이제 파싱하는 부분을 살펴보자.
// req.url === '/?a=1&b=2&c=true'
const qs = req.url.split('?')[1];
// qs === ['/', 'a=1&b=2&c=true'][1] === 'a=1&b=2&c=true'
const pairs = qs.split('&');
// pairs = ['a=1', 'b=2', 'c=true']
위와 같은 절차로 QueryString이 Key=Value 형태의 문자열 배열로 변환된다.
const map = new Map();
pairs.map(pair => {
// s = ['a', '1'] (1)
// s = ['b', '2'] (2)
// s = ['c', 'true'] (3)
const s = pair.split('=');
map.set(s[0], s[1]);
});
마지막으로 map
메소드로 pairs
를 순회하면서 각 Key, Value를 나눠서 배열에 저장한다.
그리고 Map
에 1번째 요소를 Key로, 2번째 요소를 Value로 저장한다.
완성된 Map
은 다음과 같은 모습일 것이다.
Map(3) { 'a' => '1', 'b' => '2', 'c' => 'true' }
일반적인 자바스크립트 객체에 저장해도 되겠지만, Map
을 사용하면 .size()
메소드를 통해 요소의 갯수를 알 수 있으며 순서가 보장되고, Iterator
이기 때문에 for
문을 통해 순회가 가능하다는 장점이 있다.
다음 코드를 참고하자.
const map = new Map();
m.set('a', 1);
m.set('b', 2);
m.set('c', 3);
console.log(m.size()); // 3
for (const [key, value] of map) {
// a 1 (1)
// b 2 (2)
// c 3 (3)
console.log(key, value);
}
매우 간단한 파싱 작업이었지만 QueryString을 사용할 때마다 일일이 작업을 하는 것은 매우 번거롭다는 사실을 체험했다.
불편함을 먼저 느낀 뒤에야 비로소 이러한 작업을 대신 해주는 함수나 프레임워크 등을 만났을 때, 그 역할과 편리함이 크게 와닿는 것 같다.