Skip to content

14. 비동기(Asynchronous) 처리


1. 키워드

  • 비동기(Asynchronous) 처리
  • 콜백 함수(Callback Function)
  • 프로미스(Promise)
  • async/await
  • Promise.all()Promise.race()


2. 비동기 처리 방법

1) 기본적인 형태의 비동기 처리 방법

  • 자바스크립트에서 비동기 처리를 할 수 있는 방법은 여러 가지가 있다.
  • 다음의 예제는 가장 기본적인 형태의 비동기 처리 방법이다.


  • 먼저, 실행하고자 하는 함수는 다음과 같다.


function work() {
  const start = Date.now();
  for (let i = 0; i < 1000000000; i++) {}
  const end = Date.now();

  console.log(`${end - start} ms`);
}

console.log("작업 시작");

work();

console.log("다음 작업");
작업 시작
469 ms
다음 작업


  • 위의 예제를 실행하면 "작업 시작" 출력이 실행된다.
  • 그런 다음 work() 함수의 실행 및 종료가 이루어진다.
  • 마지막으로 "다음 작업" 출력이 실행된다.


  • 위의 예제를 다음의 예제처럼 비동기 처리로 수정할 수 있다.
  • work() 함수 안에 setTimeout() 함수를 추가하고, setTimeout() 함수 안에 실행하고자 하는 코드를 입력한다.


function work() {
  // setTimeout(Function, start_after_ms) -> Background
  setTimeout(() => {
    const start = Date.now();
    for (let i = 0; i < 1000000000; i++) {}
    const end = Date.now();

    console.log(`${end - start} ms`);
  }, 0);
}

console.log("작업 시작");

work();

console.log("다음 작업");
작업 시작
다음 작업
470 ms


  • 위의 예제를 실행하면 "작업 시작" 출력이 실행된다.
  • 그런 다음 백그라운드에서 work() 함수의 실행이 되고, 포그라운드에서 "다음 작업" 출력이 실행된다.
  • 마지막으로 백그라운드의 work() 함수가 종료된다.


  • 위의 예제에서는 setTimeout(() => { ... }, ...);을 이용하여 화살표 함수 표현(Arrow Function Expression)을 사용했지만 다음과 같이 익명 함수 표현(Anonymous Function Expression)인 setTimeout(function () { ... }, ...);을 사용해도 그 결과는 같다.


function work() {
  // setTimeout(Function, start_after_ms) -> Background
  setTimeout(function () {
    const start = Date.now();
    for (let i = 0; i < 1000000000; i++) {}
    const end = Date.now();

    console.log(`${end - start} ms`);
  }, 0);
}

console.log("작업 시작");

work();

console.log("다음 작업");
작업 시작
다음 작업
470 ms


  • 즉, 화살표 함수 () => {}와 익명 함수 function () {}은 같은 효과를 내는 것이다.


  • 비동기 처리가 종료된 후 또 다른 작업을 실행할 경우 다음의 예제처럼 콜백 함수를 사용하면 된다.
  • work() 함수의 파라미터로 callback(다른 이름이어도 가능함)이라는 이름의 콜백 함수를 지정한다.
  • callback() 함수 내에 파라미터가 존재하는 경우 end - start와 같이 인자를 전달한다.
  • work 함수 밖에서 콜백 함수에 해당하는 함수(do_after_work())를 정의한다.
  • work() 함수를 호출할 때 정의한 do_after_work() 함수를 인자로 전달한다.


function work(callback) {
  // setTimeout(Function, start_after_ms) -> Background
  setTimeout(() => {
    const start = Date.now();
    for (let i = 0; i < 1000000000; i++) {}
    const end = Date.now();

    console.log(`${end - start} ms`);

    callback(end - start);
  }, 0);
}

console.log("작업 시작");

// Use Function
function do_after_work(ms) {
  console.log("작업 끝!");
  console.log(`${ms} ms 걸렸음~`);
}
work(do_after_work);

console.log("다음 작업");
작업 시작
다음 작업
470 ms
작업 끝!
470 ms 걸렸음~


  • 위의 예제를 실행하면 "작업 시작" 출력이 실행된다.
  • 그런 다음 백그라운드에서 work() 함수의 실행이 되고, 포그라운드에서 "다음 작업" 출력이 실행된다.
  • 마지막으로 백그라운드의 work() 함수가 종료된 후, 콜백 함수로 지정한 do_after_work() 함수가 실행되고 종료된다.


  • 위의 예제에서는 콜백 함수를 선언하고 정의하여 인자로 전달했지만, 다음의 예제처럼 익명 함수를 사용할 수도 있다.


function work(callback) {
  // setTimeout(Function, start_after_ms) -> Background
  setTimeout(() => {
    const start = Date.now();
    for (let i = 0; i < 1000000000; i++) {}
    const end = Date.now();

    console.log(`${end - start} ms`);

    callback(end - start);
  }, 0);
}

console.log("작업 시작");

// Use Anonymous Function
work((ms) => {
  console.log("작업 끝!");
  console.log(`${ms} ms 걸렸음~`);
});

console.log("다음 작업");
작업 시작
다음 작업
470 ms
작업 끝!
470 ms 걸렸음~


  • 위의 예제들과 같이 setTimeout() 함수 및 콜백 함수를 사용하여 비동기 처리를 할 수 있다.
  • 하지만 다음의 예제처럼 이어지는 비동기 처리 작업이 많아질 경우 코드 작성 및 관리가 어렵다는 단점이 있다.


function increaseAndPrint(n, callback) {
  setTimeout(() => {
    const increased = n + 1;
    console.log(increased);

    if (callback) {
      callback(increased);
    }
  }, 1000);
}

increaseAndPrint(0, (n) => {
  increaseAndPrint(n, (n) => {
    increaseAndPrint(n, (n) => {
      increaseAndPrint(n, (n) => {
        increaseAndPrint(n, (n) => {
          console.log("작업 끝!");
        });
      });
    });
  });
});
1
2
3
4
5
작업 끝!


2) 프로미스

  • 위의 예제처럼 비동기 처리 작업이 많아질 경우 Promise 객체를 이용하면 된다.
  • Promise 객체는 객체 생성자를 통해 생성하고, 전달할 함수의 파라미터로는 resolve, reject가 있다.


  • 먼저, 다음의 예제는 기본적인 Promise 객체를 생성하는 방법이다.


const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    // If Failure
    reject();

    // If Success
    resolve();
  }, ms);
});

promise.then().catch();


  • new 키워드를 통해 Promise 객체 생성자를 호출하고, 이때 비동기로 실행하고자 하는 함수(현재는 익명 함수 사용)를 인자로 전달한다.
  • 비동기로 실행하고자 하는 함수의 파라미터로는 resolvereject를 지정하고, 함수 내부에는 setTimeout() 함수를 지정한다.
  • resolve는 성공 시 실행할 함수이며 resolve() 함수 내의 인자를 반환하는 역할을 한다.
  • 그리고, reject는 실패 시 실행할 함수이며 reject() 함수 내의 인자를 반환하는 역할을 한다.
  • 프로미스가 성공하면 resolve() 함수가 실행되고, .then() 메서드가 실행된다.
  • 만약 프로미스가 실패하여 reject() 함수가 실행되면, .catch() 메서드가 실행된다.


  • 다음의 예제처럼 프로미스를 사용하여 콜백 함수만 사용하는 경우와 같은 동작을 하도록 할 수 있다.
  • 프로미스의 속성에서 .then() 메서드 내의 또 다른 프로미스가 존재하는 경우 연속적으로 비동기 처리를 할 수 있다.


function increasedAndPrint(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const value = n + 1;

      if (value === 6) {
        const done = "작업 끝!";

        reject(done); // -> .catch()
        return;
      }

      console.log(value);
      resolve(value); // -> .then()
    }, 1000);
  });
}

// n이 0인 백그라운드 실행 -> value는 1 -> resolve(1)
// -> 초기 프로미스 생성 및 반환 -> 자신의 .then()에 value 1을 전달
increasedAndPrint(0) // <- 프로미스 객체
  .then((n) => {
    // 파라미터 n을 전달받는 익명 함수 정의 -> n이 1인 백그라운드 실행
    // -> value는 2 -> resolve(2)
    // -> 새로운 프로미스 생성 및 반환 -> 초기 프로미스에 value가 2인 현재 프로미스 반환
    // -> 초기 프로미스의 .then()에 2 전달
    return increasedAndPrint(n);
  })
  .then((n) => {
    // 파라미터 n을 전달받는 익명 함수 정의 -> n이 2인 백그라운드 실행
    // -> value는 3 -> resolve(3)
    // -> 새로운 프로미스 생성 및 반환 -> 초기 프로미스에 value가 3인 현재 프로미스 반환
    // -> 초기 프로미스의 .then()에 3 전달
    return increasedAndPrint(n);
  })
  .then((n) => {
    // 파라미터 n을 전달받는 익명 함수 정의 -> n이 3인 백그라운드 실행
    // -> value는 4 -> resolve(4)
    // -> 새로운 프로미스 생성 및 반환 -> 초기 프로미스에 value가 4인 현재 프로미스 반환
    // -> 초기 프로미스의 .then()에 4 전달
    return increasedAndPrint(n);
  })
  .then((n) => {
    // 파라미터 n을 전달받는 익명 함수 정의 -> n이 4인 백그라운드 실행
    // -> value는 5 -> resolve(5)
    // -> 새로운 프로미스 생성 및 반환 -> 초기 프로미스에 value가 5인 현재 프로미스 반환
    // -> 초기 프로미스의 .then()에 5 전달
    return increasedAndPrint(n);
  })
  .then((n) => {
    // 파라미터 n을 전달받는 익명 함수 정의 -> n이 5인 백그라운드 실행
    // -> value는 6 -> reject("작업 끝!")
    // -> 새로운 프로미스 생성 및 반환 -> 초기 프로미스에 msg가 "작업 끝!"인 현재 프로미스 반환
    // -> 초기 프로미스의 .catch()에 "작업 끝!" 전달
    return increasedAndPrint(n);
  })
  .catch((msg) => {
    // 파라미터 msg를 전달받는 익명 함수 정의 -> msg 출력
    console.log(msg);
  });
1
2
3
4
5
작업 끝!


  • 만약 초기 프로미스의 .then() 메서드 안에서 return increasedAndPrint(n);이 아닌 increasedAndPrint(n);만을 사용하는 경우 다음의 예제처럼 작성해야 한다.


function increasedAndPrint(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const value = n + 1;

      if (value === 6) {
        const done = "작업 끝!";

        reject(done);
        return;
      }

      console.log(value);
      resolve(value);
    }, 1000);
  });
}

increasedAndPrint(0).then((n) => {
  increasedAndPrint(n).then((n) => {
    increasedAndPrint(n).then((n) => {
      increasedAndPrint(n).then((n) => {
        increasedAndPrint(n).then((n) => {
          increasedAndPrint(n).catch((msg) => {
            console.log(msg);
          });
        });
      });
    });
  });
});


  • 하지만, 위의 예제의 경우 프로미스를 사용하는 장점이 하나도 없어진다.


  • 또한, 위의 예제처럼 .then() 메서드가 반복되는 경우 다음의 예제처럼 작성할 수 있다.


function increasedAndPrint(n) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const value = n + 1;

      if (value === 6) {
        const done = "작업 끝!";

        reject(done);
        return;
      }

      console.log(value);
      resolve(value);
    }, 1000);
  });
}

increasedAndPrint(0)
  .then(increasedAndPrint)
  .then(increasedAndPrint)
  .then(increasedAndPrint)
  .then(increasedAndPrint)
  .then(increasedAndPrint)
  .catch((msg) => {
    console.log(msg);
  });
1
2
3
4
5
작업 끝!


  • 위의 예제들처럼 프로미스를 사용하는 것이 좋지만, 분기 작업을 지정하기가 어렵다.
  • 그리고 reject() 함수로 인한 에러 발생 시 어떤 비동기 처리 작업에서 에러가 발생했는지 알기 어렵다.
  • 또한, 특정 값을 전달하면서 비동기 처리하기 까다롭다.


3) async/await

  • async/await는 위의 예제들과 비슷하게 프로미스 객체를 이용한다.
  • async/await는 첫 번째로 비동기 처리가 끝나기 전에 다음 작업을 할 것인지, 두 번째로 비동기 처리가 끝날 때까지 기다린 후 다음 작업을 할 것인지 구분해서 사용한다.


  • 먼저 다음의 예제는 비동기 처리가 끝나기 전에 다음 작업을 하는 방법이다.
  • 프로미스를 생성하는 함수 sleep()을 정의하고, sleep() 함수를 비동기로 처리할 async 함수 process()를 정의한다.
  • 각 프로미스는 sleep() 함수가 실행된 후 1초 후 생성된다.


function sleep(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let wait_time = ms;

      if (wait_time === 5000) {
        const done = "작업 끝!";

        reject(done);
        return;
      }

      console.log(`${ms}ms slept in background`);
      wait_time += 1000;
      resolve(wait_time);
    }, 1000);
  });
}

async function process() {
  console.log("프로세스 시작!");

  sleep(1000)
    .then((ms) => {
      return sleep(ms);
    })
    .then((ms) => {
      return sleep(ms);
    })
    .then((ms) => {
      return sleep(ms);
    })
    .then((ms) => {
      return sleep(ms);
    })
    .catch((msg) => {
      console.log(msg);
    });

  console.log("프로세스 끝!");

  return "process() Done.";
}

process().then((msg) => {
  console.log(msg);
});
프로세스 시작!
프로세스 끝!
process() Done.
1000ms slept in background
2000ms slept in background
3000ms slept in background
4000ms slept in background
작업 끝!


  • 다음의 예제는 비동기 처리가 끝날 때까지 기다린 후 다음 작업을 하는 방법이다.
  • 위의 예제에서 sleep(1000) 앞에 await 키워드를 추가하면 된다.


function sleep(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let wait_time = ms;

      if (wait_time === 5000) {
        const done = "작업 끝!";

        reject(done);
        return;
      }

      console.log(`${ms}ms slept in background`);
      wait_time += 1000;
      resolve(wait_time);
    }, 1000);
  });
}

async function process() {
  console.log("프로세스 시작!");

  await sleep(1000)
    .then((ms) => {
      return sleep(ms);
    })
    .then((ms) => {
      return sleep(ms);
    })
    .then((ms) => {
      return sleep(ms);
    })
    .then((ms) => {
      return sleep(ms);
    })
    .catch((msg) => {
      console.log(msg);
    });

  console.log("프로세스 끝!");

  return "process() Done.";
}

process().then((msg) => {
  console.log(msg);
});
프로세스 시작!
1000ms slept in background
2000ms slept in background
3000ms slept in background
4000ms slept in background
작업 끝!
프로세스 끝!
process() Done.


  • 하지만, 위의 예제들의 경우 async/await를 사용하는 장점이 하나도 없어진다.


  • 먼저, 다음의 예제는 비동기 처리가 끝나기 전에 다음 작업을 하는 방법이다.
  • 위의 예제와 달리 수정한 곳이 많지만 결과적으로 async/await의 장점이 보인다.


function sleep(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (ms === 5000) {
        const done = "작업 끝!";
        reject(done);
      }

      console.log(`${ms}ms slept in background`);
      resolve(ms);
    }, ms);
  });
}

async function wrapper(ms) {
  await sleep(ms).catch(function (error) {
    console.log(error);
  });
}

async function process() {
  console.log("프로세스 시작!");

  try {
    Promise.all([
      wrapper(1000),
      wrapper(2000),
      wrapper(3000),
      wrapper(4000),
      wrapper(5000),
    ]);
  } catch (error) {
    console.log(error);
  }
  console.log("프로세스 끝!");

  return "process() Done.";
}

process().then((msg) => {
  console.log(msg);
});
프로세스 시작!
프로세스 끝!
process() Done.
1000ms slept in background
2000ms slept in background
3000ms slept in background
4000ms slept in background
5000ms slept in background
작업 끝!


  • 다음의 예제는 비동기 처리가 끝날 때까지 기다린 후 다음 작업을 하는 방법이다.
  • 위의 예제에서 Promise.all() 앞에 await 키워드를 추가하면 된다.


function sleep(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (ms === 5000) {
        const done = "작업 끝!";
        reject(done);
      }

      console.log(`${ms}ms slept in background`);
      resolve(ms);
    }, ms);
  });
}

async function wrapper(ms) {
  await sleep(ms).catch(function (error) {
    console.log(error);
  });
}

async function process() {
  console.log("프로세스 시작!");

  try {
    await Promise.all([
      wrapper(1000),
      wrapper(2000),
      wrapper(3000),
      wrapper(4000),
      wrapper(5000),
    ]);
  } catch (error) {
    console.log(error);
  }
  console.log("프로세스 끝!");

  return "process() Done.";
}

process().then((msg) => {
  console.log(msg);
});
프로세스 시작!
1000ms slept in background
2000ms slept in background
3000ms slept in background
4000ms slept in background
5000ms slept in background
작업 끝!
프로세스 끝!
process() Done.


  • async/await를 사용할 때 Promise.all()Promise.race()를 사용할 수 있다.
  • 먼저, Promise.all()은 모든 비동기 작업을 기다리며, 그 중 하나라도 에러가 발생하면 try/catch 문으로 에러를 잡을 수 있도록 한다.
  • 그리고 Promise.race()는 가장 빨리 처리된 비동기 작업에서 에러가 발생하면 나머지 비동기 작업을 중단시키면서 try/catch 문으로 에러를 잡을 수 있도록 한다.


  • 또한 다음과 같이 await 사용 시 반환되는 값을 사용할 수 있다.


function sleep(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (ms === 6000) {
        const done = "작업 끝!";
        reject(done);
      }

      console.log(`${ms}ms slept in background`);
      resolve(ms);
    }, ms);
  });
}

async function wrapper(ms) {
  const result = await sleep(ms).catch(function (error) {
    console.log(error);
  });

  return result;
}

async function process() {
  console.log("프로세스 시작!");

  try {
    const [first, second, third, fourth, fifth] = await Promise.all([
      wrapper(1000),
      wrapper(2000),
      wrapper(3000),
      wrapper(4000),
      wrapper(5000),
    ]);

    console.log(`First is ${first}`);
    console.log(`Second is ${second}`);
    console.log(`Third is ${third}`);
    console.log(`Fourth is ${fourth}`);
    console.log(`Fifth is ${fifth}`);
  } catch (error) {
    console.log(error);
  } finally {
    console.log("프로세스 끝!");

    return "process() Done.";
  }
}

process().then((msg) => {
  console.log(msg);
});
프로세스 시작!
1000ms slept in background
2000ms slept in background
3000ms slept in background
4000ms slept in background
5000ms slept in background
First is 1000
Second is 2000
Third is 3000
Fourth is 4000
Fifth is 5000
프로세스 끝!
process() Done.