[Node.js 디자인 패턴] async/await이 나오게된 과정

DaiVernon·2021년 7월 8일
0

Node.js 디자인패턴

목록 보기
2/2
post-thumbnail

Callback 함수


기존의 Node.js에서는 콜백이 비동기 프로그래밍의 기본 방식으로 사용되었습니다. 하지만 개발자 친화적으로 디자인 되었나 생각해보자면 친화적인것과는 거리가 멀어보이는 것이 사실입니다. 콜백은 우리가 비동기적으로 작성해야 하는 기능들에 비하면 매우 복잡하고 장황하게 코드를 작성하게 디자인 되었습니다. 대부분 우리가 작성하려는 코드들의 제어 흐름 구조는 순차적인(serial) 실행 흐름으로 작성하고 있습니다. 콜백은 이런 실행흐름에 익숙하지 못한 개발자들에게 콜백 지옥(혹은 콜백 헬)이라 불리는 문제를 만들어 냈습니다. 또한 콜백 지옥의 문제가 없더라도 콜백을 통해 구현된 순차적인 실행은 불필요하게 복잡하고 오류가 발생하기도 쉬울뿐더러 오류를 잡아내기도 어려웠습니다(오류 우선 콜백으로 어느정도 해결하려 시도했지만 근본적인 문제는 해결할 수 없었습니다.). 그리고 콜백을 통해 오류를 관리하는건 상당히 많은 취약점을 가지고 있습니다. 특히 오류를 인자로 넘기는 것을 잊거나 관련 코드의 작성을 잊으면 해당 오류에 대한 컨트롤마저 잃게 되고, 동기적으로 작성된 코드에서 에러를 탐지하지 못하면 완성된 프로그램 자체가 제대로 작동하지 않게 되었습니다

Node.js(혹은 JavaScript) 팀은 계속해서 발생하고 있는 비동기 프로그래밍 문제에 대한 근본적인 해결책을 내놓지 못해서 비난을 받으며, 몇 년을 거쳐서 콜백의 문제에 대한 해결책으로 프라미스(Promise)를 만들게 됩니다.

Promise(프라미스)


프라미스(Promise)란, 상태(resolve, reject)를 "전달(carries)" 하는 객체로 비동기식 작업의 최종 결과를 표현합니다. 프라미스는 순차적인 실행을 연결(체인)해서 쉽게 구현할 수 있으며 객체이기 때문에 쉽게 전달할 수도 있습니다. 프라미스는 기존의 콜백으로 작성되던 비동기 코드들을 단순하게 만들어 줬지만, 개선의 여지가 남아있었습니다.

프라미스 체인으로 구현한 순차적인 실행흐름은 콜백 지옥보다는 낫지만 여전히 .then()을 호출해야 했고 각 체인에서 새로운 함수를 작성해야 했습니다. 일반적으로 순차적인 제어 흐름은 프로그래밍에서 흔히 나타나게 되는데, 이때마다 .then()을 호출하고 새 함수를 작성하는 것은 너무 과하고 번거로운 일이었습니다. 따라서 비동기 프로그래밍에서 순차적인 실행 흐름을 더 쉽게 처리할 수 있는 방법이 계속해서 요구되었고, 그에 따라 ECMAScript 표준에 async함수await 표현(async/await)이 만들어졌습니다.

async/await


async/await 을 사용하면 각각의 비동기 작업에서 현재 구문의 결과가 나올때까지 다음 구문의 실행이 차단되는 것처럼 보이는 함수를 만들 수있습니다. 또한 async/await을 쓰면 동기적으로 만들어진 코드처럼 가독성이 좋아진다는 장점도 가지고 있습니다.

따라서 async/await 은 Node.js와 JavaScript 모두에서 비동기적인 코드를 처리하는데 권장되는 구문입니다. 하지만 async/await 은 위의 작성한 내용에서 보다시피 콜백과 프라미스와 같은 기존의 비동기 제어 흐름을 대체하려고 나온 것도 아니고 대체 할 수도 없습니다. 오히려 프라미스는 콜백에 크게 의지하며, async/await은 프라미스에 크게 의지(async 함수의 반환값은 무조건 프라미스여야만 하며, 프라미스가 아닌 것을 return 하려 할 경우 그 반환 값은 이행된(fufilled) 프라미스로 감싸서 반환됩니다.) 합니다.

async 함수와 await 표현


async 함수는 특별한 유형의 함수로 await 키워드를 사용하여 주어진 프라미스가 해결될 때까지 함수의 실행을 "일시 정지"할 수 있고 함수의 반환 값은 무조건 프라미스로 표현됩니다. 따라서 await 키워드는 주어진 프라미스가 해결되는 것을 기다리므로 프로미스를 리턴해 주는 함수나, 프라미스 앞에만 작성하여 사용할 수 있습니다.

(async 함수는 에러 처리를 반드시 해줘야 하기 때문에 try, catch문을 같이 사용하기를 권장합니다. finally는 옵션)

예) 파일 읽어오기 작업에서 async 함수를 사용하고 싶을 경우

const fs = require("fs").promises; // 프라미스

async function readFileWithAsync(path) {
  try {
    // fs.readFile은 프라미스를 반환하기 때문에 await 키워드 사용 가능
    const data = await fs.readFile(path); 
    console.log(data);
  } catch (err) { // 에러가 발생 시
    console.error(`We have an error: ${err}`);
  } finally { // 작업 완료 시
    console.log("Done")
  }
}

예) 주어진 시간 후에 에러와 함께 reject를 반환하는 함수를 이용해 async 함수 사용해보기

function delayError(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error(`Error after ${ms}`));
    }, ms)
  })
}

async function playingWithError(throwSyncError) {
  try {
    if (throwSyncError) {
      throw new Error('This is a synchronous Error')
    }
    // delayError() 함수는 프라미스를 반환하기 때문에 await 키워드 사용 가능
    await delayError(1000) 
  } catch (err) {
    console.error(`We have an error: ${err}`);
  } finally {
    console.log("Done")
  }
}
profile
클린 코드, 클린 아키텍처

0개의 댓글