지금껏 NodeJS에서 createServer에 들어오는 req 와 res 객체들에 대해 배워보았다. 이제 요청에 들어있는 본문 데이터를 한번 분석해볼 시간이다.
NodeJS에서는 데이터 스트림이라는 개념이 등장한다. 들어오는 요청이나 파일 작업 등 다른 스트림도 존재하지만, 요청에 초점을 맞춘 스트림을 살펴보자.
스트림은 On-going process, 즉 지속되는 프로세스이며, 이 스트림에서 요청을 받는다고 칠 때, NodeJS는 받은 요청 본문을 '한꺼번에' 읽는 것이 아니라 '청크' 단위로 나누어서 처리하게 되고, 이럴 경우 이미 읽은 청크와 기다리고 있는 청크로 나뉘어지게 된다.
이때 굳이 동기적으로 모든 내용이 읽힐 때까지 기다린 후 작업할 이유가 없다는 것이다. 왜냐하면 내용이 짧으면 모르겠지만 파일을 업로드 하는 등 소요가 큰 작업의 경우 얼마나 양이 방대한지 NodeJS는 모르기 때문이다. 그러므로 빨리 끝낸 청크를 미리 데이터를 다룬다는 것이 목표이다.
하지만 해당 청크가 무엇인지도 모르기 때문에 코드를 이용해서 청크를 임의로 나눌 수도 없는 노릇이다. 그러므로 이렇게 들어오는 '청크'들을 '체계적'으로 관리하기 위해 '버퍼'라는 개념이 들어온다. 즉, 여러 개의 청크가 들어왔을 때 파싱이 모두 끝나기 전에 데이터를 다루어 작업할 수 있게 만드는 것이다. 다음과 같은 예제 문제를 통해 이해해보자.
//
if (url === "/message" && method === "POST") {
fs.writeFileSync('message.txt','Dummy');
res.statusCode = 302;
res.setHeader('Location','/'); // url "/"로 리디렉션
return res.end();
}
//
})
server.listen(3000);
이전 코드를 가져와보았다. 서버가 계속 돌아가면서, HTTP 메소드가 POST이며 URL이 message일 경우의 조건문이다. 이런 요청값(req)에 대해 event listenr를 넣을 수 있는데, 바로 on() 함수이다.
if (url === "/message" && method === "POST")
const body = [];
req.on('data',(chunk)=> {
console.log(chunk);
body.push(chunk);
})
fs.writeFileSync('message.txt','Dummy');
res.statusCode = 302;
res.setHeader('Location','/'); // url "/"로 리디렉션
return res.end();
}
on 함수는 첫번째 인자로 이벤트의 종류를 받는다. 여기서는 데이터와 관련된 요청이므로 data를 넣었고, 이외에 close, end, readable 등 다양한
이벤트들이 있다. 즉, 새로운 청크가 읽힐 때 data와 관련된 이벤트가 나온다.
두번째 인자로는 콜백함수이다. 이후에 처리할 일들에 대해 적으면 된다.
하지만 현재 이벤트 리스너를 통해 중간 다리를 놓아주긴 했지만, 해당 청크에 대해 콘솔을 찍으면 우리가 해체할 수 없는 형태의 Buffer 객체가 나온다. 다음과 같이 말이다.
<Buffer 6d 65 12 67 ~~~~ >
우리는 버스정류장처럼 이러한 청크들이(버스들) 잠시 멈추어갈 수 있는 정류장(버퍼)가 필요하다. 다음과 같이 말이다.
if (url === "/message" && method === "POST")
const body = [];
//
req.on('end',() => {
const parsedBody = Buffer.concat(body).toString();
console.log(parsedBody)
})
}
on 메소드의 end 이벤트는 " The 'end' event indicates that the entire body has been received." 즉, 요청한 본문의 청크들을 모두 받는데 성공했을 때를 의미한다. 데이터에 대한 이벤트 리스너가 계속 body에 청크를 넣어주고, 끝났을 때 Buffer를 통해 이 청크들을 모아놓은 데이터를 가지고 작업을 진행할 수 있다.
다음과 같은 경우에 콘솔을 찍으면 submit 을 할 때, form 안에 name 속성이 정의된 여러 input들이 chunk에 들어가는 값이 된다.
이후 fs.writeFilySync에 해당 데이터를 정의하면 된다. 이후 느낌표같은 것들을 보면 암호화되어 있는 것을 볼 수 있다.
지금껏 배운 건 NodeJS의 미가공 데이터들인데, 이를 숨기는 NodeJS의 프레임워크인 Express.js가 있다고 한다.
마지막으로 우리는 NodeJS가 비동기식으로 작동한다는 것을 알아야 한다. 자 다음 코드를 봐보자.
///
if (url == "/message" && method === "POST") {
const body = [];
req.on("data", (chunk) => {
console.log(chunk, "bye");
body.push(chunk);
});
req.on("end", (chunk) => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split("=")[1];
fs.writeFileSync("message.txt", message);
res.statusCode = 302;
res.setHeader("Location", "/");
return res.end();
});
}
res.write("<html>");
res.write("<head><title>First Page</title></head>");
res.write("<body><h1>Hello world!</h1></body>");
res.write("</html>");
res.end();
});
server.listen(3000);
NodeJS는 이벤트 리스너가 일어났다고 해서 해당 코드를 바로 실행시키는 것이 아니다. NodeJS는 내부적으로 이벤트 리스너를 다루는 시스템이 있는데, 이에 대해 이해하는 것이 중요하다.
우선 if 문에 들어가면, 이벤트 리스너를 곧바로 동작시키는 것이 아니라 이벤트와 이벤트 리스너가 있는 함수를 이벤트 이미터(event emitter)에 등록을 한 후 바로 다음 코드를 실행하게 된다. 즉, setHeader가 '/'로 리디렉션하기 전에 이미 다음 코드가 실행되어 Hello world!가 화면에 출력되는 것이다. 이에 다음과 같은 에러가 출력된다.
Cannot set headers after they are sent to the client
이미 클라이언트에게 보내진 후에는 헤더를 다시 설정할 수 없다는 것이다. 즉 이미 res.end()까지 도달했다는 것이다. 이미 end를 해서 프로세스를 종료시켰는데 그 이후에서야 이벤트 이미터에 등록해놓은 함수를 동작시켜서 충돌이 일어나게 되는 것이다. 즉, 응답을 발송(end) 했다고 해서 이벤트 리스너가 중단되는 것이 아니라는 것!
그래서 우선 전 코드를 빌려서 추가 설명을 하자면, 해당 코드를
///
if (url == "/message" && method === "POST") {
const body = [];
req.on("data", (chunk) => {
console.log(chunk, "bye");
body.push(chunk);
});
req.on("end", (chunk) => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split("=")[1];
fs.writeFileSync("message.txt", message);
});
// 이 지점에서 충돌이 발생, event listenr에게 영향을 줄 수 있는 부분, 왜냐고?
이미터에 등록이 될 뿐이지 아직 안에 리스너가 처리된 것이 아니므로,
위 작업이 처리되기도 전에 리디렉션 및 응답을 발송해버림.
res.statusCode = 302;
res.setHeader("Location", "/");
return res.end();
}
//여기서는 위에서 return이 필수적으로 실행되므로 충돌은 안남
res.write("<html>");
res.write("<head><title>First Page</title></head>");
res.write("<body><h1>Hello world!</h1></body>");
res.write("</html>");
res.end();
});
server.listen(3000);
이렇게 바꾸는 것이 맞는 이유다.
///
if (url == "/message" && method === "POST") {
const body = [];
req.on("data", (chunk) => {
console.log(chunk, "bye");
body.push(chunk);
});
req.on("end", (chunk) => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split("=")[1];
fs.writeFileSync("message.txt", message);
res.statusCode = 302;
res.setHeader("Location", "/");
return res.end();
// 해당 경우에서는 코드가 순차적으로 실행되므로 모든 작업이 실행됨
});
}
//하지만 이제 여기서 충돌이 일어남.
위의 return 문이 이벤트 리스너 안으로 들어갔기에 바로 실행되지 않기 때문
res.write("<html>");
res.write("<head><title>First Page</title></head>");
res.write("<body><h1>Hello world!</h1></body>");
res.write("</html>");
res.end();
});
server.listen(3000);
그럼 이 코드는 또 어떻게 수정하면 되냐고?
///
if (url == "/message" && method === "POST") {
const body = [];
req.on("data", (chunk) => {
console.log(chunk, "bye");
body.push(chunk);
});
return req.on("end", (chunk) => {
const parsedBody = Buffer.concat(body).toString();
const message = parsedBody.split("=")[1];
fs.writeFileSync("message.txt", message);
res.statusCode = 302;
res.setHeader("Location", "/");
return res.end();
});
}
res.write("<html>");
res.write("<head><title>First Page</title></head>");
res.write("<body><h1>Hello world!</h1></body>");
res.write("</html>");
res.end();
});
server.listen(3000);
무조건 해당 함수를 반환하도록 해놓으면, 밑에 코드는 실행되지 않기에 이벤트 이미터에 등록이 되어 조금 늦어져도 응답은 미리 발송되지 않고 리스너에 있는 코드를 따르게 된다.