5장 Stream API (1/3) - 스트림 개요 및 Readable Stream

이 글은 Node.js 디자인 패턴 CH 05 스트림 코딩의 일부를 참고해서 작성하였다. 이번 글은 Stream API에 대해 깊이 다루기보다 스트림 자체에 대해 다룬다.


스트림 개요

스트림은 파일을 버퍼 단위로 옮겨서 전부 옮길 때까지 기다린 후 처리하기보다 매 버퍼 단위로 전송하는 방식이다.

스트림은 본질적으로 비동기 패러다임으로, 기다린 후 처리하는 Sync 방식에 대비된다. 물론 fs.readFile 역시 Node.js 런타임에서 I/O를 처리해주니 스레드가 Block 되진 않겠지만, 애초에 I/O 수준에서도 기다릴 일이 없게 하는 것이 처리량에서 우위이지 않을까?

(처리량에서 정말 우위일지는 잘 모르겠다. 스트리밍 오버헤드에 대해 공부해본 적이 없기 때문.)

스트림의 공간 효율성

스트림은 메모리에 파일의 전체 내용을 올리지 않고 버퍼의 크기만큼만 메모리를 할당하기 때문에 공간 효율적이다. 더 좋은 점은 파일의 크기에 상관 없이 일정한 양의 메모리를 점유한다는 점이다.

이것과 별개로 V8 엔진은 32bit 기준 ~1GB, 64bit 기준 ~1.7GB 정도의 메모리만 사용하도록 설정돼있어(더 높이려면 빌드해야 함.) 파일이 큰 경우 전체 파일을 한 번에 메모리에 올릴 수 없음.


공간 비효율적인 파일 압축 코드 (ex: example.tar -> example.tar.gz)

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('fs');
const zlib = require('zlib');

const file = process.argv[2];

fs.readFile(file, (err, buffer) => {
zlib.gzip(buffer, (err, buffer) => {
fs.writeFile(file + '.gz', buffer, err => {
console.log('File successfully compressed');
});
});
});

공간 효율적인 파일 압축 코드 (Stream API)

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');
const zlib = require('zlib');

const file = process.argv[2];

// 파일을 읽는데에 buffer 크기만큼만 메모리를 점유하기 때문에 공간 효율적
// pipe 체이닝으로 각 chunk에 대해 이런 저런 처리를 할 수 있음.
fs.createReadStream(file)
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream(file + '.gz'))
.on('finish', () => console.log('File successfully compressed'));

참고로 gzip이 어떻게 스트림에 대해 동작하는지 궁금하다면 아래 글들을 참고해보면 좋을 것 같다.

How is it possible to GZIP a stream before the entire contents are known? | StackOverFlow

How does gzip compression rate change when streaming data? | StackOverFlow


스트림의 시간 효율성

Stream은 TTFB(Time to First Byte)에 강점이 있는데, TTFB는 파일의 크기에 비례하여 빠를 수 밖에 없다. 파일의 크기가 클 수록 읽는 데 대기시간이 필요하지만 Stream은 곧바로 응답을 보내기 시작하기 때문이다.

웹에서 TTFB는 매우 중요하다.

자세한 건 Next.js의 재밌는 이슈(Stream rendering to reduce TTFB and CPU load) 참고.

파일을 단위로 전송하는 Server-Client 모델

다음의 사이클을 단 1회 거치게 된다: read > compress > send > receive > decompress > write


chunk 단위로 전송하는 Server-Client 모델

위의 사이클을 매 chunk마다 거치게 되므로 파이프라이닝과 같은 형태로 병렬 처리가 가능하다. 물론 chunk의 크기마다 다르겠지만 각 단계를 거치는 만큼 오버헤드가 있을 것이다. (HTTP header 등. 이 부분에 대해선 잘 알지 못한다.)

Node.js 동시성을 활용하는 것이므로 순서를 맞춰줘야 하는데 Stream API가 알아서 처리한다고 한다.


아래는 파일을 전송하는 스트림 예제 코드이다.

client: 파일을 받아 디스크에 쓰는 역할

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const http = require('http');
const fs = require('fs');
const zlib = require('zlib');

const server = http.createServer((req, res) => {
const filename = req.headers.filename;
console.log('File request received: ' + filename);
req
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream(filename))
.on('finish', () => {
res.writeHead(201 /* CREATED */, {'Content-Type': 'text/plain'});
res.end('That\'s it\n');
console.log(`File saved: ${filename}`);
});
});

server.listen(3000, () => console.log('Listening'));

server: 파일을 읽고 전송하는 역할

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const fs = require('fs');
const zlib = require('zlib');
const http = require('http');
const path = require('path');

const file = process.argv[2];
const server = process.argv[3];

const options = {
hostname: server,
port: 3000,
path: '/',
method: 'PUT',
headers: {
filename: path.basename(file),
'Content-Type': 'application/octet-stream',
'Content-Encoding': 'gzip'
}
};

const req = http.request(options, res => {
console.log('Server response: ' + res.statusCode);
});

fs.createReadStream(file)
.pipe(zlib.createGzip())
.pipe(req)
.on('finish', () => {
console.log('File successfully sent');
})
;

스트림의 문제 해결력

스트림은 Composition으로 문제 해결을 한다. Express Middleware와 같이 마음껏 파이프라인을 만들어낼 수 있다.

  • 파이프라인은 각 기능 간에 결합이 없기 때문에 항상 1차원으로 코드가 표현된다. (분기가 없다는 게 아니라 가독성이 좋다는 것.)
  • 선언형으로 프로그래밍하기 수월하다. 선언형 패러다임은 코드를 요약해서 바라볼 수 있기 때문에 쉽게 이해하기 좋다.

스트림을 기반으로 비동기 이벤트를 처리하는 패러다임을 Reactive라고 하고 이를 위한 RxJS가 있다.

(ex) 암호화 기능 추가

1
2
3
4
5
// 복호화
.pipe(crypto.createDecipheriv("aes-192-gcm", "a_shared_secret"))

// 암호화
.pipe(crypto.createCipheriv("aes-192-gcm", "a_shared_secret"))

Node.js에서 지원하는 스트림

Node.js가 지원하는 스트림은 EventEmitter 객체를 상속하며 binary, 문자열 뿐만 아니라 거의 모든 Javascript의 값을 읽을 수 있다. 이러한 스트림에는 크게 네 종류가 있는데 이번 글에서는 (글이 길어지는 관계로) Readable만 다룬다.

  • Readable, Writable, Duplex, Transform

1. Readable

Readable 스트림은 외부에서 읽기 위한 스트림으로, 자신이 가진 값을 chunk로 써서 내보내는 역할이다.

사용 예:

readable 이벤트에 listener를 등록하고 이벤트 발생 시 버퍼에 있는 내용을 모두 읽기

API로는 아래의 함수가 있다.

readable.read([size]) // read는 동기 함수이다.

(ex) 표준 입력(stdin) 받아서 표준 출력(console.log, stdout.write)하기

1
2
3
4
5
6
7
8
9
10
11
process.stdin
.on('readable', () => {
let chunk;
console.log('New data available');
while((chunk = process.stdin.read()) !== null) {
console.log(
`Chunk read: (${chunk.length}) "${chunk.toString()}"`
);
}
})
.on('end', () => process.stdout.write('End of stream'));

Stream v1, v2에 따라 non-flowing mode, flowing mode 로 나뉘는데 어차피 v1은 사용되지 않으므로 설명을 생략한다.


ReadableStream을 하나 새로 만드는 예제

지금까지는 fs, http의 스트림을 그대로 사용했지만 직접 ReadableStream을 만들어 활용할 수도 있다.

stream.Readable을 상속해 abstract function인 _read([size])(public 인터페이스인 read와 헷갈리면 안 된다)를 구현하면 ReadableStream 객체를 하나 만들 수 있다.

구현을 위해 push(data[, encoding]) 함수를 호출해 내부 버퍼에 값을 쓸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const stream = require('stream');
const Chance = require('chance');

const chance = new Chance();

// [1] 생성
class RandomStream extends stream.Readable {
constructor(options) {
super(options);
}

_read(size) {
const chunk = chance.string();
console.log(`Pushing chunk of size: ${chunk.length}`);
this.push(chunk, 'utf8');
if(chance.bool({likelihood: 5})) {
this.push(null);
}
}
}

// [2] 사용
const RandomStream = require('./randomStream');
const randomStream = new RandomStream();

randomStream.on('readable', () => {
let chunk;
while((chunk = randomStream.read()) !== null) {
console.log(`Chunk received: ${chunk.toString()}`);
}
});

TODO

나머지 스트림 종류 다루기

백 프래셔

스트림 기반 비동기 제어

Pipe Composition

멀티 플렉싱, 디멀티 플렉싱

Atomic Design - 1 Intro

이 글은 Atomic Design의 목적과 활용에 대해 간단하게 다룬다. 이후 시리즈에서는 실제 적용 예시를 다루고자 한다.


목적

현대 프론트엔드는 두 가지 복잡성을 맞이했는데, Atomic Design은 이 문제를 해결하는데 효과적이다.

  • 이전에 비해 다양한 해상도를 지원해야 함.

  • UI 컴포넌트의 종류가 다양해지고 복잡해짐. 많은 상태를 갖게 됨.


즉 유지보수 비용이 크게 늘어난 상태이다. 소프트웨어 개발에서 복잡성을 낮추는 방식은 분할 정복과 의존성 격리를 통한 SRP 만족인데, Atomic Design은 프론트엔드의 구성 요소를 디자인 수준에서 분할하므로, 개발자는 이를 재사용하여 총 개발 비용을 낮출 수 있다.

기존에도 HTML, CSS 수준에서 컴포넌트 개발이 어느 정도 가능했으나 React.js의 등장 후 CSS-in-JS 라는 생태계가 조성된 덕분에 컴포넌트 개념의 완전한 구현이 가능해졌기 때문에 Atomic Design이 더 많이 언급되고 활용되는 듯하다.


현대에는 디자인 시스템 (EN)을 사용하는 것이 일반적인데, 디자인 시스템이란 브랜딩 디자인, 컴포넌트를 위한 스타일 가이드, 컴포넌트 등으로 구성된 디자인 & 추상적인 개념의 집합이다.

이런 디자인 시스템의 Typo, Spacing 등을 포함해 컴포넌트를 라이브러리로 개발해놓으면, 전사적으로 디자인이 통일성을 갖게 되기도 하고 개발 측면에선 이미 개발된 디자인 요소를 빠르게 선언적으로 활용할 수 있기 때문에 생산성에 많이 유리하다.

  • 이미 널리 알려진 디자인 시스템의 라이브러리 또한 많다. 아무래도 Material Design의 구현체인 Material-UI가 가장 유명할 것이다.

Atomic Design의 접근 방식은 디자인 시스템을 구축하는 데 효과적이므로 많이 사용된다. 이후에 Atomic Design이 어떻게 개발 과정에서 유용하게 사용되는지 설명한다.

이론

가장 큰 개념으로 페이지, 컴포넌트가 있다. 컴포넌트가 조합돼 페이지를 구성하는 형태이다.

컴포넌트는 조합되므로 재사용된다. 즉 효율적이며 페이지 간 일관성 또한 보장된다.

컴포넌트는 아래의 4개의 하위 요소로 나뉜다. (작은 순으로 정렬)

  • 원자 (Atom)
  • 분자 (Molecule)
  • 유기체 (Organism)
  • 템플릿 (Template)

원자 (Atom)

더 분해할 수 없는 가장 작은 요소이다. 아주 작은 기능도 Atom의 조합으로 만들어지는, 콘텐츠로서의 구체성은 없다.

(ex) 검색 폼: { 검색 Text, 검색 Input 바, 전송 버튼 } 이라는 Atom의 조합으로 생성 (여기서 어떻게 조합하는지는 또 별개의 정보.)

종류:

  • 버튼
  • 텍스트 인풋, 색 팔레트, …
  • 타이포그래피, 제목 (h1, h2, h3, …)
  • 아이콘
  • 체크박스, 라디오 버튼

분자 (Molecule)

분자는 원자들의 조합을 그 단위로 한다. 분자부터 콘텐츠로서의 구체성이 있다.

(ex) 검색 폼: { 검색 Text, 검색 Input 바, 전송 버튼 } 이라는 구체적인 목적이 있는 요소.

분자는 원자들의 조합이지만 계속해서 재사용성이 유지해야 한다.

(ex) 검색 폼은 여러 페이지에서 사용됨.

아무래도 재사용성 유지를 위해 적은 수의 Atom을 조합하여 그 범용성을 높인다.


유기체 (Organism)

유기체는 분자 뿐만 아니라 원자까지 조합해 만들어진다.

유기체부터는 재사용이 강제되지 않는다. 즉 분자와 유기체의 차이는 재사용성이라고 생각할 수 있다.

(ex) Header, Footer 컴포넌트

유기체는 Container의 형태를 띄기도 한다. (ex) ProductList (ProductListItem의 배치 역할)

아무래도 이런 List 컴포넌트는 재사용하기 어렵기 때문에 유기체로 분류되는 것이 일반적이다.

몇 개의 Organism을 배치하면 하나의 페이지가 완성된다고 하는데 잘 모르겠다.


템플릿 (Template)

하위 컴포넌트들의 배치가 목적인 컴포넌트로, 페이지 구조나 레이아웃 구성을 담당한다.

하위 컴포넌트는 자신의 형태만 다룰 뿐 자신이 어떻게 배치될 지에 대해 책임지지 않는데, 템플릿은 이러한 컴포넌트들을 어떻게 배치시키고 표현할 것인지 결정한다.

약간 UI = render(state) 느낌이기도 하다.


페이지 (Page)

페이지는 템플릿에 예제/실제 데이터가 들어간 상태를 말한다. (ex) Mockup

템플릿에 데이터를 주고 초기화한 인스턴스라고 생각해도 좋다.


장점

Atomic Design은 디자이너도 함께 실행해야 하는 것이지만 개발자 측면에서의 장점만 나열하겠다.

1. 원활한 의사소통

컴포넌트 단위로 나누는 것은 그 단위가 미리 정의가 돼야 하며 모두가 동의해야 한다. 그러한 단위를 굳이 Atomic Design이 주장하는 바대로 따를 필요는 없지만 의사 소통 이전에 미리 정의돼야함은 변함이 없다. 이 때 Atomic Design은 이런 단위를 미리 정의해서 제공한다.

충분히 납득할만한 수준으로 잘 정의됐으며 이미 널리 알려져 있기 때문에 사용할 가치가 높다고 생각한다.

2. 의존성 분리

Atomic Design에 부합하도록 각 요소를 잘 분리한다면 아래와 같은 실익을 얻을 수 있다.

  • 개발 과정을 병렬로 진행할 수 있고
  • 각 요소가 변경되더라도 변경되는 요소를 최소한으로 줄일 수 있음

3. 작은 컴포넌트

Atomic Design의 각 단위에 맞게 하나의 역할을 하는 컴포넌트로 개발하면 각 컴포넌트를 작게 유지할 수 있다. 덕분에 그 개수가 늘어나도 변경에 의한 변경을 최소화할 수 있으며 따라서 복잡성이 지수적으로 올라가기보다 선형적으로 증가할 것이다. 뭐든 작게 유지하는 것은 key to computer science 이고 UNIX 철학에도 들어맞는다.

Make each program do one thing well.


주의점

1. 재사용을 위한 반응형 디자인

컴포넌트는 데이터에 따라 여러 상태를 가질 수 있고, 따라서 여러 해상도에서 표시될 수 있다. 컴포넌트가 여러 해상도를 지원하려면 가로 너비(width)가 변할 수 있게 개발해야 한다.

1
2
3
4
5
6
7
8
9
/* before */
.button {
width: 120px;
}

/* after */
.button {
width: auto;
}

2. 하위 컴포넌트에서 레이아웃 속성 최대한 피하기

Atomic Design에서 컴포넌트를 배치하는 역할은 상위 컴포넌트의 역할이다. 배치란 위치를 결정하는 일이고 따라서 각 컴포넌트는 자신이 그려지는 바깥 범위의 레이아웃을 방해하면 안 된다.

1
2
3
4
5
6
7
8
9
/* 여백은 배치(레이아웃) 속성이며 이렇게 스스로 값을 줘버리면 배치하는 입장에서 굳이 덮어써야 한다. */
.atom {
margin-right: 30px;
}

/* Atom을 조합하는 Molecule이 조합된 Atom을 알아서 배치하는 것이 적절하다. */
.molecule .atom {
margin-right: 15px;
}

3. CSS3 Flexbox/GridBox 사용하기

flex, grid 속성은 배치에 최적화된 속성이며 Organism의 경우 List 컴포넌트와 같이 Molecule을 배치하는 역할을 주로 하기 때문에 이 때 사용하는 것은 적절하다고 할 수 있다.

1
2
3
4
5
6
7
.organism {
width: 300px;
height: 300px;

display: flex;
align-items: center;
}

실제 활용 예시

Atomic Design으로 Todo 만들기 (KR)


출처: 더 괜찮은 웹 개발자가 되기 위한 리액트 스타일 가이드 (Aladin)

JS Async Functionality 2 - Promise

이 시리즈는 자바스크립트에서 비동기를 다룰 때 마주치는 개념들을 다룬다. 이번 글에서는 Promise를 다룬다.

JS Async Functionality 1 - Intro


Promise:

{ pending, fulfilled, rejected }상태를 가지는 객체로, executor 함수를 인자로 받는다.

  • executor 함수( (resolve, reject ) => {} )의 역할:

    1. 비동기 함수를 호출하고
    2. 그 비동기 함수의 콜백에서 resolve를 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const fetch = url => new Promise((resolve, reject) => {
    // 이 함수가 executor 함수이다. (주석 설명 참조)

    // 1. example Async API provided by Node.js
    http.get(options, result => {
    let data;
    result.on('data', chunk => data += chunk);

    // 2. Calls either [ resolve, reject ] from async callback.
    result.on('end', () => resolve(data));
    result.on('error', err => reject(err));
    });
    });

    executor 함수는 기존의 비동기 처리 방식을 그대로 옮겨온 것으로 이해하기 어렵지 않다.

  • 다만 Promise Chaining이라는 개념으로 Callback Hell을 1차원으로 들여쓰기 단계를 낮출 수 있다.

  • 이렇게 들여쓰기 단계를 줄이는 것은 중요한데 가독성에 의한 논리 오류가 빈번하게 발생했기 때문이다.


또한 Javascript 특성상 CPS 패턴으로 작성된 비동기 처리 함수에서, 이후에 호출되는 함수는 이전 함수의 클로저 참조도 할 수 있다.

  • 부주의하게 클로저 영역의 변수들을 사용하는 경우 메모리 사용량 면에서 좋을 게 없었다.
  • (ex) 전체 비동기 절차가 끝날 때 까지 호출 함수의 지역 변수들이 해제되지 못하는 등.

Promise Chaining:

Promise는 타입이자 객체이다. Promise(); 인스턴스는 자신의 실행 흐름에 관여하는 메소드를 세 개 갖는다: then, catch, finally (참고로 메소드는 총 5개이다. race, all이 그 나머지 둘이다.)

  • Prototype에 등록된 함수로, then은 Promise를 반환하고(그래서 Chaining이 가능하고) catch는 reject된 Promise에 한 해 수행되는 조건문으로 then(undefined, onRejected)와 동등하다. finally는 JS의 try-catch-finally의 finally와 동등하다.

then

then = (onFulfilled, onRejected) => Promise

즉 then의 두 번째 인자는 catch 절인 셈이다. 웬만하면 가독성을 위해 따로 catch 절을 사용한다.

  • onFulfilled = value => Promise (여기서 value는 Promise가 resolve한 값이다. 보통 비동기 함수의 결괏값.)

  • onRejected = value => {} (여기서 value는 Promise가 reject한 값이다. 보통 Error 객체.)

  • 1
    2
    3
    4
    5
    6
    7
    p.then(onFulfilled, onRejected);

    p.then(function(value) {
    // 비동기가 별 탈 없이 진행된 경우.
    }, function(reason) {
    // 비동기 함수 수행 중 오류가 난 경우.
    });

    then에서는 값을 그냥 반환하는 경우 Promise.resolve로 감싼 것과 같다. 즉 Promise가 반환되는 것인데, 그렇기 때문에 Promise Chaining이 가능한 것이다.


catch

catch = onRejected => Promise (!)

  • catch 메소드는 try-catchcatch와 같은 역할이다. 즉, catch가 성공했느냐, 실패했느냐에 따라 다시 then이 실행될 수도 있고 다른 catch가 실행될 수도 있고 앱이 멈출 수도 있다.

  • Case 1: catch 절에서 resolved Promise를 반환하는 경우: 이후의 then 수행

  • catch 절에서 따로 throw를 하거나, Promise.reject()를 호출하지 않는 경우 Promise는 resolved 상태로 변하여 then을 수행한 것과 동등하게 된다.

  • Case 2: catch 절에서 rejected Promise를 반환하는 경우: 이후의 catch 수행

  • JS의 try-catch에서 catch는 여러 개가 존재할 수 없는 것에 비해 Promise가 rejected 상태이면 catch절은 계속해서 호출된다. 보통 여러 개의 catch 절은 특정 오류만 잡고 싶을 때 사용한다.

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var p1 = new Promise(function(resolve, reject) {
    resolve('Success');
    });

    // 아래처럼 pin-point로 catch 절을 사용하는 것이 가능하다.
    // 기존 JS에서 try-catch를 여러 번 순차적으로 사용한 것과 동등하다.

    // 1.Promise를 생성
    p1.then(function(value) { // 1. throw를 호출해 catch 절로 이동
    throw new Error('oh, no!');
    }).catch(function(e) { // 2. reject 혹은 throw를 하지 않으므로 then 수행
    console.error(e.message);
    }).then(function(){ // 3. 이 then이 수행되게 됨.
    console.log('after a catch the chain is restored');
    }).catch(function () { // 4. 만약 [2], [3]에서 throw를 하는 경우 여기로 오게 됨.
    console.log('Not fired due to the catch');
    });

finally

(설명 생략)


알기 어려운 Promise의 특징

1. then, catch는 비동기로 실행된다.

  • 아무리 Promise.resolve(); 로 resolve가 동기로 수행되더라도 then, catch는 비동기로 queue 된다.

  • 1
    2
    3
    4
    5
    6
    7
    Promise.resolve(null).then(v => console.log('Async: ' + v));
    console.log('Sync!');
    // 결과:
    // Sync!
    // Async: null
    // WHY? then이 async로 microtask queue에 들어갔기 때문.
    // then이

2: then, catch는 비동기이지만 한꺼번에 수행된다.

  • 만약 then, catch가 setTimeout과 같은 일반적인 비동기였다면 Task Queue에서 처리된다. Task Queue는 한 작업만 처리하고 나머지 작업은 다음 순서로 넘긴다.

    • 1
      2
      3
      4
      5
      6
      function loop() {
      setTimeout(loop, 0);
      }
      loop();
      // 무한 루프에 걸리지 않는다.
      // Microtask가 아니므로 이벤트 루프에서 한 작업씩만(!!!) 처리한다.
  • 그러나 Promise, then, catch는 Microtask Queue에서 수행되는데, 이는 Event Loop 내의 Event Loop으로 생각하면 된다.

    • 굳이 이렇게 하는 이유는

      1. 다른 Javascript 수행이 되지 않음을 보장
      2. 화면이 변경되지 않음을 보장 하기 위해서이다.

  • Microtask가 호출한 microtask 역시 이어서 수행되며 microtask queue가 빌 때까지 이 단계는 끝나지 않는다.

  • Promise가 연속적으로 수행되어 문제가 발생하는 예제를 생각하려고 했으나 대부분의 비동기는 microtask를 사용하지 않기에 큰 문제는 없을 것 같다. 따라서 이 본문의 내용을 몰라도 거의 문제는 없을 것 같다.


출처

Promise then

Promise catch (재밌는 점은 catch 문서는 한국어 번역이 없다는 점이다.)

Using Promises


TODO

처음에 React를 통해 ES6를 배우면서 Promise를 접했을 때보다 문서 개수나 번역이 훨씬 좋아졌다는 걸 느꼈다. 앞으로의 JS 표준을 다루는 MDN Wiki 문서가 있으면 나도 기여해야겠다

NestJS를 사용해야 할 이유

Express를 사용해야 할 이유 (1)을 쓰면서 NestJS는 굉장히 매력적인 대상으로 다가왔다. 이번 글에서는 NestJS를 사용해야 할 이유를 조금 더 자세히 정리하고, NestJS를 간략히 알아본다.


NestJS를 사용해야 할 이유

NestJs만의 장점이 뭐가 있을까? 생각보다 좀 있었는데, 아무래도 Rich Framework의 특징을 많이 갖고 있다.

장점 설명
CLI 생산성에 도움이 되는 유틸을 제공한다고 한다.
문서화 자세하고 유지보수가 잘 되는 문서를 갖고 있다고 한다. (타 프레임워크에 비해 기능이 훨씬 많은데도)
활성화된 개발 팀 이전 글에서 알아본 바로 이는 굉장히 특장점이다. (다른 언어에 비해 백엔드가 많이 약하다고 생각함)
Nest 전용 모듈 Spring처럼 타 라이브러리를 쉽게 사용할 수 있게 전용 모듈을 개발했다. TypeORM, Mongoose, GraphQL, Logging, Validation, Caching, WebSockets 등의 모듈이 있다고 한다.
MSA를 염두에 둔 설계 (이건 내가 MSA에 대한 이해가 적어서 얼마나 효과적일지 모르겠음.)
Typescript 채택 (Typescript 경험이 일천해 얼마나 효과적일지 모르겠음.)
테스트 용이성 프레임워크 핵심 가치에 테스트 용이성이 있고 프레임워크에서 설계를 제공하므로, 다른 프레임워크에 독자적인 설계를 했을 때보다 테스트가 용이할 것으로 기대됨.

NestJS를 사용하는 기업

사용하는 기업 목록 중 SW적으로 큰 기업이 없는데 무슨 이유일지 모르겠다. 분명 시장의 선택은 합리적일텐데 정말 선택받지 못한거라면 중요한 문제가 있을 거라는 합리적인 의심을 해볼 만하다.


NestJS의 핵심


패러다임

Typescript를 지원하면서 OOP, FP, FRP(Functional Reactive) 요소를 조합한 백엔드 프레임워크이다.

Typescript 사용은 강제가 아니다.

Express, Fastify 를 기반해 개발됐고, NestJs가 윗단으로 추상화 계층을 제공하지만 바로 접근할 수도 있다.

NestJs는 Node.js HTTP 프레임워크 추상화 계층을 구현해놓았기 때문에, Express, Fastify 이외에도 Adapter 패턴을 통해 인터페이스 구현체만 제공한다면 어떤 기술 위에서도 작동할 수 있다고 한다. 아마 Fastify로 이주할 때 개발해 놓은듯하다.

@nestjs/platform-expres, @nestjs/platform-fastify 로 패키지가 분리돼있으니 참고해보면 재밌을 것 같다.

다만 NestJs에서 Express 생태계가 필요할 것 같지도 않고 Fastify가 훨씬 빠른데 굳이 Express를 기본값으로 해놓은 이유는 모르겠다.

NestJs는 Typescript를 사용하지 않는다면 Babel이 필요하다고 하며 Node.js 10.13버전 미만으로는 지원하지 않는다.

Typescript가 정확히 어떤 Javascript 버전까지 지원하는지 확인할 수 없었는데 조만간 Typescript를 학습하면서 정리해야겠다.


목표

프론트 3대 프레임워크 덕분에 개발자 생산성이 향상됐고 빠르고 테스트 가능하고 확장성있는 프론트엔드 개발이 가능해졌는데, 그 외 좋은 라이브러리들이 많지만 애플리케이션 구조, 설계 측면의 문제를 해결하는 프로젝트는 없었다. (이건 Javascript 계열의 특징이라고 생각한다. 아마도 대규모로 개발하는 제품에 Javascript를 쓰지 않기 때문인 것으로 보인다. 요즘은 언어가 많이 좋아졌는데도 말이다.)

NestJs는 애플리케이션 아키텍처를 제공한다(즉 개발자가 결정하는 게 아님). 테스트 가능하고, 확장성 있고, 느슨히 결합되고, 쉽게 유지보수 가능한 설계이다. Angular에서 영감을 받았다. (Angular는 강제성 있는 구조를 제공한다. React는 그 반대이고.)


NestJS Docs Intro 요약

문서의 내용을 요약했다.

프로젝트 제너레이터

nest new {project_name} 을 입력하면 프로젝트 폴더가 생성된다. (npm i -g @nestjs/cli로 설치되는 CLI 유틸인듯)

기본으로 생성되는 프로젝트 구조는 아래와 같다. 도메인이나 레이어 별로 폴더가 나뉘진 않는 듯하다.

src

  • app.controller.ts
  • app.controller.spec.ts
  • app.module.ts
  • app.service.ts
  • main.ts

아래는 Express에서의 파일 역할 비교이다.

file NestJS Express
Controller app.controller.ts (사용자 나름)
Service app.service.ts (사용자 나름)
App app.module.ts App.js
Index (진입점) main.ts index.js

재밌는 문서 구성

아래는 몇 개의 하위 문서에 들어갔을 때 맨 위에 보이는 모식도 몇 개를 가져온 것이다.

Spring에서도 제공하지 않는 모식도를 Nest에서 제공하는 게 재밌었는데,

  1. 개발진들이 정말 OOP를 좋아하는 것 같다고 느껴졌고
  2. 그래서 오브젝트 책을 보면서 같이 배우면 재밌을 것 같았고
  3. Spring에 비해 Nest는 확실히 설계를 결정해주는 느낌이 들어서 자신감이 느껴졌고

그동안 설계를 정해준 프레임워크는 사용해 본적이 없었는데 받아들이기만 한다면 생산성도 꽤 좋아질 것 같다. 설계 수준 또한 오픈 소스로 개발되니 어느 정도 검증됐을 거라고 생각한다. 따라서 꽤 좋은 학습 경험을 주지 않을까 생각이 든다.

Controller 문서의 모식도

Provider 문서의 모식도Module 문서의 모식도


출처:

Express를 사용해야 할 이유 (1) - 생태계 조사

잠시나마 사용해본 Express는 내게 React 같았다. 무엇이든 할 수 있어 보였으나 직접 하기에는 매우 불편하고, 그러다보니 REST API를 작성할 때 이런 것까지 해야 돼? 혹은 이런 기능이 없어서 불편하네 등이 많았는데 이번에 알아보려고 한다. 얼마나 많이 Express를 사용하며, 왜 Express를 사용하는지 팩트 위주로 체크해봤다.


1. Node와 Express의 장점을 헷갈리면 안 된다.

대부분의 웹사이트에서 소개하는 Express의 장점들은 Javascript, Node.js의 장점들이었다. 많은 글을 읽어보아도 Express의 장점을 소개하는 글은 많이 없었고 대부분 Node.js의 장점을 소개하고 있었다.

Express가 Node 기반인 게 큰 장점이라는 걸까… 그래서 Node.js와 같은 목적으로 생성된 프레임워크/런타임을 조사해보았다.


2. Reactor Pattern을 구현한 프레임워크/런타임

A. 역시 Node.js만 있는 것은 아니었다. Javascript를 깊게 배우고 생태계를 옮겨 탈 바에 기존에 사용하던 언어로 작업하는 게 현실적이긴하다.

Lang Sync Framework Async Framework
Java Spring Web MVC Spring WebFlux (Reactor Pattern), Vert.x (JVM 기반)
Python Flask, Django FastAPI, Tornado, Sanic, … (꽤 많다.)
Javascript - *

다른 언어에 대해선 찾아보지 않았지만 Java, Python이 점유율이 큰 언어들이므로 충분하다고 생각한다. 벤치마크를 찾아보진 않았지만 같은 패턴을 기반으로 제작됐기 때문에 실제 서비스로 구현했을 땐 성능 면에서도 비슷할 것으로 예상된다.

다만 Node.js의 장점이라면, 선천적으로 비동기 API가 장려되어왔기 때문에 비동기 API로 작성된 라이브러리 활용 면에서 낫지 않을까 생각한다.

Spring Web Flux 구조 - Node.js EventLoop과 유사

출처:


3. Node.js 백엔드 프레임워크 간의 점유율/만족도 비교

제대로 비교하기 전에 통계 자료부터 확인하자.


점유율 요약 (아래 그림):

  • Express의 점유율이 압도적이다.
  • Koa, Hapi 라는 네임드의 점유율이 꽤 낮다.
  • 서비스 개발에 가장 유리할 거라고 생각했던 Nest.js의 점유율이 13%밖에 안돼서 의문이다.

2020 점유율 순위

Hapi Koa Nest Express

물론 Express를 기반으로 하는 다른 프레임워크 등이 어느 정도 반영됐을 것이긴 하다. Nest도 처음에는 Express 기반이었으니까. 그래도 다운로드 수의 큰 차이를 보면 나머지 프레임워크의 시장성이 의심되긴 한다.


만족도 비교 (아래 그림):

  • Express는 점유율에 이어 만족도도 최상위권이다.
  • Nest.js가 5% point 정도의 차이가 있지만 준수한 편이다.
  • Koa의 만족도가 76%인 점인 이유는 장점이었던 동기식 코딩 방식인 async-await이 표준화됐기 때문임으로 보인다.
  • Hapi는 만족도가 매우 낮은 것으로 보아 사용할 수 없겠다는 생각이 들었다. (추후 조사를 해봐야겠다.)

2020 만족도 순위

출처: 2020 State Of JS (한국어 번역)


4. 왜 이렇게 Express를 많이 쓰는 걸까?

정말 Express가 좋은걸까?

다른 언어의 프레임워크를 비교해봤을 때 솔직히 좋다고 하진 못 할것 같다.


1. 단순함 (+0)

정말 많은 블로그에서 Express의 최장점을 단순함으로 꼽고 있었는데 장점보다는 목적에 가까운 것이라 생각한다. 목표에 따라 단순함은 장점이 될 수도, 단점이 될 수도 있기 때문이다. 단순함을 장점으로 꼽는 경우 둘 중 하나이다.

  • Rich Framework를 감당할 만큼 숙련된 개발자로 채우기 어려운 조직이거나
  • 애초에 큰 규모의 서비스를 작성하기 위해 Express를 사용하지 않거나

만약 서비스 개발을 위해 Express를 사용한다면 단순함은 직접적인 단점이 된다.

  • 기본적인 의존성만 담은 Boilerplate(1.4k stars)만 보더라도 같이 깔아야 할 라이브러리들이 많아 학습 곡선이 가팔라진다.

  • 처음 입문하는 경우 미들웨어들을 직접 찾는 추가적인 일을 하게 된다. (한 프레임워크 내에서 찾는 것과 대조적.)

  • Rich Framework 들에 비해 설계를 너무 근본적인 것들부터 해야 해 오히려 설계 측면에선 난도가 높다. (DI/IOC가 없고 여러 라이브러리를 비교 분석 후 사용해야 함.)

다만 적절한 Boilerplate를 찾으면 이 문제가 어느 정도 해소된다는 점과 이후 단락에서 소개할 내용들을 통해 단순함의 단점을 상쇄할 수 있다.

Node.js는 출시 후 아직까지도 작은 서비스를 만드는 데 적합하다는, 프로토 타이핑 위주라는 인식이 남아 있는 것 같고, 그런 용도로 채택하여 단순함이 종종 장점이 되는 것 같기도 하다.


2. Express Middleware (+0)

어떤 언어, 프레임워크로 웹 개발을 하더라도 Express에 미들웨어에 해당하는 계층에서 확장성을 가져가는 것은 기본이지 특별한 기능은 아니다. 또한 Express에서 제공하던 자체 Middleware들은 모두 Connect 미들웨어 라이브러리옮겨갔다. Next.js에서는 이 미들웨어들을 지원하는데, 그럼 다른 프레임워크에서도 의도하기만 하면 재사용 할 수 있는 셈이다. (의존성이 req, res, next 인자 밖에 없으니.)


3. Community (+3)

Express는 꽤 많은 사용자 풀을 보유하고 있다. 이미 사용자가 많아 검색을 통한 문제 해결이 비교적 원활하다.

아래는 StackOverFlow 트렌드인데 koa(js), hapi(js)는 태그로 잡히지도 않아서 비교가 불가능했다. 이는 생태계 조성이 거의 전무하다는 뜻인데 koa나 hapi는 출시된 지 시간이 지났음에도 이정도이며 특히 Hapi는 정말 작은 사용자 풀을 보여준다(사용하지 마세요).

StackOverFlow Trends: Express vs Nest vs Next


4. Async-await을 workaround로 쓸 수 있다. (+1)

Express v5 부터는 Response Handler 및 Middleware에서 async/await을 사용할 수 있지만 아직 Release 되지 않은 관계로 사용할 수는 없다.

Express QnA 이슈의 답변이다.

Q. How to use async/await in express 5?

A: There is one main difference between v4 and v5 when it comes to async/await and promises in general. In v5, if you return a promise from a response handler (or middleware), if that promise rejects and is not handled elsewhere, then Express will handle the error. It handles the rejection by passing the rejection reason to next for you.

v4에서도 async-await을 쓸 수 있는데, 아주 간단한 미들웨어 express-async-handler로 한 번 감싸주면 된다. (원리는 이 설명 참고) (같은 원리로 Promise도 처리 가능)

1
2
3
4
5
6
7
8
9
10
11
12
13
// find a user by id
router.get(
'/:id',
asyncHandler(async (req, res) => {
if (req.user.id === req.params.id) {
return res.status(403).send(FORBIDDEN);
}
const user = await UserRepository.findUserById(req.params.id);
if (!user) return res.status(404).send(NOT_FOUND);

res.json(user);
}),
);

5. Koa나 Express나 둘 다 개발은 하지 않는다. (+0)

Koa나 Express나 발전을 멈춘지 좀 됐다.

KoaJS는 2013년에 시작해 제너레이터 기반으로 미들웨어를 쉽게 작성하기 위해 나온 프레임워크인데, async-await 표준이 2017년 초부터 Node.js에서 공식적으로 지원되면서 그 의미가 퇴색되지 않았나 생각이 든다.

Express 역시 Documentation 위주의 유지보수, v5를 6-7년 째 안 내고 있긴 하다. (14, 15년도 쯤까지만 일한듯)


5. 서비스 개발 측면에선 NestJs가 더 낫지 않을까? (추후 보강 예정)

Express, Koa는 현재 사실상 유지보수가 되고 있지 않다. 프로젝트에서 돈을 벌지 못하기 때문인 것으로 보이는데, 기업 스폰서가 없으며 프레임워크도 간단해 기술 지원이 불가능해 수익 모델이 없다. (Hapi는 Walmart에서 사용 중이긴 하지만 너무 마이너하다. 왜 인기가 없을까?)

NestJS는 구조가 Angular의 영향을 받았다고 돼있지만 Spring과 유사한 구조와 개발자 경험을 제공한다고 생각하며, Spring은 그 기능과 복잡성을 통해 기술 지원으로 돈을 벌고 있기 때문에 NestJS가 이 모델을 구현한다면 긴 시간 유지보수를 해나갈 수 있을 것 같다.


정확히 무슨 벤치마크를 했는진 모르겠지만 성능 측면에서 NestJs-Fasitfy[현재 버전]가 Express보다 낫다고 한다. (출처)

Framework Req/sec Trans/sec Req/sec DIFF Trans/sec DIFF
Nest-Express 15370 3.17MB +4.38% +4.23%
Nest-Fastify 30001 4.38MB +2.20% +2.23%
Express 17208 3.53MB +8.38% +8.31%
Fastify 33578 4.87MB +6.55% +6.53%

NestJS에 대해선 추후 더 조사하려고 한다.


TODO

1. NestJS

NestJS는 다루는 양이 방대하기도 하고 앞의 리서치에서 시간을 너무 많이 사용해서 따로 시간을 내서 리서치하진 못 해서 다음 기회에 꼭 하도록 한다.

2. Fasify

한 번 조사해봐야 할 것 같다. async-await도 지원하며 제대로 관리되고 있는 것 같다.

아래는 README에 게시된 벤치마크인데 성능도 역시 좋고.

Framework Version Router? Requests/sec
Express 4.17.1 15,978
hapi 19.1.0 45,815
Restify 8.5.1 49,279
Koa 2.13.0 54,848
Fastify 3.0.0 78,956
-
http.Server 12.18.2 70,380

3. HapiJS

Hapi도 개발이 계속 진행 중이고 Nest처럼 Rich한 Framework를 목표로 하는 것 같고, Walmart에서 실제로 사용하면서 주도적으로 개발하다가 작년 중순부터 Community-driven으로 간다고 한다. 성장 가능성이 꽤 있는 것 같아서 시간이 나면 조사하면 좋을 것 같다. Facebook이 React를 만들어 프론트엔드 생태계를 많이 바꿔낸 것처럼.

4. Express In Action (2016)

이 책을 좀 더 읽어보고 Express의 가치를 발견하다면 정말 좋을 것 같다.

5. 기타

Promise, Async-await이 성능이 CPS 패턴에 비해 느리다는 의견이 종종 나왔는데 왜 그런지 확인해보기

D2에서 Node.js는 Socket.IO 때문에 떴다고 하던데 정말인지 확인해보기

Javascript의 Generator

이 글은 자바스크립트의 제너레이터 문법에 대해 간략히 소개한다.


정의

JavaScript의 제너레이터는 function* 으로 정의된 제너레이터 함수가 반환한 객체이다. 이 객체는 이터레이터(iterator)이다.

1
2
3
4
5
6
7
8
9
function* foo() {
yield 1;
yield 2;
yield 3;
}
// foo()로 생성된 제너레이터를 순회하며 값을 읽어간다.
for (let i of foo()) {
console.log(i);
}

Generator 객체와 함수(팩토리)

제너레이터 함수를 호출하면 제너레이터 객체를 반환하고 끝난다.

제너레이터 객체의 next(...args)를 통해 제너레이터의 본문을 일정 부분 실행할 수 있다.

  • 이터레이터는 next() 함수로 파라미터를 전달할 수 없다.
  • 제너레이터가 값을 읽을 수 있기 때문에 협력적 멀티 태스킹이 가능하다.

Generator 기반 협력적 멀티 태스킹

협력적 멀티 태스킹은 코루틴에 나오는 개념이다. (추후 정리할 예정이다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go(function* producer() {
for (let i = 0; i < 10; i++) {
yield write(i);
yield sleep(100); // -- sleep이 가능해진다!
}
});
go(function* consumer() {
let v;
while (
typeof (v = yield read()) !== 'undefined'
) {
console.log('read:', v);
}
});

이 코드의 go와 같은 함수를 제너레이터 실행기라고 한다. co 라이브러리가 훌륭한 제너레이터 실행기를 제공한다.

제너레이터 실행기는 원래 동기적으로 수행되는 제너레이터를 비동기 호출을 수행하게 만든 다음 callback을 통해 다시 제너레이터를 호출하게끔 하여 비동기 코드를 동기 코드처럼 작성할 수 있게 하는 목적의 함수이다.

제너레이터 실행기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 제너레이터 실행기
function grun(g) {
const it = g();
(function iterate(val) {
const x = it.next(val);
if (!x.done) {
if (x.value instanceof Promise) {
x.value
.then(iterate)
.catch((err) => it.throw(err));
} else {
setTimeout(iterate, 0, x.value);
}
}
})();
}

// 제너레이터 실행기를 사용한 모습.
// 꽤 async-await과 같이 가독성이 있다.
function* theFutureIsNow() {
let data;
try {
data = yield Promise.all([
nfcall(fs.readFile, 'a.txt'),
nfcall(fs.readFile, 'b.txt'),
nfcall(fs.readFile, 'c.txt'),
]);
} catch (err) {
console.error(
'Unable to read one or more input files: ' +
err.message,
);
throw err;
}
yield ptimeout(60 * 1000);
try {
yield nfcall(
fs.writeFile,
'd.txt',
data[0] + data[1] + data[2],
);
} catch (err) {
console.error(
'Unable to write output file: ' +
err.message,
);
throw err;
}
}

Generator 직접 만들어보기

자료 중 재밌는 것이 있었다: Babel은 Generator를 어떻게 바꾸나. 지금 나한테는 바로 이해하긴 어렵다. 직접 만들어보면 이해에 큰 도움이 될 듯 하다.

Generator의 단점: Iterable은 가변 인자가 아니다

제너레이터 객체는 위에서 말했듯 Iterable이지만, 이는 가변 인자와는 달라서, Math.min같은 함수를 이용할 때 spead 연산자로 배열로 만들어 넘겨야 하므로, 인자 전달 부분에서 아쉽다고 한다.

1
2
3
4
5
6
7
8
9
10
11
const foo = function* () {
/* ... */
};

// 제너레이터 객체만 넘길 순 없음.
// 굳이 제너레이터 객체를 넘길거면, 받는 함수 입장에서 이터레이터를 써야 할 듯?
// for-of 문으로.
let min = Math.min(foo);

// spread 연산자로 넘겨줘야 함.
let min = Math.min(...foo());

Generator의 콜 스택?

Calling .next() method just pushes that call on the top of the stack. It will then run the code inside the generator function.

difference: it has to remember the state of all local variables, but engines already know how to do that from the implementation of closures. A generator call will restore the state so that it can continue where it left off.

일반적인 콜 스택과 동일하다고 한다. 다만 제너레이터 내에서 제너레이터를 호출하는 경우 복잡하다고 하는데, 거기까지 알 필요는 없을 듯 하다.

출처: Javascript stack model for generators | StackOverFlow

출처: Javascript Generator의 재미 (2016.12)

출처: Learning Javascript, O Reilly

TODO:

부족한 내용 보충하기. 제너레이터가 개념 뿐 아니라 사용이 중요한 개념이어서 정리가 난잡한데 다음주 중으로 정리하려고 한다.

Promise.all은 Parallel로 실행되는가?

Javascript의 모든 코드는 이벤트 루프에서 처리된다. 그리고 Node.js의 이벤트 루프 구현체로 libuv가 사용 된다는 사실은 널리 알려져 있다. 그런데 이상하지 않은가?

Promise.all은 Parallel로 실행되는가?

이벤트 루프 모델을 이해했다면 자바스크립트에 병렬 실행은 없다는 것을 이해했을 것이다. 파일, 네트워크 I/O는 자바스크립트 코드가 직접 처리하는 것이 아니고, 콜백은 이벤트 루프에 의해 호출되어 순차적으로 실행된다.

그럼, Promise.all은 어떤가?

음… 애초에 자바스크립트 코드가 병렬적으로 실행될 수 있는가?

Promise.all을 잘 몰라서 생긴 일

Promise.all은 Promise의 호출 순서와는 전혀 관계가 없다. Promise의 기본 동작을 하나도 건드리지 않는다.

Promise.all이 제공하는 기능이란, 트랜잭션과 같이 하나라도 실패하면 catch 훅으로 넘어가게 하는 것이다.

1
2
3
4
5
6
7
Promise.all([p1, p2, p3, p4, p5])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.log(error.message);
});

오히려 순차적으로 Promise를 실행하는 것이 더 특별하다.

reduce를 사용해 iterable.reduce((p, fn) => p.then(fn), Promise.resolve())로 순차적으로 실행시킬 수 있다. (웬만하면 then으로 직접 잇겠지만.)

Async Functions in Javascript 1를 참고해도 좋을 것 같다.

출처: How does Promise.all all works interanlly

출처: Is Node.js native Promise.all processing in parallel or sequentially? | StackOverFlow

이벤트 루프는 환경마다 다를까?

Javascript의 모든 코드는 이벤트 루프에서 처리된다. 그리고 Node.js의 이벤트 루프 구현체로 libuv가 사용 된다는 사실은 널리 알려져 있다. 그런데 이상하지 않은가?

왜 Node.js를 설명할 때 이벤트 루프의 구현체라며 따로 소개하는 걸까?

과연 브라우저 상의 이벤트 루프와 Node.js의 이벤트 루프는 동일하다고 생각해도 될까?

이벤트 루프 구현체가 다르다는 것을 어떻게 생각하면 좋을까?


원인은 환경 차이

이벤트 루프의 처리 방식은 스펙으로 결정돼있지만 벤더마다 약간씩 다르게 구현하는 부분이 있다고 한다. Node.js 또한 예외는 아니다.

기능 브라우저 NodeJs
File, Network I/O No Yes
Event의 종류 Mouse, Keyboard Events File, Network I/O
이벤트 루프 구현체 libevent (크롬 기준) libuv
자바스크립트 엔진 V8 (크롬 기준) V8
process.nextTick (process 모듈 자체 API) No Yes
setImmediate (ECMA 표준 아님) No Yes

위 표 이외에 처리 순서의 차이가 약간 있긴 하지만 Node v11부터는 그 차이마저 적은 편이다.

결론: 환경의 차이 때문에 약간의 구현 상의 차이가 있다 정도로 받아들이자.

출처: JavaScript Event Loop vs Node JS Event Loop

Javascript의 라이센스

이 글은 자바스크립트의 라이센스 - 소유권과 결정권에 대한 내용과 근거를 찾아 정리한 글이다.


1. Javascript의 라이센스

Javascript 이해관계자는 크게 언어 명세을 결정하는 쪽과 언어를 개발하는 쪽으로 나뉜다.

주제 소유자 라이센스 생명 주기
언어 명세(ECMA 262) TC39 수정 하지 않는 선에서 사용 가능 매년
런타임(SW) 각 벤더 개별적으로 다름 상시 업데이트

재밌는 점은 TC39에 벤더들이 참여한다는 점이다.

Trademark(유사 상표권)는 Oracle에 있는데, 명칭하는 데 사용하는 것은 문제가 되지 않는다고 한다.

언어 명세(구현 코드가 아님)은 ECMA International(TC39의 상위 개념)에서 제정한다. 언어 표준의 라이센스는 이 글 | StackExchange을 참고

언어 - 인터프리터, 컴파일러, 런타임 - 구현의 몫은 각 벤더사에 있으며 벤더사가 적용하는 라이센스에 따라 각자 라이센스를 가질 수 있다.

아래는 역사를 요약 설명한 내용이다.

in 1995, Netscape decided to add a scripting language to Navigator. They pursued two routes to achieve this: collaborating with Sun Microsystems to embed the Java programming language, while also hiring Brendan Eich to embed the Scheme language.

Netscape management soon decided that the best option was for Eich to devise a new language, with syntax similar to Java and less like Scheme or other extant scripting languages. Although the new language and its interpreter implementation were officially called LiveScript when first shipped as part of a Navigator release in September 1995, the name was changed to JavaScript three months later.

1995: Netscape Navigator라는 브라우저의 기능으로 스크립트 언어를 내장하기로 개발

In November 1996, Netscape submitted JavaScript to ECMA International, as the starting point for a standard specification that all browser vendors could conform to. This led to the official release of the first ECMAScript language specification in June 1997.

1996: Netscape -> ECMA International로 소유권 이양 (이후 특정 기업의 소유는 아니게 됨)

출처: Javascript | Wikipedia

2. TC39 소개

Ecma International’s TC39 is a group of JavaScript developers, implementers, academics, and more, collaborating with the community to maintain and evolve the definition of JavaScript.

TC39에서는 미팅 회의록을 공개하는데, 최근 미팅을 보니 주요 브라우저 벤더사만 참여하는 것은 아니고 회의마다 여러 주체나 외부 인사도 참여하는 듯 하다. TC39에서 Javascript의 스펙을 결정한다고 한다.

This Standard defines the ECMAScript 2021 general-purpose programming language.

TC39는 가장 최신의 Javascript 스펙 문서를 온라인으로 유지한다. 현재는 ECMAScript 2021이며 이 문서는 Living Standard로 주기적으로 갱신되는 듯하다.

TC39이 표준을 제정하는 방식에 대해선 ECMAScript와 TC39 | ahn.heejong을 참고하라.

3. WHATWG 소개

The WHATWG was formed in response to the slow development of World Wide Web Consortium (W3C) Web standards (…) On 28 May 2019, the W3C announced that WHATWG would be the sole publisher of the HTML and DOM standards.

WHATWG는 W3C이 제정하는 표준 중 HTML, DOM 표준을 독자적으로 제정하는 그룹이다. WHATWG는 주요 브라우저 벤더로 구성된다. 자세한 내용

WHATWG에서 제정한 HTML 5 스펙은 HTML 뿐만 아니라 - HTML5라고 해서 HTML만을 다루지는 않는다 - (현대) 웹 기술의 전반에 대해 표준화된 문서이다.

4. 번외: 구글은 왜 Chrome, V8을 오픈소스화했을까?

[Chromium Release Announcement 발췌]

With a richer set of APIs we can build more interesting apps allowing people to do more online. The more people do online, the more they can use our services. […]

We believe that open source works not only because it allows people to join us and improve our products, but also (and more importantly) because it means other projects are able to use the code we’ve developed.

[StackExchange 답변 중]

they just needed the web as a whole to become more attractive. Their decision to open-source V8 led to one such effect: the NodeJS system was built on V8 … The web has become so attractive as a development environment that it is even displacing native apps, e.g. Electron is based on Chromium.

공유되는 일부 내용을 정리해보자면, 크롬을 통해 구글은 웹 시장을 키우고 싶었고, 오픈소스화(2008)로 웹의 기능을 늘리고 늘어난 기능이 널리 퍼지는 것을 유도했다(다른 크로미움 기반 브라우저를 통해). 오픈소스를 활용해 크롬을 개발했으며, V8기반의 NodeJS(2011)를 통해 Javascript 생태계가 커졌다. (숙련된 개발자가 많아질 수 있는 환경 조성)

출처: Why did Google make Chromium Open Source? | StackExchange

TODO:

Q. Netscape은 왜 넘겼을까? 돈이 안 돼서 그런걸까? 독자적인 기능으로 탑재했다면 더 성공할 수 있지 않았을까?

Q. 굳이 오픈소스화 하지 않아도 경쟁을 통해 기능은 늘어났을 것이고 크롬 출시연도인 2009년에 이미 2위 브라우저로 시작해 2012년엔 1위 브라우저가 됐는데 오픈 소스화가 여기서 얼마나 큰 역할을 했는지는 잘 모르겠다. 좀 더 찾아봐야 할 듯.

JS Async Functionality 1 - Intro

이 글은 자바스크립트에서 비동기를 다룰 때 마주치는 개념들인 Promise, Generator, Async-Await을 큰 범위에서 다룬다. 중간 중간에 재밌는 패턴들도 수록했다.


Why Promise?

What’s Promise?
Promise는 순차적인 비동기 코드를 깔끔하게 짤 수 있게 하는 문법이다. 문법에 포함된 Promise 객체로 처리한다. Promise로 거의 모든 비동기를 처리한다고 해도 과언이 아니다.

Promise가 익숙하지 않다면 MDN을 참고:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise > https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises

비동기 작업 시 순차적인 흐름을 많이 구현해야 하는데, CPS 방식으론 간결하게 짤 수 없다. CPS 패턴 사용 시의 Tip | JSQnA 참고.

Promise의 장점: (콜백과 관련한 비교에 대한 내용은 CPS 패턴 참고.)

  • 프로미스 체인을 사용하면 작업들을 순차 실행시키는 일은 그리 어렵지 않다.
  • throw를 프로미스 체인에서 사용할 수 있다.
  • catch 될 때까지 전체 체인에 오류를 자동으로 전파할 수 있다. 비동기 오류가 누락될 확률이 줄어든다.
  • 동기적으로 값을 반환해도 비동기적인 호출을 보장한다. 함수가 동기, 비동기 반환을 섞어서 하는 것은 나쁘다.
  • Promise.all 함수를 통해 비동기 작업을 병렬로 실행할 수 있다. (이건 CPS도 가능)
  • Promise.race 함수를 통해 비동기 작업 중 가장 먼저 수행이 끝난 결과만 사용할 수 있다. (CPS에선 직접 구현해야 함.)

Promise로 함수 배열을 순차적으로 실행하는 패턴 (현재 이해 부족으로 인해 수정 필요함.):

책에 재밌는 코드가 있어 가져왔다. Promise로 함수의 배열을 순차적으로 실행하는 방법이 있을까?

1
2
3
4
5
6
7
8
9
10
11
12
13
// sequential :: Array(() => Promise) => Promise
function sequential(tasks) {
// 빈 값을 반환하는 Promise를 생성한다.
let promise = Promise.resolve();
tasks.forEach((task) => {
// promise에 then으로 체인을 걸고,
// 다음 순번의 '이전 작업'이 되기 위해 promise 변수로 할당한다.
// UPDATE: task는 Promise를 반환하는 함수여야 한다.
promise = promise.then(task);
});
// 최종 Promise를 반환한다.
return promise;
}

reduce로도 가능하다:

1
2
3
4
5
6
7
8
9
10
11
const tasks = [
/* ... */
];
let promise = tasks.reduce(
(prev, task) => prev.then(task),
Promise.resolve(),
);

promise.then((result) => {
// TODO: retreive result
});

제한된 개수로 병렬 실행하기: (현재 이해 부족으로 인해, 추후 삽입 예정)


ES8 비동기 함수

정의에 대한 자세한 내용은 MDN async function, MDN AsyncFunction 생성자를 참고하라.

ES7 비동기 함수는 비동기적으로 동작하는, async, await 문법이 활용된 함수이다.

(설명 보충 예정.)


Why Generator?

What’s Generator?

Generator는 시작 지점이 여러 개이며 중간에 실행을 정지/재개할 수 있는 함수이다.

  • 시작 지점이 여러 개: 다른 시작 지점에 대해 매번 새로운 arguments로 호출할 수 있다.
  • 정지/재개할 수 있다: 제너레이터 함수는 실행 후 값을 반환할 때 정지한다. 이후 호출하면 다시 재개된다.

Generator가 익숙하지 않다면 MDN을 참고:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator

시작하기 전에 아래 두 코드의 결과를 모르겠다면 이후 내용을 이해하기 어려우므로, Generator에 대해 추가적으로 공부를 하기 바란다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Generator Example 1
function* fruitGenerator() {
yield 'apple';
yield 'orange';
return 'watermelon';
}

const newFruitGenerator = fruitGenerator();
console.log(newFruitGenerator.next());
console.log(newFruitGenerator.next());
console.log(newFruitGenerator.next());

// Generator Example 2
function* iteratorGenerator(arr) {
for (let i = 0; i < arr.length; i++) {
yield arr[i];
}
}

const iterator = iteratorGenerator([
'apple',
'orange',
'watermelon',
]);
let currentItem = iterator.next();
while (!currentItem.done) {
console.log(currentItem.value);
currentItem = iterator.next();
}

Generator with CPS into Async-Await:

놀랍게도 Generator에 약간의 양념을 치면 ES7 비동기 함수를 만들어낼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const fs = require('fs');
const path = require('path');

// 제너레이터로 비동기 흐름을 구현하는 방법이다.
function asyncFlow(generatorFunction) {
// callback 함수는 비동기 함수에 CPS 패턴으로 넘겨져서, 결괏값으로 다시 제너레이터를 호출하는 데 사용된다.
function callback(err) {
if (err) {
return generator.throw(err);
}
const results = [].slice.call(arguments, 1);
generator.next(
results.length > 1 ? results : results[0],
);
}
const generator = generatorFunction(callback);
generator.next();
}

// asyncFlow, callback을 감추고, yield를 await으로 바꾼다면 async-await과 같은 문법을 지닌다.
asyncFlow(function* (callback) {
const fileName = path.basename(__filename);
const myself = yield fs.readFile(
fileName,
'utf8',
callback,
);
yield fs.writeFile(
`clone_of_${fileName}`,
myself,
callback,
);
console.log('Clone created');
});

try-catch

Async-Await과 유사하게, 제너레이터에는 throw API가 있는데, 제너레이터 함수 내에서 try-catch로 이를 처리할 수 있다:

1
2
3
const twoWay = twoWayGenerator();
twoWay.next(args); // args를 전달
twoWay.throw(new Error()); // throw로 Error 객체 전달. 제너레이터 함수 내의 catch 절로 이동하게 된다.

참고자료 전문: Async-Await ≈ Generators + Promises

참고 2: Difference between async/await and ES6 yield with generators | StackOverFlow

참고 3: ES2017 - Async vs. Yield | StackOverFlow


하나의 API로 CPS와 Promise 모두 지원하는 방법

mongoose와 같은 많은 라이브러리는 CPS와 Promise 방식을 모두 지원한다. 어떻게 한 함수로 동시에 지원할 수 있을까? 아래 코드와 같이 구현한다면 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 마지막 인자로 callback 함수를 받는다.
// Promise로 사용하는 경우 callback 함수를 넘기지 않으니, 상관 없다.
function asyncDivision(dividend, divisor, cb) {
// 항상 Promise를 반환한다.
// 어차피 CPS 패턴을 사용하는 코드라면 Promise로 결과를 받아서 처리하지 않는다.
return new Promise((resolve, reject) => {
// 비동기로 반환
process.nextTick(() => {
const result = dividend / divisor;
if (
isNaN(result) ||
!Number.isFinite(result)
) {
const error = new Error(
'Invalid operands',
);
if (cb) {
cb(error);
} // 콜백이 있으면, 콜백을 호출한다.
return reject(error); // 콜백이 있든 없든, Promise reject로 catch 체인을 실행한다.
}

if (cb) {
cb(null, result);
} // 콜백이 있으면, 콜백을 결과로 호출한다.
resolve(result); // Promise resolve로 then 체인을 실행한다.
});
});
}

장점:

  • Promise, CPS 패턴 사용자 모두에게 기능을 제공할 수 있다.

비동기와 함수형 자바스크립트

Javascript는 순수한 함수형 언어가 아니므로, 모든 코드를 함수형 패러다임을 적용해서 작성할 수 없다고 한다. 비동기를 다루는 코드에 있어서는, 특히 async-await 키워드를 사용하여 작성할 때는 명령형 코드가 되므로, 더 함수형과 멀어지게 되는데, 결론적으론 Javascript에서 함수형 패러다임을 실천할 때에는 함수형인 코드 베이스와 그렇지 않은 부분으로 나누는 게 좋다고 한다. 또한 Promise든 Async-Await이든 하나를 택해서 통일하는 게 좋다고 하니 참고 바란다.

전문: JS: Promises, async/await, and functional programming.


TODO:

  • Generator는 아직도 공부 중이다. Iterable 프로토콜에 대한 얘기도 있고, 비동기 처리 외에 Generator의 쓰임새나 Generator 자체 개념에 대해 더 공부해야 한다.
  • 코루틴에 대해서도 공부해봐야 할 것 같다. 공부 중 접하게 된 키워드이다.
  • 제너레이터에 대한 설명을 보강해야겠다.
  • 이해가 완료되면 자체 제작한 예제 코드로 교체한다.