Skip to content

JavaScript | 비동기 (콜백 함수, Promise, async/await)

프로그래밍에서 비동기는 기초적이며 중요한 작업입니다.

그래서 이번에는 비동기 작업에 관한 글을 작성해보았습니다. 동기와 비동기에 대해 알아보고 비동기 작업에 해당되는 콜백 함수, Promise, async/await에 대해 알아볼 것입니다.

1. 동기 & 비동기

1-1. 동기 (synchronous)

동기(synchronous)의 사전적 의미는 '동시에 일어난다'는 뜻입니다. 요청과 그 결과가 동시에 일어난다는 약속이며, 바로 요청하면 시간이 얼마 걸리던지 요청한 자리에서 결과가 주어져야 합니다.

  • 요청과 결과가 한 자리에서 동시에 일어납니다.

1-2. 비동기(Asynchronous)

비동기(Asynchronous)는 반대로, '동시에 일어나지 않는다'라는 뜻입니다. 요청과 결과가 동시에 일어나지 않을 것이라는 약속입니다.

  • 요청한 그 자리에서 결과가 주어지지 않습니다.

1-3. 장단점

  • 동기 방식

  • 장점

    설계가 매우 간단하고 직관적이다.

  • 단점 결과가 주어질 때까지 어무것도 못하고 대기해야 한다.

  • 비동기 방식

  • 장점

    (결과가 주어지는데 시간이 걸리더라도) 그 시간 동안 다른 작업이 가능해서 자원의 효율적인 사용이 가능하다.

  • 단점 동기보다 복잡하다.

1-4. 서버 API 사용 시, 처리

서버 API 사용 시, 처리할 때 둘은 어떤 차이가 있을까요?

  • 동기적으로 한다면,

요청이 끝날 때까지 기다리는 동안 중지 상태가 되며 다른 작업을 할 수 없게 됩니다. 즉, 끝나야 다른 작업이 가능하게 됩니다.

  • 비동기적으로 한다면, 웹 애플리케이션이 멈추지 않습니다. 동시에 여러 요청 처리가 가능하며, 기다리는 과정에서 다른 함수를 호출할 수 있습니다.

이렇게 서버 API를 호출할 때 외에도 작업을 비동기적으로 처리할 때가 있습니다. 바로 setTimeout함수를 사용하여 특정 작업을 예약할 때입니다.

1-5. setTimeout(비동기적 처리)

특정 작업 예약

예시 코드를 살펴보겠습니다.

아래 코드는 3초 후에 printMe 함수를 호출합니다.

  • 실행 코드
function printMe() {
  console.log("Hello World");
}

setTimeout(printMe, 3000);
console.log("대기중...");
  • 결과
"대기중...";
"Hello World";

예시 코드는 setTimeout 이 사용되는 시점에서 코드가 3초간 멈추는 게 아닌, 일단 코드가 위~아래까지 다 호출되고 3초 뒤에 지정해준 printMe가 호출되고 있는 것입니다.

이처럼 자바스크립트에서 비동기 작업을 할 때 가장 흔히 사용하는 방법이 콜백 함수를 사용하는 것입니다. 여기서 콜백 함수는 setTimeout함수의 인자로 전달된 'printMe'함수입니다.

그럼 콜백 함수가 정확히 무엇인지 알아보도록 하겠습니다.

2. 콜백 함수

다른 함수에 매개변수로 넘겨준 함수

2-1. 개념

콜백 함수는 위에 적어둔 것처럼, 다른 함수에 매개 변수로 넘겨준 함수입니다.

매개변수로 넘겨받은 함수는 일단 넘겨 받고 때가 되면 나중에 호출(called back)한다는 것이 개념입니다.

2-2. 콜백 지옥

  • 개념

코드 형태가 콜백 안에 콜백을 넣어 구현되어 있습니다.

이것이 많아질 경우, 여러 번 중첩이 되고 그럼 코드 가독성이 나빠지게 됩니다.

  • 예시 파라미터 값이 주어지면 1초 뒤, 10 더해서 반환하는 함수를 만들었습니다. 이 함수를 처리한 후에 어떤 작업을 더 하고 싶다면, 콜백 함수를 활용합니다.
function increase(number, callback) {
  setTimeout(() => {
    const result = number + 10;
    if (callback) {
      callback(result);
    }
  }, 1000);
}

increase(0, (result) => {
  console.log(result);
});

// 결과
// 10

그런데 이를 여러 번 순차적으로 처리하고 싶다면, 콜백 함수가 중첩되어 다음과 같은 코드가 나오게 됩니다.

function increase(number, callback) {
  setTimeout(() => {
    const result = number + 10;
    if (callback) {
      callback(result);
    }
  }, 1000);
}

console.log("작업 시작");
increase(0, (result) => {
  console.log(result);
  increase(result, (result) => {
    console.log(result);
    increase(result, (result) => {
      console.log(result);
      increase(result, (result) => {
        console.log(result);
        console.log("작업 완료");
      });
    });
  });
});

// 실행 결과
/*
    '작업 시작'

    10

    20

    30

    40
    '작업 완료'
    */

왜 콜백 지옥이라고 부르게 되었는지 이해가 되시나요?

이런 코드를 형성하지 않게 하는 방안이 있습니다. 바로 Promise입니다.

3. Promise

여러 작업을 연달아 처리할 때, 함수를 여러 번 감싸지 않고 .then 을 사용하여 다음 작업을 설정하게 됩니다. (그럼 콜백 지옥이 형성되지 않게 됩니다.)

3-1. 예시 코드

  • 실행 코드
function increase(number){
  const promise = new Promise((resolve, reject)=>{
    // resolve는 성공, reject는 실패
    setTimeout(() => {
      const result = number + 10;
      if(result > 50) {
        // 50보다 높으면 에러 발생
        const e = new Error('numberTooBig');
        return reject(e);
      }
      resolve(result); // number 값에 + 10 후 성공 처리
    }, 1000);
  })
  return promise
}

increase(0)
.then(number => {
  // Promise에서 resolve된 값은 .then을 통해 받아 올 수 있음
  console.log(number)
  return increase(number) // Promise를 리턴하면
})
.then(number => {
  // 또, .then으로 처리 가능
  console.log(number)
  return increase(number)
})
.then(number => {
  console.log(number)
  return increase(number)
})
.then(number => {
  console.log(number)
  return increase(number)
})
.then(number => {
  console.log(number)
  return increase(number)
})
.catch(e => {
  // 도중에 에러가 발생한다면, .catch를 통해 알 수 있음
  console.log(e)
})
  • 결과
    Promise { <pending> }
    10
    20
    30
    40
    50
    Error: 'numberTooBig'

가독성이 훨씬 좋아진 것 같습니다.

하지만, Promise를 더 쉽게 사용할 수 있도록 해주는 문법이 있습니다. 바로 async/await 입니다.

4. async/await

Promise를 더 쉽게 사용할 수 있도록 해주는 ES2017(ES8) 문법

4-1. 사용법

함수 앞 부분에 async 추가하고 함수 내부의 Promise 앞 부분에 await 사용을 사용합니다.

4-2. 동작 원리

Promise 끝날 때까지 기다리고 결과 값을 특정 변수에 담을 수 있습니다.

4-3. 예시 코드

  • 실행 코드
function increase(number) {
  const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
      const result = number + 10;
      if (result > 50) {
        const e = new Error("numberTooBig");
        return reject(e);
      }
      resolve(result);
    }, 1000);
  });
  return promise;
}

async function runTasks() {
  try {
    let result = await increase(0);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result);
    console.log(result);
    result = await increase(result);
    console.log(result);
  } catch (e) {
    console.log(e);
  }
}
  • 결과
10;

20;

30;

40;

50;

Error: "numberTooBig";

코드가 더 간결해졌습니다.


참고


작성자: 정선미

작성일: 2021-11-14