본문 바로가기

프로그래밍/JavaScript

JS - 비동기 작업의 이해

웹 애플리케이션을 만들다 보면 시간이 걸리는 작업들이 있다.

예를들면 서버 쪽 데이터가 필요할 때는 Ajax 기법을 이용하여 서버의 API를 호출함으로써 데이터를 수신한다.

이렇게 서버의 API를 사용해야 할 때는 네트워크 송수신 과정에서 시간이 걸리기 때문에 작업이 즉시 처리되는 것이 아니라, 응답을 받을 때까지 기다렸다가 전달받은 응답 데이터를 처리한다. 그리고 이 과정에서 해당하는 작업들을 비 동기적으로 처리하게 된다.

 

만약 작업을 동기적으로 처리한다면 요청이 끝날 때까지 기다리는 동안 중지 상태가 되기 때문에 다른 작업을 할 수가 없다. (한 작업 끝나면 그다음 작업, 또 한 작업 끝나면 그다음 작업 이런 식으로 수행)

 

하지만 비 동기적으로 작업을 처리한다면 중지 상태가 되지 않기 때문에 동시에 여러 가지 요청을 처리할 수도 있고, 기다리는 과정에서 다른 함수를 호출 할 수도 있다.

 

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

 

function printMe() {
  console.log("Hello World");
}

setTimeout(printMe, 3000);
console.log("대기중");


// -- result -- 
// 대기중
// Hello World

setTimeout이 사용되는 시점에서 코드가 3초 동안 멈추는 것이 아니라, 일단 코드가 위부터 아래까지 다 호출되고 3초 뒤에 printMe 함수가 호출된다.

 

비동기 작업을 할 때 가장 흔히 사용하는 방법은 콜백 함수를 사용하는 것이다.

(위 코드에서 printMe가 3초 뒤에 호출되도록 printMe 함수 자체를 setTimeout 함수의 인자로 전달해 주었는데, 이런 함수를 콜백 함수라고 부른다.)

 

콜백 함수

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

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

파라미터 값이 주어지면 1초뒤에 10을 더해서 반환하는 함수가있고, 해당 함수가 처리된 직후 어떠한 작업을 하고 싶다면 위 처럼 콜백 함수를 활용해서 작업 해야한다.

 

1초에 걸쳐서 10, 20, 30, 40과 같은 형태로 여러 번 순차적으로 처리하고 싶다면 콜백 함수를 중첩하여 구현할 수도있다.

 

function increase(number, callback) {
  setTimeout(() => {
    const resultNum = number + 10;
    if (callback) {
      callback(resultNum);
    }
  }, 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("작업 완료");
      });
    });
  });
});

이런식으로 콜백 안에 또 콜백을 넣어서 구현할 수 있는데, 너무 여러 번 중첩되서 코드의 가독성이 좋지않다.

이러한 형태의 코드를 "콜백 지옥" 이라고 부르며, 지양해야 할 형태의 코드이다.

 

Promise

Promise 는 콜백 지옥 같은 코드가 형성되지 않게 하는 방안으로 ES6에 도입된 기능이다.

위에서 Callback으로 구현한 코드를 Promise를 이용하여 구현하면 다음과 같다.

 

function increase(number) {
  const promise = new Promise((resolve, reject) => {
    // resolve는 성공, reject는 실패
    setTimeout(() => {
      const resultNum = number + 10;
      if (resultNum > 50) {
        // 50 보다 높으면 에러 발생
        const e = new Error("NumberTooBig");
        return reject(e);
      }
      resolve(resultNum);
    }, 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);
  })
  .catch((e) => {
    // 도중에 에러가 발생한다면 .catch를 통해 알 수 있음
    console.log(e);
  });

여러 작업을 연달아 처리한다고 해서 함수를 여러 번 감싸는 것이 아니라 .then을 사용하여 그 다음 작업을 설정하기 때문에 콜백 지옥이 형성되지 않는다.

 

 

async / await

async / await 는 Promise를 더욱 쉽게 사용할 수 있도록 해 주는 ES8 문법이다. 이 문법을 사용하려면 함수의 앞부분에 async 키워드를 추가하고, 해당 함수 내부에서 Promise 앞 부분에 await 를 사용한다. 이렇게 하면 Promise 가 끝날 때까지 기다리고, 결과 값을 특정 변수에 담을 수 있다.

 

function increase(number) {
  const promise = new Promise((resolve, reject) => {
    // resolve는 성공, reject는 실패
    setTimeout(() => {
      const resultNum = number + 10;
      if (resultNum > 50) {
        // 50 보다 높으면 에러 발생
        const e = new Error("NumberTooBig");
        return reject(e);
      }
      resolve(resultNum);
    }, 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);
  }
}

runTasks();

 

 

 

* velopert 님의 리액트를 다루는 기술에 나오는 내용을 참고하였습니다. *