인터넷에서 특정 키워드나 이미지 등을 검색 할 때, 보통 QueryString에 검색어에 대한 정보를 넣어서 서버로 전달하게 된다.
예를 들어 네이버에서 macbook을 검색한다면, 다음 url
로 이동되는 것을 볼 수 있다.
https://search.naver.com/search.naver?sm=tab_hty.top&where=nexearch&query=macbook&oquery=%EB%A7%A5%EB%B6%81&tqi=h8OEfsp0J1sssh17bvCssssssgV-351238
이렇게 QueryString에 데이터를 넣어서 전달하면 해당 URL이 캐싱되기 때문에, 중복 요청을 줄여 자원을 절약할 수 있다는 장점이 있으나, 길이의 제한이 있고(Explorer 11 기준 약 2047자) 정보가 그대로 노출되기 때문에 보안 상 민감한 정보들을 포함하면 정보 노출에 취약하다는 한계가 있다.
그럴 때는 http body로 데이터를 전송하는 방법을 사용할 수 있는데, 이전에 본 것 처럼 form을 이용한 application/x-www-form-urlencoded
방식을 통해 a=1&b=2
와 같은 형식으로 전송할 수도 있지만, 최근에는 json
형태를 더 많이 사용하는 추세다.
json
은 다음과 같은 포맷을 갖는다.
{ "key": "value" }
기본적인 내용을 알아 보았으니, Node.js 서버에서 json
데이터를 수신, 파싱하는 코드를 살펴 보자.
const http = require('http');
http.createServer((req, res) => {
if (req.method === 'GET') {
res.writeHead(200, {
'Content-Type': 'text/html',
})
res.end('<h1>HOME</h1>');
return;
}
let buf = [];
req.on('data', chunk => {
console.log(chunk);
buf.push(chunk);
});
req.on('end', () => {
// String from Buffer
const str = Buffer.concat(buf).toString();
console.log(str);
// JS Object from String
const json = JSON.parse(str);
console.log(json);
json.ok = true;
res.writeHead(200, {
'Content-Type': 'application/json',
});
// String from JS Object
res.end(JSON.stringify(json));
});
}).listen(3000);
이전에 보았던 form
을 파싱한 코드의 유사하다.
우선 스트림 데이터를 수신할 때마다 req.on('data')
에서 바이너리 데이터를 받아와 buf
배열에 각 요소를 저장한다.
데이터를 모두 받아오면 req.on('end')
에서 받아온 데이터를 하나의 Buffer
에 병합하여 문자열로 변환한다.
클라이언트에서 json
포맷의 데이터를 전송할 것이기 때문에, 문자열을 JS의 Object로 변환할 수 있다.
json
자체가 JS 객체 문법을 따르기 때문에 상호 변환이 가능하기 때문이다.
JS 객체로 변환되어 잘 작동하는지 확인하기 위해, 변환한 json
객체에 ok
라는 새로운 필드를 추가한다.
그리고 Content-Type
을 application/json
으로 설정하고, JSON.stringify(json)
으로 JS 객체를 다시 문자열로 변환한 뒤, 클라이언트에 전송한다.
위 과정을 실제 데이터를 넣어 POSTMAN에서 실험해보자.
서버를 실행시킨 뒤, json
body를 추가하여 POST로 localhost:3000
에 요청을 보낸다.
req.on('data', chunk => {
// <Buffer 7b 0a 20 20 20 20 22 6e 61 6d 65 22 3a 20 22 6d 79 4e 61 6d 65 22 2c 0a 20 20 20 20 22 6c 65 76 65 6c 22 3a 20 39 39 0a 7d>
console.log(chunk);
buf.push(chunk);
});
입력한 데이터는 { "name": "myName", "level": 99 }
였으나, Node.js 서버에서 raw 바이너리 데이터로 수신된다.
스트림 방식이지만 데이터가 짧으므로 수신이 완료되었다.
이제 buf
는 [<Buffer 7b 0a 20 20 20 20 22 6e 61 6d 65 22 3a 20 22 6d 79 4e 61 6d 65 22 2c 0a 20 20 20 20 22 6c 65 76 65 6c 22 3a 20 39 39 0a 7d>]
와 같이 요소가 1개인 배열일 것이다.
req.on('end', () => {
// String from Buffer
const str = Buffer.concat(buf).toString();
console.log(str);
// ...
}
buf
를 하나로 병합하여 문자열로 변환하면 클라이언트에서 보낼 때 그대로 문자열화 된다.
{ "name": "myName", "level": 99 }
이를 JS 객체로 변환하여 필드를 추가한다.
// JS Object from String
const json = JSON.parse(str);
console.log(json); // { name: 'myName', level: 99 }
json.ok = true;
console.log(json); // { name: 'myName', level: 99, ok: true }
잘 작동하는 것을 볼 수 있다.
하나 알아두면 좋은 것이, 문자열이나 바이너리 데이터 등을 각 프로그래밍 언어의 객체 형태로 변환하는 작업을 부르는 다양한 용어들이 있는데, Java, C#, Rust 등에서는 이를 Serialization/Deserialization이라 부르고, Go에서는 Marshalling/Unmarshalling 이라고 부른다.
아래와 같은 변환이 Deserialization(Unmarshalling), JS에서는 JSON.parse()
"{ "name": "myName", "level": 99 }"
-> { name: 'myName', level: 99 }
그리고 다시 객체를 문자열로 변환하는 것이 Serialization(Marshalling), JS에서는 JSON.stringify()
{ name: 'myName', level: 99 }
-> "{ "name": "myName", "level": 99 }"
이제 마지막 부분이다.
res.writeHead(200, {
'Content-Type': 'application/json',
});
// String from JS Object
res.end(JSON.stringify(json));
Response Header의 상태 코드를 200으로, Content-Type을 application/json으로 설정한다.
그리고 JSON.stringify(json);
를 통해 JS 객체를 다시 문자열 형태로 변환한 뒤, res.end()
로 그대로 클라이언트에 응답을 보내준다.
결과는 위와 같다.
ok
필드가 잘 추가된 것을 볼 수 있다.
JSON.parse()
를 통해 파싱을 해야 한다는 점을 제외하고는 form과 크게 다르지 않았다.
오히려 파싱을 자동으로 해주기 때문에 더 편하게 느껴지기도 했다.