들어가며

Promise를 사용해서 반복적인 callback 함수에서 벗어났지만 여전히 고질적인 문제가 발생한다. Promise도 결과적으로는 then, catch, finally를 사용하면서 callback이 최소 1회는 사용된다는 점이다.

함수를 분리하고 최대한 작성을 잘 하더라도, 아래와 같은 코드가 작성되는 경우가 생길 수 있다. 물론 동시에 호출하는 경우에는 Promise.all을 사용할 수 있겠지만 호출한 값을 사용해서 또 함수를 호출해야 하는 상황이 발생한다면 어쩔 수 없다.

/* 코드1 */
getUser().then((user) => {
  getPosts(user.id).then((posts) => {
      ...
  });
});

/* 코드2 */
getUser()
  .then((user) => {
    return getPosts(user);
  })
  .then((posts) => {
    console.log(posts);
  });
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

비동기 함수를 동기 함수로 사용하기 위해서, ES8에서 asyncawait이 나오게 된다. asyncawait은 쉽고 간단하게 사용할 수 있다.

async & await 시작하기

async는 함수를 선언할 때 function 앞에 사용하는데, 비동기 함수라고 표시한다고 보면 된다. async 키워드가 붙은 함수의 __proto__를 확인해보면 AsyncFunction이라는 객체라는 것을 알 수 있다.

async function example() {
    ...
}

const example = async () => {
    ...
};
1
2
3
4
5
6
7

awaitasync 함수 내부에서만 사용할 수 있으며, Promise가 처리될 때까지 이후의 작업을 멈춰주는 역할을 한다. resolve를 통해 반환한 값을 사용할 수 있다.

function timer(time) {
  return new Promise((res, rej) => {
    setTimeout(() => {
      console.log(time);
      res();
    }, time);
  });
}

/* 기존 방식 */
function example1() {
  timer(1000);
  console.log("done");
}

/* async & await 방식 */
async function example2() {
  await timer(1000);
  console.log("done");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

기존 방식은 done이라는 메시지가 콘솔에 출력된 이후에 비동기 함수 timer에 넘긴 값이 콘솔에 출력되는 구조였다. 하지만 asyncawait을 사용하게 되면 비동기 함수 timer가 완료될 때까지 이후의 작업은 중지되며, 작업이 완료되면 console.log("done")가 실행된다.

직접 example1example2 함수를 사용해서, 어떻게 작동하는지 확인해보자.

async & await 활용하기

실제 데이터를 가지고 온다고 가정하고, asyncawait을 활용해서 어떻게 코드를 작성하는지 알아보자. 유저와 게시물 정보를 가지고 오는 API가 마땅히 없기 때문에 setTimeout을 활용해서, API를 호출한 것처럼 비동기 함수를 만들었다.

function getUser() {
  const user = {
    id: "user1",
    name: "유저1",
    email: "user1@gmail.com",
    age: 20,
  };

  return new Promise((res, rej) => {
    setTimeout(() => {
      res(user);
    }, 100);
  });
}

function getPosts(user_id) {
  const posts = [
    {user_id: "user1", name: "게시물1", content: "본문1"},
    {user_id: "user1", name: "게시물2", content: "본문2"},
  ];

  return new Promise((res, rej) => {
    setTimeout(() => {
      res(posts);
    }, 100);
  });
}
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

getUser는 사용자의 정보를 가지고 오는 함수고, getPosts는 사용자가 작성한 게시물을 가지고 오는 함수다. 실제 DB에서 가지고 오는 데이터는 아니다보니, getPosts에서 user_id를 받고 있지만 사용하지 않도록 설계했다.

이제 asyncawait을 사용해서, 사용자와 게시물 정보를 가지고 올텐데, 앞서 설명했던 것처럼 간단하게 가지고 올 수 있다.

async function init() {
  const user = await getUser();
  const posts = await getPosts(user.id);
}
1
2
3
4

기존 callback, Promise 방식과 비교해보면 asyncawait을 사용하면 가독성과 유지보수 측면에서 좋다는 사실을 알 수 있다.

async & await 주의점

마냥 좋을 것만 같은 asyncawait에도 주의해야 하는 점들이 존재한다. 위 예제들은 이해를 돕기 위해 만든 함수를 사용하다보니, reject가 발생한다던지 반복문 사용이라던지 여러가지 상황이 발생하지 않았는데, 아래 예제들을 통해 주의해야하는 사항들을 알아보자.

에러 처리

실제 API 혹은 일부 함수를 사용하다 보면 예기치 못하게 오류가 발생할 수 있는데, asyncawait만 가지고는 에러를 제어할 수 없다.

다음은 에러를 제어하는 예제로 기존 init 코드에서 크게 달라지는 것은 없다. 코드를 작성할 때, 에러가 생길 수 있는 상황을 고려해서 작성하길 권장한다.

async function init() {
  try {
    const user = await getUser(true);
    const posts = await getPosts(user.id);
  } catch (e) {
    ...
  }
}
1
2
3
4
5
6
7
8

배열 처리

예를 들어, 화면에 이미지 여러 개를 드래그 & 드랍하고 그 이미지들을 화면에 보여줘야 하는 프로그램을 만들어야 한다고 생각해보자. 드래그 & 드랍을 다루는 페이지는 아니다 보니, 해당 코드에 대한 내용은 생략한다.

이렇게 드래그 & 드랍을 통해, 이미지를 배열로 저장했다고 가정하고, 이미지를 로드하는 함수를 Promise로 만들어보자. 대부분 loadImage와 비슷한 형식의 함수를 작성했을텐데, 여기까지는 크게 주의할 점은 없다.

/* 이미지 src에 사용할 주소 배열 */
const images = [
  ...
];

/* 이미지를 로드하는 함수 */
async function loadImage(source) {
  return new Promise((res, rej) => {
    const img = new Image();
    img.src = source;
    img.onload = () => res(img);
    img.onerror = (err) => rej(err);
  });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

우리는 배열을 forEach로 반복하면서, loadImage를 호출하고 다 완료될 경우, Done이라는 텍스트를 가진 div를 화면에 추가하고자 한다.

하지만 아래 코드를 실행할 경우, 어찌된 영문인지 모르겠지만 Done 텍스트가 화면 최상단에 보이고, 그 이후로 이미지가 보이는 것을 확인할 수 있다.

async function init() {
  try {
    images.forEach(async (source) => {
      const img = await loadImage(source);
      document.body.appendChild(img);
    });

    const text = document.createElement("div");
    text.innerText = "Done";
    document.body.appendChild(text);
  } catch (e) {
    console.log(e);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

우리가 생각한 것과 다르게 작동한 이유는 forEach함수는 asyncawait을 사용하더라도 처리를 기다려주지 않아서 생긴 문제다. 그럼 우리가 선택할 수 있는 다른 방안은 두 개로 볼 수 있다.

1. for문 사용

for문을 사용할 경우, 순차적으로 loadImage 함수가 실행되면서 원하는 화면을 볼 수 있다. 하지만 병렬로 처리되지 않아서, 용량이 큰 이미지가 존재하면 시간이 오래 걸리 수 있다.

async function init() {
  try {
    for (let i = 0; i < images.length; i++) {
      const img = await loadImage(images[i]);
      document.body.appendChild(img);
    }

    const text = document.createElement("div");
    text.innerText = "Done";
    document.body.appendChild(text);
  } catch (e) {
    console.log(e);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

2. Promise.all 사용

Promise.all는 비동기 함수들을 병렬로 처리 가능해서, for문을 사용했을 때의 문제점을 어느정도 보완할 수 있다. 순차적으로 호출이 필요한 API 같은 경우를 제외하고는 Promise.all을 사용해서, 병렬로 처리하는게 좋다고 본다.

async function init() {
  try {
    const loadImages = await Promise.all(
      images.map((source) => loadImage(source))
    );

    loadImages.forEach((img) => {
      document.body.appendChild(img);
    });

    const text = document.createElement("div");
    text.innerText = "Done";
    document.body.appendChild(text);
  } catch (e) {
    console.log(e);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

키워드를 사용하지 않는 부모 함수

예를 들어, example 함수를 또 다른 함수인 init 내부에서 사용할 때 await 키워드를 사용하지 않은 경우, example 함수의 내부 로직은 동기로 작동하더라도 init 함수의 내부는 동기로 작동하지 않는다.

function timer(time) {
  return new Promise((res, rej) => {
    setTimeout(() => {
      res(time);
    }, time);
  });
}

async function example() {
  console.log("example start");
  const result = await timer(1000);
  console.log(result);
  console.log("example end");
}

function init() {
  console.log("init start");
  example();
  console.log("init end");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

실제로 init 함수를 실행해보면 example 함수의 처리를 기다리지 않고, 콘솔이 출력되는 것을 볼 수 있다.

init 함수가 동기로 작동하기 위해서는 example 함수에 await을 추가해야 한다. 아래 코드를 실행해보면 example 함수가 처리될 때까지 기다리고, 그 이후에 "init end" 콘솔이 출력된다.

function timer(time) {
  return new Promise((res, rej) => {
    setTimeout(() => {
      res(time);
    }, time);
  });
}

async function example() {
  console.log("example start");
  const result = await timer(1000);
  console.log(result);
  console.log("example end");
}

async function init() {
  console.log("init start");
  await example();
  console.log("init end");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

항상 asyncawait을 사용할 때는 예상치 못 한 오류가 발생하지 않도록 주의하며 코드를 작성하길 바란다.

Last Updated:
Contributors: dailyuno