자바스크립트로 서버와 클라이언트 구축하기 — Part 3 & 4. Node.js와 비동기 패턴
Part 3. Node.js
1. http 모듈과 서버 요청 테스트
Node.js를 활용하면 간단하게 웹 서버를 띄울 수 있습니다.
// 모듈 호출
const http = require('http')
// 서버 생성
http.createServer(function(request, response){
response.writeHead(200)
response.end('hello world')
}).listen(3000, () => { // 서버 리스닝
console.log('server on: 3000 port')
})모듈 호출
node.js에서 모듈을 사용할 때는 require를 이용합니다. 여기서는 http 모듈을 가져오는데, http 모듈은 Node.js 설치 시 자동으로 내장되어 있는 모듈이며, TCP/IP 기반 프로토콜로 HTML 페이지를 전달하는 데 주로 사용합니다.
서버 생성
![]()
Part 4. Node.js의 특징 - 비동기 패턴
Node.js의 가장 큰 특징은 다음 3가지로 볼 수 있습니다.
- 이벤트 기반 (Event-Driven)
- 단일 쓰레드 (싱글 쓰레드)
- 논 블로킹 I/O (
IO는 Input, Output의 약어)
1. 비동기(asynchronous) 패턴
이벤트 기반
- 이벤트가 발생했을 때 저장해둔 작업 방식을 수행하는 방식입니다.
- 이벤트 리스너에 미리 콜백 함수를 저장해놓고, 입력된 이벤트에 따라 해당하는 작업을 수행합니다.
- 발생한 이벤트는 순차적으로 처리하며, 발생한 이벤트가 없으면 대기합니다.
이벤트 루프가 처리 순서를 관리합니다.
싱글 스레드
- 프로세스 내에서 하나의 스레드가 하나의 요청만을 수행합니다.
- 논블로킹 싱글 스레드로 처리 시 멀티 스레드와 비슷하게 처리할 수 있으며, 멀티 스레드 기반의 프로그램보다 메모리나 기타 자원이 적게 소모됩니다.
논 블로킹
- 이전 작업이 완료될 때까지 기다리지 않고 다른 작업을 수행합니다.
- 통신 상대가 여럿이거나 여러 작업을 병행하려면 논블로킹 또는 비동기 모드를 사용해야 합니다.
- 블로킹보다 구현이 어렵지만 성능과 확장에 유리합니다.
책에 나와 있는 예시 코드의 동작 순서를 확인해봅니다.
이것은 논블로킹(비동기) 방식으로 처리되기 때문입니다.
fs.readFile의 파일 읽는 작업을 스레드로 받습니다.- 메인 스레드에서 해당 작업을 다른 스레드에게 넘기고
console.log('test')를 받아옵니다. - 메인 스레드에서
console.log('test')를 실행하는 동안fs.readFile작업을 받은 쓰레드가 콜백 함수를 메인 스레드에 넘깁니다. - 메인 스레드에서
fs.readFile의 콜백을 실행합니다.
만일 비동기 처리가 나열되어 있다면 어떻게 될까요?
이를 통해 비동기 방식으로 처리할 경우 순서를 보장받을 수 없다는 사실을 확인할 수 있습니다.
2. 비동기 패턴 해결 — 콜백(callback)
콜백 함수를 이용하여 비동기 패턴을 해결할 수 있습니다.
const fs = require('fs')
fs.readFile('./test.txt', (err, data = '') => {
console.log(1)
console.log(data.toString())
fs.readFile('./test.txt', (err, data = '') => {
console.log(2)
console.log(data.toString())
fs.readFile('./test.txt', (err, data = '') => {
console.log(3)
console.log(data.toString())
console.log('test')
})
})
})이 때 동작하는 방식은 다음과 같습니다.
- 첫 번째
fs.readFile을 실행합니다. - 메인 쓰레드는 해당 코드를 다른 쓰레드(
워커 쓰레드)로 넘깁니다. 워커 쓰레드에서 실행이 끝나면 결과를 메인 쓰레드에 반환합니다.- 1~3의 동작을 반복합니다.
문제는 이와 같은 방식으로 구조를 짤 경우 코드의 가독성이 매우 떨어진다는 것입니다. 콜백이 중첩되면서 코드가 점점 안쪽으로 들어가 화살표 형태가 되는데, 이것을 콜백 헬(지옥) 이라 합니다.
3. 비동기 패턴 해결 — Promise
Promise 패턴은 코드의 가독성 문제를 해결하고, 동기 형태로 구현하기 위해 사용됩니다.
원래는 응답을 지연시켜 동시에 실행하는 것을 제어하기 위한 목적으로 사용했던 패턴인데, 이것을 활용하여 비동기 처리를 하고 있습니다.
const fs = require('fs')
const _p = () => {
return new Promise((resolve, reject) => {
fs.readFile('./test.txt', (err, data = '') => {
if(err) {
reject(err)
}
resolve(data.toString())
})
})
}
_p().then((result) => {
console.log(1)
console.log(result)
return _p()
}).then((result) => {
console.log(2)
console.log(result)
return _p()
}).then((result) => {
console.log(3)
console.log(result)
return _p()
}).catch(err => {
console.log(err)
})Promise는 객체로, 비동기 작업을 처리 시 다음 상태 중 하나를 갖습니다.
대기(Pending)
이행하지도, 거부하지도 않은 초기 상태입니다. new Promise() 메서드를 호출하는 것이 대기 상태입니다.
이행(Fulfilled)
연산이 성공적으로 완료된 상태입니다. resolve에 해당하며, 이 상태가 되었을 때 then()을 이용하여 처리 결과 값을 받을 수 있습니다.
실패(Rejected)
연산이 실패한 상태입니다. reject에 해당하며, 실패한 이유에 대해 catch()로 받을 수 있습니다.
resolve를 호출하면 then이 실행되고, reject를 호출하면 catch가 실행됩니다. 그렇다고 실패 시 catch만 실행하는 것은 아닙니다.
이런 특성을 고려하지 않고 then() 마다 catch()를 붙여서 오류를 감지하는 방식으로 작성할 경우, 앞의 콜백 지옥처럼 가독성 측면에서 문제가 생길 수 있습니다.