TypeScript :: 7. Promise와 async/await 구문

김진호·2022년 2월 7일
5

TypeScript

목록 보기
5/6
post-thumbnail

07-1 비동기 콜백 함수

이번 장은 node.js가 제공하는 readFile과 같은 비동기 API를 예로 들고 있다. 타입스크립트에서 readFile같은 노드 제이에스 API를 사용하려면 tsconfig.json 파일에 별도의 설정이 필요하다. 따라서 이번 장의 예제 소스를 동작시키려면 node.js 프로젝트 설정이 필요하다

실습 프로젝트 설정

먼저, ch07-1 디렉터리를 만들고 터미널에서 다음 명령을 실행한다. 각 줄의 명령은 package.json 파일을 생성하고, 관련 파일을 내려받고, 소스 파일을 저장할 src 디렉터리를 생성한다.

npm init --y
npm i -D typescript ts-node @types/node
mkdir src

그다음 02-1절에서 작성했던 tsconfig.json 파일을 그대로 복사해서 07-1 디렉터리에 붙여넣는다. 또는 tsc --init 명령으로 tsconfig.json 파일을 생성하고 다음과 같은 내용으로 대체한다. 특히 이번 장에서 다루는 예제를 문제 없이 실행되게 하려면 tsconfig.json에서 downlevelIteration 항목을 ture로 설정해야 한다.

{
  "compilerOptions": {
    "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
    "module": "commonjs" /* Specify what module code is generated. */,
    "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
    "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */,
    "sourceMap": true /* Create source map files for emitted JavaScript files. */,
    "outDir": "dist" /* Specify an output folder for all emitted files. */,
    "downlevelIteration": true /* Emit more compliant, but verbose and less performant JavaScript for iteration. */,**
    "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
    "strict": true /* Enable all strict type-checking options. */,
    "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */,
    "skipLibCheck": true /* Skip type checking all .d.ts files. */,
    "paths": { "*": ["node_modules/*"] }
  },
  "include": ["src/**/*"]
}

이와 같은 과정을 각 절을 시작할 때 반복해서 프로젝트를 구성한다.

동기와 비동기 API

node.js에는 파일 시스템(file system)과 관련된 기능을 모아둔 fs 패키지를 제공한다.
fs 패키지는 같은 기능을 동기(synchronous)비동기(asynchronous) 버전으로 나누어 제공한다. 예를 들어, 파일을 읽는 기능은 동기 버전인 readFileSync비동기 버전인 readFile로 제공한다.
먼저, src 디렉터리에 test.ts 파일을 만들고 다음처럼 작성한다. test.ts파일은 package.json 파일을 읽어 화면에 출력하는데, 이를 동기와 비동기 방식으로 구현한 예를 보여준다. 그리고 이번 장에서 다룰 Promise를 이용해 비동기 방식을 만든 다음 async/await 구문으로 구현한 예도 보여준다.

src/test.ts

import { readFileSync, readFile } from "fs";

// package.json 파일을 동기 방식으로 읽는 예
console.log("read package.json using synchronous api...");
const buffer: Buffer = readFileSync("./package.json");
console.log(buffer.toString());

// package.json 파일을 비동기 방식으로 읽는 예
readFile("./package.json", (error: Error, buffer: Buffer) => {
  console.log("read package.json using asynchronous api...");
  console.log(buffer.toSring());
});

// Promise와 async/await 구문을 사용한 예
const readFilePromise = (filename: string): Promise<string> => {
  new Promise<string>((resolve, reject) => {
    readFile(filename, (error: Error, buffer: Buffer) => {
      if (error) reject(error);
      else resolve(buffer.toString());
    });
  });
};

(async () => {
    const content = await readFilePromise('./package.json);
    console.log('read package.json using Promise and async/await...');
    console.log(content);
})()

실행결과

read package.json using synchronous api...

....

read package.json using asynchronous api...

...

read package.json using Promise and async/await...

...

운영체제가 제공하는 서비스를 API라고 한다. API는 타입스크립트와 같은 프로그래밍 언어의 함수 형태로 제공된다. 그런데 API 함수는 일반 함수와 달리 하드디스크에 저장된 파일을 읽는 등 실행 시 물리적인 시간이 소요된다.
따라서 파일 내용을 모두 읽을 때까지 프로그램의 동작을 잠시 멈추는 동기 방식의 API와 프로그램의 동작을 멈추지 않는 대신 결과를 콜백 함수로 얻게하는 비동기 방식의 API를 제공한다.
비동기 API의 콜백 함수를 특별히 비동기 콜백 함수라고 한다. 비동기 콜백 함수는 일반 함수와 달리 API의 물리적인 동작 결과를 수신하는 목적으로 만 사용된다.

readFileSync와 readFile API

웹 브라우저와 달리 node.js에는 운영체제 파일 시스템에 있는 파일을 읽을 수 있다.
node.js에서 파일 읽기는 readFileSync라는 이름의 API를 사용해서 구현한다.
readFileSync는 파일을 읽어서 Buffer라는 타입으로 전달해준다.

import {readFileSync} from 'fs'
readFileSync(path: string): Buffer

Buffer는 node.js가 제공하는 클래스로서 바이너리 데이터를 저장하는 기능을 수행한다. Vuffer의 데이터를 문자열로 만들려고 할 때 Buffer의 toString 메서드를 사용한다. node.js에서 API 이름이 xxxSync인 것은 모두 동기 방식으로 동작한다. 동기 방식 API는 작업이 종료될 때까지 프로그램을 일시적으로 멈추게 하는 특징이 있다.
다음 코드는 디렉터리의 package.json 파일을 readFileSync를 사용해 바이너리 데이터로 읽은 다음, Buffer의 toString 메서드를 사용해 화면에 출력한다. 그런데 다음 코드는 04행에서 package.json 파일을 읽는 동안 코드는 일시적으로 멈춘다.

src/readFileSync-test.ts

import { readFileSync } from "fs";
// package.json 파일의 바이너리 내용
const buffer: Buffer = readFileSync("./package.json");
const content: string = buffer.toString();
console.log(buffer); // package.json 파일의 텍스트 내용

node.js는 동기 버전인 readFileSync 이외에도 비동기 버전인 readFile도 제공한다.

import {readFile} from 'fs'
readFile(파일경로, 콜백함수: (error:Error, buffer:Buffer) => void)

readFile은 동기 버전과 달리 예외가 발생하면 이 에외를 콜백함수의 첫 번째 매개변수에 전달해준다. 다음 코드는 비동기 방식으로 동작하는 readFile의 사용 예이며, 실행 결과는 동기방식의 결과와 같다.

import { readFile, readFileSync } from "fs";

readFile("./package.json", (error: unknown, buffer: Buffer): void => {
  if (error) {
    throw error;
  } else {
    const content: string = buffer.toString();
    console.log(content);
  }
});

이 코드는 비동기 방식으로 동작하므로 03행을 실행할 때 멈추지 않고 바로 10행 이후를 실행한다. package.json파일을 읽은 결과는 readFile() 함수의 두 번째 매개변수인 콜백 함수로 전달된다.

단일 스레드와 비동기 API

자바스크립트는 단일 스레드(single-thread)로 동작하므로 될 수 있으면 readFileSync와 같은 동기 API를 사용하지 말아야 한다. 타입스크립트 또한 ES5 자바스크립트로 변환되어 실행되므로 자바스크립트와 마찬가지로 될 수 있으면 동기 API를 사용하지 말아야 한다.

💡 단일 스레드로 동작하는 자바스크립트


스레드는 CPU가 프로그램을 동작시키는 최소 단위이다. 운영체제에서 프로그램이 실행되고 있는 상태일 때를 프로세스라고 한다. 프로세스는 한 개의 주 스레드와 여러 개의 작업 스레드를 동작시킨다. 자바스크립트 코드는 항상 한 개의 작업 스레드에서 실행된다. 웹 브라우저나 node.js프로그램 자체는 다중 스레드로 동작하지만, 자바스크립트 코드는 한개의 작업 스레드, 즉 단일 스레드에서 동작한다.

타입스크립트 (혹은 자바스크립트) 코드에서 동기 API가 실행되면, 운영체제는 동기 API의 작업 결과를 함수의 반환값(return value)으로 돌려주어야 한다. 이 때문에 운영체제는 동기API가 실행된 코드를 일시적으로 멈춘 다음, 또 다른 스레드에서 실제 작업을 실행해 결과물을 얻으면 그때서야 비로소 잠시 멈췄던 동기 API를 실행하면서 결괏값을 반환한다.
동기 API의 이러한 동작 방식은 코드를 작성하기 쉽게 해주지만 프로그램의 반응성을 떨어뜨린다. 만일, 동기 API를 호출하는 자바스크립트 코드가 웹 서버에서 실행되면, 단일 스레드로 동작하는 자바스크립트의 물리적은 특징상 웹 서버는 동기API가 결괏값을 반환할 때까지 일시적으로 멈춘다. 그리고 웹 브라우저에서 이 웹 서버로 접속이 안되는 현상이 발생한다.
타입스크립트 (혹은 자바스크립트)는 이처럼 단일 스레드에서 동작하므로 코드를 작성할 때 항상 비동기 방식으로 동작하는 API를 사용해 프로그램의 반응성을 훼손하지 말아야 한다.

콜백 지옥

비동기 API를 사용하면 콜백 함수에서 다시 또 다른 비동기 API를 호출하는 코드를 만들때 코드가 매우 복잡해진다. 다음 코드는 package.json 파일을 읽은 다음 다시 tsconfig.json 파일을 읽는 것을 작성한 것으로, readFile('./package.json')의 콜백 함수 몸통에 또 다시 readFile('./tsconfig.json')을 호출하는 중첩 방식의 코드 구조를 보인다.

src/callback-hell.ts

import { readFile } from "fs";

readFile("./package.json", (err: Error, buffer: Buffer) => {
  if (err) throw err;
  else {
    const content: string = buffer.toString();
    console.log(content);
  }

  readFile("./tsconfig.json", (err: Error, buffer: Buffer) => {
    if (err) throw err;
    else {
      const content: string = buffer.toString();
      console.log(content);
    }
  });
});

이처럼 복잡한 형태로 얽힌 콜백 구조를 자바스크립트 분야에서는 콜백 지옥(callback hell)이라고 표현한다. Promise는 이런 콜백 지옥에 빠진 코드를 좀 더 다루기 쉬운 형태의 코드로 만들려는 목적으로 고안되었다.

07-2 Promise 이해하기

자바스크립트 언어에서 프로미스는 ES6 버전에서 정식 기능으로 채택되었다. 자바스크립트에서 프로미스는 Promise라는 이름의 클래스이다. 따라서 Promise 클래스를 사용하려면 일단 new 연산자를 적용해 프로미스 객체를 만들어야 한다. 그리고 new 연산자로 프로미스 객체를 만들 때 다음처럼 콜백 함수를 제공해야 한다.

const promise = new Promise(콜백 함수)

여기서 Promise의 콜백 함수는 resolve와 reject라는 두 개의 매개변수를 가진다

(resolve, reject) => {};

타입스크립트에서 Promise는 다음처럼 제네릭 클래스 형태로 사용한다.

const numPromise: Promise<number> = new Promise<number>(콜백 함수);
const strPromise: Promise<string> = new Promise<string>(콜백 함수);
const arrayPromise: Promise<number[]> = new Promise<number[]>(콜백 함수);

타입스크립트 Promise의 콜백 함수는 다음처럼 resolve와 reject 함수를 매개변수로 받는 형태이다.

new Promise<T>((resolve: (sucessValue: T) => void, reject: (any) => void) => {
  //코드 구현
});

resolve와 reject 함수

프로미스를 코드 없이 이해하기는 어려우므로 일단 프로미스를 사용하는 코드를 작성해 보자. 다음 코드는 앞 절에서 설명한 비동기 방식 API인 readFile을 호출하는 내용을 프로미스로 구현한 예이다.

src/readFilePromise.ts

import { readFile } from "fs";

export const readFilePromise = (filename: string): Promise<string> =>
  new Promise<string>(
    (resolve: (value: string) => void, reject: (error: any) => void) => {
      readFile(filename, (err: any, buffer: Buffer) => {
        if (err) reject(err);
        else resolve(buffer.toString());
      });
    }
  );

코드에서 08, 09행을 보면 에러가 발생할 때는 reject(err) 함수를 호출하고, 에러 없이 정상적으로 실행되었을 때는 파일 내용이 담긴 buffer를 이용해 resolve(buffer)함수를 호출한다. 이제 이렇게 구현한 readFilePromise 함수를 사용하는 쪽 코드를 살펴보자. 다음 코드는 readFilePromise 함수가 반환하는 Promise 타입 객체의 then, catch, finally 메서드를 메서드 체인 형태로 사용한다.
src/readFilePromise-test.ts

import { readFilePromise } from "./readFilePromise";

readFilePromise("./package.json")
  .then((content: string) => {
    console.log(content); // package.json 파일을 읽은 내용
    return readFilePromise("./tsconfig.json");
  })
  .then((content: string) => {
    console.log(content); // tsconfig.json 파일을 읽은 내용
    /* catch 쪽 콜백 함수에 'EISDIR: illegal operation on a directory, read' 라는 오류메세지 전달*/
    return readFilePromise(".");
  })
  .catch((err: any) => console.log("error", err.message))
  .finally(() => console.log("프로그램 종료"));

코드를 실행해 보면 readFilePromise에서 resolve 함수를 호출한 값은 then 메서드의 콜백함수쪽에 전달되고, reject 함수를 호출한 값은 catch 메서드의 콜백 함수 쪽에 전달되는 것을 볼 수 있다.
그리고 마지막에 '프로그램 종료'라는 출력을 볼 수 있는데, 이것은 finally 메서드가 호출되었다는 것을 의미한다. Promise 객체의 메서드 체인 코드에서 finally는 항상 마지막에 호출된다.

Promise.resolve 메서드

Promise 클래스는 resolve라는 클래스 메서드(정적 메서드)를 제공한다.
앞서 Promise객체를 생성할 때 resolve 함수를 호출했는데, Promise.resolve는 이를 클래스 메서드로 구현한 것이다. Promise.resolve(값) 형태로 호출하면 항상 이 은 then메서드에서 얻을수 있다.

src/Promise.resolve-test.ts

Promise.resolve(1).then((value) => console.log(value));

Promise.resolve("hello").then((value) => console.log(value));

Promise.resolve([1, 2, 3]).then((value) => console.log(value));

Promise.resolve({ name: "jack", age: 32 }).then((value) => console.log(value));

Promise.reject 메서드

Promise.reject(Error 타입 객체)를 호출하면 이 Error 타입 객체는 항상 catch 메서드의 콜백 함수에서 얻을 수 있다.

src/Promise.reject-test.ts

Promise.reject(new Error("에러 발생")).catch(
  (err: Error) => console.log("error", err.message) //error: 에러 발생
);

then-체인 프로미스 체이닝

Promise의 then 인스턴스 메서드를 호출할 때 사용한 콜백 함수는 값을 반환할 수 있다.
이 then에서 반환된 값은 또 다른 then 메서드를 호출해 값을 수신할 수 있다. 흥미롭게도 then 메서드는 반환된 값이 Promise 타입이면 이해를 해소(resolve)한 값을 반환한다. 만약 거절(reject)당한 값일 때는 catch 메서드에서 이 거절당한 값을 얻을 수 있다. Promise 객체에 then 메서드를 여러 번 호출하는 코드 형태를 then-체인이라고 한다. (책 이상... 보통 프로미스체이닝(Promise Chaining ))이라고 한다. 다음 코드는 then-체인 형태의 코드를 보여준다.

src/then-chain.js

Promise.resolve(1)
  .then((value: number) => {
    console.log(value); // 1
    return Promise.resolve(true);
  })
  .then((value: boolean) => {
    console.log(value); // true
    return [1, 2, 3];
  })
  .then((value: number[]) => {
    console.log(value); // [ 1, 2, 3 ]
    return { name: "Jack", age: 32 };
  })
  .then((value: { name: string; age: number }) => {
    console.log(value); // {name: Jack, age: 32}
  });

Promise.all 메서드

Array 클래스는 every라는 이름의 인스턴스 메서드를 제공한다. every 메서드는 배열의 모든 아이템이 어떤 조건을 만족하면 true를 반환한다. 다음 코드에서 isAllTrue 함수는 every메서드를 사용해 배열에 담긴 값이 모두 true인지 확인한다.
src/Array.every-test.ts

const isAllTrue: boolean = (values: boolean[]) =>
  values.every((value) => value == true);

console.log(isAllTrue([true, true, true])); // true
console.log(isAllTrue([false, true, true])); // false

Promise 클래스는 앞 every처럼 동작하는 all이라는 이름의 클래스 메서드를 제공한다.

all(프로미스 객체 배열: Promise[]): Promise<해소된 값들의 배열(혹은 any)>

Promise.all 메서드는 Promise 객체들을 배열 형태로 받아, 모든 객체를 대상으로 해소(resolve)된 값들의 배열로 만들어준다. Promise.all 메서드는 이런 내용 구성된 또 다른 Promise 객체를 반환하므로 해소된 값들의 배열은 then 메서드를 호출해서 얻어야 한다.
만약 배열에 담긴 Promise 객체 중 거절(reject) 객체가 발생하면 더 기다리지 않고 해당 거절 값을 담은 Promise.reject 객체를 반환한다. 이 거절 값은 catch 메서드를 통해 얻는다.

src/Promise.all-test.ts

const getAllResolvedResult = <T,>(promises: Promise<T>[]) =>
  Promise.all(promises);

getAllResolvedResult<any>([
  Promise.resolve(true),
  Promise.resolve("hello"),
]).then((result) => console.log(result)); // [ true, hello]

getAllResolvedResult<any>([
  Promise.reject(new Error("error")),
  Promise.resolve(1),
])
  .then((result) => console.log(result)) // 호출되지않음
  .catch((error) => console.log("error", error.message)); // error:error

Promise.race 메서드

Array 클래스는 배열의 내용 중 하나라도 조건을 만족하면 true를 반환하는 some이라는 인스턴스 메서드를 제공한다.

src/Array.some-test.ts

const isAnyTrue = (values: boolean[]) => values.some((value) => value == true);

console.log(isAnytrue([false, true, false])); // true
console.log(isAnytrue([false, false, false])); // false

Promise.race 클래스 메서드는 배열에 담긴 프로미스 객체 중 하나라도 해소(resolve)되면 이 값을 담은 Promise.resolve 객체를 반환한다. 만일, 거절 값이 가장 먼저 발생하면 Promise.reject객체를 반환한다.

race(프로미스 객체 배열:Promise[]):Promise<가장 먼저 해소된 객체의 값 타입>(혹은 Error)

다음 코드에서 02행은 true가 출력되는데,그 이유는 배열에 담긴 프로미스 객체들의 순서를 보면 Promise.resolve(true)가 먼저이기 때뭉니다. 이와 비슷하게 04행에서 배열의 순서를 보면 Promise.resolve(true)가 먼저이므로 06행은 호출되지 않는다. 그러나 08행은 Promise.reject()가 먼저이므로 09행 대신 10행이 실행된다.

src/Promise.race-test.ts

Promise.race([Promise.resolve(true), Promise.resolve("hello")]).then((value) =>
  console.log(value)
); // true

Promise.race([Promise.resolve(true), Promise.reject(new Error("hello"))])
  .then((value) => console.log(value)) // true
  .catch((error) => console.log(error.message)); // 호출되지 않는다.

Promise.race([Promise.reject(new Error("error")), Promise.resolve(true)])
  .then((value) => console.log(value)) // 호출되지 않는다.
  .catch((error) => console.log(error.message)); // error

Promise는 비동기 API 사용에서 나타나는 콜백 지옥 형태의 코드를 어느 정도 관리할 수 있는 코드 형태로 바꿔준다. 그런데 ESNext 자바스크립트와 타입스크립트는 Promise를 좀 더 쉬운 형태의 코드로 만들 수 있게 하는 async/await 구문을 제공한다.

07-3 async와 await 구문

다음 코드는 자바스크립트에서 async/await 구문을 사용한 예이다.

async/await 구문 예

const test = async () => {
  const value = await Promise.resolve(1);
  console.log(value); // 1
};

await 키워드

await 키워드는 피연산자(operand)의 값을 반환해준다. 피연산자가 Promise객체이면 then 메서드를 호출해 얻은 값을 반환해 준다.

let value = await Promise 객체 or 값

async 함수 수정자

await 키워드는 항상 async라는 이름의 함수 수정자가 있는 함수 몸통에서만 사용 할 수 있다.

const test1 = async() => { // 화살표 함수
  await Promise 객체 or 값
}

async function test2 () { // function 키워드 함수
  await Promise 객체 or 값
}

다음 코드는 화살표 함수 형태로 async함수를 구현한 예이다.
src/test1.ts

export const test1 = async () => {
  let value = await 1;
  console.log(value); // 1
  value = await Promise.resolve(1);
  console.log(value); // 1
};

다음 코드는 function 함수 형태로 async 함수를 구현한 예이다.
src/test2.ts

export async function test2() {
  let value = await "hello";
  console.log(value); // hello
  value = await Promise.resolve("hello");
  console.log(value); // hello
}

async는 함수이므로 04행과 05행에서 보듯 일반 함수처럼 호출할 수 있다.
src/await-test.ts

import { test1 } from "./test1";
import { test2 } from "./test2";

test1();
test2();

실행결과

1

hello

1

hello

실행결과가 흥미롭다. test1() 함수가 먼저 호출되었으므로 '1'이 두번 출력되고, 다음 test2() 함수가 호출되었으므로 'hello'가 나중에 두번 출력될 것으로 생각되지만, 두 함수가 동시에 실행된 것 처럼 보인다.

async 함수의 두 가지 성질

async 함수 수정자가 붙은 함수는 다음의 성질이 있다.

- 일반 함수처럼 사용할 수 있다.
- Promise 객체로 사용할 수 있다.

앞에서 본 await-test.ts는 async 함수를 일반 함수처럼 사용한 예이다. 반면에 다음 코드는 async 함수를 Promise 객체로 사용한 예이다.
src/async-as-promise.ts

import { test1 } from "./test1";
import { test2 } from "./test2";

test1().then(() => test2());

실행결과

1

1

hello

hello
앞 코드는 test1() 함수 호출이 해소(resolve)된 다음에 비로소 test2() 함수를 호출하므로 실행결과가 await-test.ts와 다르다.

async 함수가 반환하는 값의 의미

async 함수는 값을 반환할수 있다. 이 때 반환값은 Promise 형태로 변환되므로 다음처럼 then 메서드를 호출해 async 함수의 반환값을 얻어야 한다.
src/async-return.ts

const asyncReturn = async () => {
  return [1, 2, 3];
};

asyncReturn().then((value) => console.log(value)); //[1, 2, 3]

async 함수의 예외 처리

async 함수에서 다음처럼 예외가 발생하면 프로그램이 비정상으로 종료된다.

const asyncException = async () => {
  throw new Error("error");
};
asyncException(); // 예외 발생

예외가 발생해서 프로그램이 비정상으로 종료하는 상황을 막으려면, 다음처럼 asyncException을 단순히 asyncException() 혐태의 호출 방식이 아닌, asyncException()이 반환하는 프로미스 객체의 catch 메서드를 호출하는 형태로 코드를 작성해야 한다.

src/async-exception.ts

const asyncException = async () => {
  throw new Error("error");
};

asyncException().catch((err) => console.log("error:", err.message)); // error:error

await 구문에서 Promise.reject값이 발생하면 앞에서와 마찬가지로 프로그램이 비정상으로 종료된다.

const awaitReject = async () => {
  await Promise.reject(new Error("error"));
};
awaitReject();

이처럼 프로그램이 비정상으로 종료하는 코드는 앞에서 작성한 async-exception.ts처럼 구현하면 방지할 수 있다.

src/await-reject.ts

const awaitReject = async () => {
  await Promise.reject(new Error("error"));
};

awaitReject().catch((err) => console.log("error:", err.message)); //error:error

async 함수와 Promise.all

앞 절에서 비동기 API인 readFile을 프로미스로 만든 readFilePromise.ts를 async 함수에 적용해 보겠다.
다음 코드는 package.json과 tsconfig.json 두 파일의 내용을 async 함수와 Promise.all, readFilePromise를 사용해 화면에 출력한다.

src/async-readFilePromise-test.ts

import { readFilePromise } from "./readFilePromise"; // 07-2절의 readFilePromise.ts

const readFilesAll = async (filenames: string[]) => {
  return await Promise.all(
    filenames.map((filename) => readFilePromise(filename))
  );
};

readFilesAll(["./package.json", "./tsconfig.json"])
  .then(([packageJson, tsconfigJson]: string[]) => {
    console.log("<package.json> : ", packageJson);
    console.log("<tsconfig.json> : ", tsconfigJson);
  })
  .catch((err) => console.log("error:", err.message));

03행의 readFilesAll 함수는 매개변수 filenames에 담긴 string[] 타입 배열에 map 메서드를 적용해 Promise[]타입 객체로 전환한다. 그 다음 이를 다시 Promise.all 메서드를 사용해 단일 Promise 객체를 만든다. 그리고 이 결과로 만들어진 객체에 await 구문을 적용해 해소된 결과값을 반환한다.
09 ~ 15행은 readFilesAll 함수를 Promise 객체로 취급해 then과 catch 메서드를 then-체인 형태로 연결한다.
이로써 두 파일의 내용을 출력함과 동시에 예외가 발생하더라도 프로그램이 비정상으로 종료하지 않도록 한다.
지금까지 타입스크립트의 주요 문법과 기능을 소개하였다. 다음 장부터는 본격적으로 함수형 프로그래밍에 관해 살펴보자.

profile
느린 걸음도 먼 길을 갈 수 있다.

0개의 댓글