[포스코x코딩온] 웹 풀스택 과정 7기 4주차 화요일 회고
Node.js
Node.js는 브라우저 밖에서도 JS를 실행할 수 있게 해주는 런타임 환경이다.
Chrome 의 V8 엔진을 이용하여 만들어졌다.
이벤트 기반, Non-Blocking I/O, 싱글 스레드 모델을 사용한다.
이게 무슨 말인가 싶었는데 내가 알고 있던 지식과 검색해서 나온 결과에 맞추어 나름대로 생각해 봤다. 이는 단지 Node.js 의 특징이 아니라 JS라는 언어의 특징인 것 같다.
JS는 태생적으로 브라우저가 클릭이나 스크롤 등 사용자의 입력에 반응하기 위해 만들어진 언어이다.
즉, 이벤트 기반으로 설계된 것이다.
또한 이벤트가 일어나길 기다린다고 웹페이지를 멈출 수 없다.
따라서 입출력을 위해 다른 작업이 멈추지 않도록 Non-Blocking I/O 에 맞게 설계됐을 것이다.
그리고 JS 탄생 당시 JS는 복잡한 작업이 아닌, 웹페이지의 보조적 기능을 위한 경량 프로그래밍 언어로 설계됐다.
따라서 쉽고 가벼운 언어를 만들기 위해서 싱글 스레드 기반으로 설계됐다고 한다.
이와 같은 JS의 설계적 특징이 자연스럽게 JS를 실행하는 Node.js의 특징이 되었다고 생각한다.
추가적으로, Node.js 자체는 멀티 스레드로 실행되지만 JS 코드를 구동하는 스레드는 하나, 이벤트 루프 뿐이기에 싱글 스레드라고 한다.
- 참고
비동기
JS는 비동기적으로 동작한다. 즉, 어떤 작업을 실행하는 동시에 다른 작업을 실행할 수 있다는 것이다.
그러나 이는 예기치 않은 불상사를 낳을 수 있다.
예를 들어 많은 JS 초보자들은 setTimeout
함수를 사용하면 그 아래에 있는 코드는 setTimeout
함수가 끝날 때까지 실행되지 않는다고 생각한다.
하지만 JS를 써본 사람이면 대부분 알다시피 그렇지 않다.😭
예를 들어 다음과 같은 코드를 가정하자.
console.log(1);
setTimeout(() => {
console.log(2);
}, 1000);
console.log(3);
우리는 이 코드가 1을 출력하고, 1초를 기다렸다가 2를 출력한 후에 3을 출력하길 기대한다.
하지만 슬프게도 이 코드는 1과 3을 출력한 뒤 1초 후에 2를 출력한다.
이는 setTimeout
함수가 비동기적으로 실행되기 때문이다.
먼저 console.log(1);
이 콜스택에 쌓이고 실행된다. 그리고 setTimeout(...);
구문이 콜스택에 쌓이는데, 이 때 () => {console.log(2);}
콜백 함수는 콜스택에서 실행되지 않고 Web API로 넘어간다.
Web API는 setTimeout
함수를 실행하고 1초를 기다린다.
그동안 이벤트 루프는 콜스택에 console.log(3);
을 쌓고 실행한다.
그리고 1초가 지나면 Web API는 () => {console.log(2);}
콜백 함수를 콜백 큐로 넘긴다.
콜스택이 비어있는 것을 확인한 이벤트 루프는 콜백 큐에 있는 () => {console.log(2);}
콜백 함수를 콜스택으로 넘긴다. 이와 같은 과정을 통해 우리가 원치 않는 결과를 얻게 된다.
우리가 원하는 결과를 위해서는 어떻게 해야할까?
Callback
우리는 비동기적으로 실행되는 코드를 동기적으로 실행되는 코드처럼 작성할 수 있다.
먼저 이전에 실행될 코드와 나중에 실행되길 원하는 코드를 함수로 분리한다.
그리고 이전에 실행될 함수에서 나중에 실행될 함수를 콜백 함수로 받아 맨 마지막에 실행한다.
그러면 동기적으로 실행할 수 있다.
function log2Callback(callback) {
console.log(2);
callback();
}
function log3() {
console.log(3);
}
function main() {
console.log(1);
setTimeout(log2Callback, 1000, log3);
}
main();
물론 보다시피 보기 좋지 않다.
이와 같은 콜백 함수들이 이어지고 이어지는 것을 개발자들은 농담삼아 콜백 지옥이라고도 부른다.
이를 해소하기 위해 Promise
가 등장했다.
Promise
Promise
는 비동기적으로 실행되는 코드를 동기적으로 실행되는 코드처럼 작성할 수 있게 해주는 객체이다.
보통 Promise
는 fullfill
일 경우 실행되는 resolve
함수와 ok === false
일 경우 실행되는 reject
함수를 인자로 받는다.
resolve
함수는 then
메소드를 통해 실행되고, reject
함수는 catch
메소드를 통해 실행된다. 하지만 여기서는 간단하게 resolve
함수만 인자로 받는다.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function main() {
console.log(1);
delay(1000)
.then(() => {
console.log(2);
console.log(3);
});
}
main();
그다지 많이 깔끔해지진 않은 것 같다.
이를 더 깔끔하게 만들기 위해 async
와 await
가 등장했다.
async, await
또다시 새로운 개념이 나와 헷갈릴 수 있지만 그냥 Promise
객체를 더 깔끔하게 사용할 수 있게 해주는 문법이라고 생각하면 된다.
async
함수는 Promise
객체를 반환한다.
await
키워드는 Promise
객체가 resolve될 때까지 기다렸다가 해당 값을 반환한다.
이 때 await
는 async
함수 내에서만 사용할 수 있다.
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function main() {
console.log(1);
await delay(1000);
console.log(2);
console.log(3);
}
main();
훨씬 깔끔하다.
Node.js BE
Vanilla Node.js
순수 Node.js 만으로는 다음과 같이 서버를 열 수 있다.
import http from "http";
const server = http.createServer(function (req, res) {
res.writeHead(200, { "Content-Type": "text/html" });
res.write("Hello World!");
res.end(data);
});
server.listen(8080, function () {
console.log("서버 실행");
});
server.on("request", function (code) {
console.log("request 이벤트");
});
server.on("connection", function (code) {
console.log("connection 이벤트");
});
Express
Express
는 Node.js를 위한 BE 프레임워크이다.
Express
를 이용해 간단히 서버를 구현해봤다.
import express from "express";
const app = express();
const port = 8000;
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(port, () => {
console.log(`Listening at http://localhost:${port}`);
});