Til Home

2020-08-13-TIL

Fact

-

Feelings

-

Findings

-

Event Emitter 외에 Promise, async/await 등 다른 비동기 방식이 동기방식과 달리 어떻게 다르게 동작하는지 자기만의 방식으로 정리한다.

동기식 처리 모델(Synchronous processing model)은 직렬적으로 태스크(task)를 수행한다. 즉, 태스크는 순차적으로 실행되며 어떤 작업이 수행 중이면 다음 태스크는 대기하게 된다. 예를 들어 서버에서 데이터를 가져와서 화면에 표시하는 태스크를 수행할 때, 서버에 데이터를 요청하고 데이터가 응답될 때까지 이후의 태스크들은 블로킹된다.

동기식 처리 모델(Asynchronous processing model 또는 Non-Blocking processing model)은 병렬적으로 태스크를 수행한다. 즉, 태스크가 종료되지 않은 상태라 하더라도 대기하지 않고 즉시 다음 태스크를 실행한다. 예를 들어 서버에서 데이터를 가져와서 화면에 표시하는 태스크를 수행할 때, 서버에 데이터를 요청한 이후 서버로부터 데이터가 응답될 때까지 대기하지 않고(Non-Blocking) 즉시 다음 태스크를 수행한다. 이후 서버로부터 데이터가 응답되면 이벤트가 발생하고 이벤트 핸들러가 데이터를 가지고 수행할 태스크를 계속해 수행한다. 자바스크립트의 대부분의 DOM 이벤트와 Timer 함수(setTimeout, setInterval), Ajax 요청은 비동기식 처리 모델로 동작한다.

바스크립트에서 빈번하게 사용되는 비동기식 처리 모델은 요청을 병렬로 처리하여 다른 요청이 블로킹(blocking, 작업 중단)되지 않는 장점이 있다.

하지만 비동기 처리를 위해 콜백 패턴을 사용하면 처리 순서를 보장하기 위해 여러 개의 콜백 함수가 네스팅(nesting, 중첩)되어 복잡도가 높아지는 콜백 헬(Callback Hell)이 발생하는 단점이 있다. 콜백 헬은 가독성을 나쁘게 하며 실수를 유발하는 원인이 된다. 아래는 콜백 헬이 발생하는 전형적인 사례이다.

  • event emitter는 이벤트를 듣고 콜백을 실행한 다음 값을 사용하여 해당 이벤트를 방출하는 패턴이다.
process.on("a", value => (console.log(value)));

process.emit("a", 18);

// n: 18

process.emit("a", 5);

// n: 5

이벤트 ‘a’ 가 실행 될 시에 value => (console.log(value)) 콜백이 실행된다.

promise은 코드에 대해 상당히 선형적이고 이해하기 쉬운 관점을 유지한다 - 어떤 일이 일어나고 언제가 되는지에 대해 쉽게 추론할 수 있으며, 더 중요한 것은 정의된 코드의 어디에 있는지 알 수 있다.

이벤트는 명백한 시각적/개념적 연결 고리를 깨는 경우가 있다. 이벤트가 던져지는 위치와 대응에서 일어나는 일 사이의 명백한 시각적/개념적 연결 고리를 깨트린다 - 코드베이스의 어느 곳에서든 이벤트를 포착하고 작업을 수행할 수 있다.

이벤트는 편리하고, 수만 줄 이상의 코드들을 가진 코드베이스에서 일하고 있을 때, 이벤트들에 내재된 느슨한 결합은 코드베이스 전체에 예측할 수 없을 정도로 (그리고 더 나쁜 것은, 예측 불가능한/불가능한 순서로) 뛰어다니는 불명확하고 따르기 어려운 통제 흐름으로 이어질 수 있다.

물론 때로는 느슨한 결합이 모듈화나 우려의 분리 같은 것을 정확히 원하거나 필요로 하는 것이기도 하지만, 특히 그것을 사용하는 방법에 대해 별로 조심하지 않고 규율하지 않으면 쉽게 자신을 베어버릴 수 있는 양날의 검이다.

모듈/객체를 어떻게 나누고, 어떻게 모듈간의 협력관계를 맺는게 좋은지 자기만의 기준을 정리한다.

  • 모듈

    • 독립적으로 재활용 할수 있는 코드를 묶은 것
  • 모듈화

    • 프로그램을 각 기능별로 분할, 설계 및 구현기법
  • 모듈 객체를 유의미한 기능 혹은 모델로 나누는게 좋다고 생각한다.

    • 예를들어서 어제 풀었던 문제 처럼 cashier, manager, barista… 처럼
    • 혹은 자주 쓰는 기능을 모듈화 해서 다른 모듈들이 쓸 수 있게끔 따로 나눈다.
  • 협력 관계 맺는 방법은 프로그램을 어떻게 짰는지에 따라 다르겠지만 보통 결합도가 낮은게 좋다고 한다.

    • 결합도란 각 모듈별로 의존성의 척도를 말한다.
    • 강한 결합도가 있을 시에는 모듈 하나가 안되면 프로그램 전체가 안될 수도 있으니 약한 결합도가 있는게 좋다.

다같이 확인할 사항

비동기 방식으로 구현할 때 어려운 점은 무엇이고, 디버깅을 쉽게 도와주는 다양한 방법을 찾아서 비교한다.

  • callback hell callback

만일 비동기 함수의 처리 결과를 가지고 다른 비동기 함수를 호출해야 하는 경우, 함수의 호출이 중첩(nesting)이 되어 복잡도가 높아지는 현상이 발생하는데 이를 Callback Hell이라 한다.

콜백 방식의 비동기 처리가 갖는 문제점 중에서 가장 심각한 것은 에러 처리가 곤란하다는 것이다. 아래의 코드를 살펴보자.

비동기 처리 함수의 콜백 함수는 해당 이벤트(timer 함수의 tick 이벤트, XMLHttpRequest의 readystatechange 이벤트 등)가 발생하면 태스트 큐(Task queue)로 이동한 후 호출 스택이 비어졌을 때, 호출 스택으로 이동되어 실행된다. setTimeout 함수는 비동기 함수이므로 콜백 함수가 실행될 때까지 기다리지 않고 즉시 종료되어 호출 스택에서 제거된다. 이후 tick 이벤트가 발생하면 setTimeout 함수의 콜백 함수는 태스트 큐로 이동한 후 호출 스택이 비어졌을 때 호출 스택으로 이동되어 실행된다. 이때 setTimeout 함수는 이미 호출 스택에서 제거된 상태이다. 이것은 setTimeout 함수의 콜백 함수를 호출한 것은 setTimeout 함수가 아니다라는 것을 의미한다. setTimeout 함수의 콜백 함수의 호출자(caller)가 setTimeout 함수라면 호출 스택에 setTimeout 함수가 존재해야 하기 때문이다.

예외(exception)는 호출자(caller) 방향으로 전파된다. 하지만 위에서 살펴본 바와 같이 setTimeout 함수의 콜백 함수를 호출한 것은 setTimeout 함수가 아니다. 따라서 setTimeout 함수의 콜백 함수 내에서 발생시킨 에러는 catch 블록에서 캐치되지 않아 프로세스는 종료된다.

https://poiemaweb.com/es6-promise

콜백 지옥은 복잡하고 중첩된 콜백으로 코딩함으로써 야기되는 큰 이슈다. 여기서, 모든 콜백은 이전의 콜백의 결과물을 쓸때. 이런 식으로 코드 구조는 피라미드처럼 보여서 읽고 유지하기가 어렵다. 또한 한 함수에 오류가 있으면 다른 모든 함수가 영향을 받는다.

  • 해결방법
  • promise 를 쓰면 된다. promise는 후속 처리 메소드를 체이닝(chainning)하여 여러 개의 프로미스를 연결하여 사용할 수 있다. 이로써 콜백 헬을 해결한다.
  • 디버깅
  • async debugger
const { AsyncDebugger } = require("./lib/async-debugger.js");

function doTimeout() {
    console.log("Starting timeout.");

    setTimeout(() => {
        console.log("Timeout finished.");
    }, 2000);
}

const asyncDebugger = new AsyncDebugger();
asyncDebugger.notifyComplete(() => console.log("All done!"));
asyncDebugger.startTracking("test-1", doTimeout);

console.log("End of script");
  • Chrome DevTools
  • jest

스레드를 생성하는 것과 여러 비동기 방식을 비교해보고, 어떤 제약사항이 있는지 비교한다.

  • node api, browser api 등…

Future Action

-