[JS/Node] 비동기 (1) - Callback, Promise, Async/Await

선정·2022년 5월 27일
0

Today I Learned

  • 비동기
    • 동기와 비동기
    • Callback
    • Promise
    • Async/Await

비동기

동기와 비동기

( 이미지 출처 : https://poiemaweb.com/js-async )

동기(synchronous)

JavaScript의 동기 처리란 ‘특정 코드의 실행이 완료될 때까지 기다리고 난 후 다음 코드를 수행하는 것’을 의미한다. 코드는 순차적으로 실행되며 어떤 작업이 수행 중이면 다음 작업은 대기하게 된다.

( 이미지 출처 : https://poiemaweb.com/js-async )

예를 들어 서버에서 데이터를 가져오는 작업을 동기적으로 처리하면, 서버로부터 데이터를 응답 받을 때까지 작업이 중단(Blocking)된다.


동기식 처리 모델 예시

function func1() {
  console.log('func1');
  func2();
}

function func2() {
  console.log('func2');
  func3();
}

function func3() {
  console.log('func3');
}

func1();
// 'func1'
// 'func2'
// 'func3'

비동기(asynchronous)

JavaScript의 비동기 처리는 ‘특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드들을 수행하는 것’을 의미한다. 코드가 종료되지 않은 상태라 하더라도 대기하지 않고 다음 코드를 실행한다.

( 이미지 출처 : https://poiemaweb.com/js-async )

예를 들어 서버에서 데이터를 가져오는 작업을 비동기적으로 처리하면, 서버에 데이터를 요청한 이후 서버로부터 데이터가 응답될 때까지 대기하지 않고(Non-Blocking) 즉시 다음 태스크를 수행한다. 이후 서버로부터 데이터가 응답되면 이벤트가 발생하고 이벤트 핸들러가 데이터를 가지고 수행할 태스크를 계속해 수행한다.


비동기식 처리 모델 예시

function func1() {
  console.log('func1');
  func2();
}

function func2() {
  setTimeout(function() {
    console.log('func2');
  }, 0);

  func3(); // setTimeout을 기다리지 않고 실행됨
}

function func3() {
  console.log('func3');
}

func1();
// 'func1'
// 'func3'
// 'func2'

( 이미지 출처 : https://poiemaweb.com/js-async )

setTimeout 메소드는 비동기 함수이다. 따라서 setTimeout을 실행한 뒤 delay만큼 대기하지 않고 즉시 다음 작업을 수행한다.

비동기적으로 처리되는 대표적인 작업

  • 대부분의 DOM 이벤트 핸들러
  • Timer 함수(setTimeout, setInterval)
  • Ajax 요청


Callback

Callback 함수를 통해 비동기 코드의 순서를 제어할 수 있지만 여러 개의 콜백함수가 중첩됨으로써 가독성이 떨어지는 이른바 Callback Hell 현상이 발생하기 쉽다.

Callback Hell

step1(function(value1) {
  step2(value1, function(value2) {
    step3(value2, function(value3) {
      step4(value3, function(value4) {
        step5(value4, function(value5) {
          // value2를 사용하는 작업
          console.log(value5)
        });
      });
    });
  });
});

전통적인 콜백 패턴의 단점

  • 콜백 헬(Callback Hell)로 인한 가독성 악화
  • 비동기 처리 중 발생한 에러를 처리하기 어려움
  • 여러 개의 비동기 처리를 한번에 처리하기에 한계가 있음


Promise

프로미스(Promise)는 ES6에서 비동기 처리를 위해 도입한 또 다른 패턴으로, 콜백 패턴의 단점을 보완한다.

Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타낸다. Promise는 Promise 생성자 함수를 통해 인스턴스화하므로, new 키워드를 통해 인스턴스 객체를 생성해야 한다. Promise 생성자 함수는 코드가 정상적으로 처리됐을 때 호출될 resolve 함수와 에러가 발생했을 때 호출될 reject 함수를 콜백 함수로 전달 받는다.

const promise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행한다.

  if (/* 비동기 작업 수행 성공 */) {
    resolve('성공');
  }
  else { /* 비동기 작업 수행 실패 */
    reject('실패');
  }
});


Promise 객체의 내부 프로퍼티

state : 비동기 처리가 성공(fulfilled)했는지 실패(rejected)하였는지 등에 관한 상태(state) 정보

  • pending (대기) : 비동기 처리가 아직 수행되지 않은 상태
  • fullfilled (이행) : 비동기 처리가 수행된 상태 (성공)
  • rejected (거부) : 비동기 처리가 수행된 상태 (실패)
  • settled (처리) : 비동기 처리가 수행된 상태 (성공 또는 실패)

result

  • undefined : 비동기 처리가 아직 수행되지 않은 상태일 때
  • value : resolve(value)가 호출 됐을 때
  • error : reject(error)가 호출 됐을 때


Promise의 후속 처리 메소드

Promise로 구현된 비동기 함수는 Promise 객체를 반환한다. 이는 Promise의 후속 처리 메소드인 .then, .catch, .finally 메서드를 사용해야 접근이 가능하다.


then
비동기 처리가 성공하면 resolve 함수를 호출하고 .then 메소드로 접근할 수 있다.
.then 안에서 리턴한 값이 Promise면 Promise의 내부 프로퍼티 result를 다음 .then 의 콜백 함수의 인자로 받아올 수 있다.

const promise = new Promise((resolve, reject) => {
	resolve("성공");
});

promise.then((value) => {
	console.log(value); // "성공"
})

catch
비동기 처리가 실패하면 reject 함수를 호출하고 .catch 메소드로 접근할 수 있다.

const promise = new Promise(function(resolve, reject) {
	reject(new Error("실패"))
});

promise.catch(error => {
	console.log(error); // Error: 실패
})

finally

비동기 처리의 성공/실패 여부와 상관없이 .finally 메소드를 통해 반환된 Promise에 접근할 수 있다.

const promise = new Promise(function(resolve, reject) {
  if (/* 비동기 작업 수행 성공 */) {
    resolve('성공');
  }
  else { /* 비동기 작업 수행 실패 */
    reject('실패');
  }
});

promise
.then(value => {
	console.log(value); // "성공"
})
.catch(error => {
	console.log(error);
})
.finally(() => {
	console.log("성공이든 실패든 작동!"); // "성공이든 실패든 작동!"
})


Promise chaining

비동기 작업을 순차적으로 진행할 때, Promise chaining이 필요하다. Promise의 후속 처리 메소드들이 Promise를 리턴하기 때문에 체이닝이 가능하다.

const promise = new Promise(function(resolve, reject) {
  if (true) {
    resolve('성공');
  }
  else {
    reject('실패');
  }
});

promise
  .then((value) => {
    console.log(value);
    return '성공1';
  })
  .then((value) => {
    console.log(value);
    return '성공2';
  })
  .then((value) => {
    console.log(value);
    return '성공3';
  })
  .catch((error) => {
    console.log(error);
    return '실패';
  })
  .finally(() => {
    console.log('성공이든 실패든 작동!');
  });

// 성공
// 성공1
// 성공2
// 성공이든 실패든 작동!


Promise.all()

Promise.all()은 여러 개의 비동기 작업을 동시에 처리하고 싶을 때 사용한다.

const promiseOne = () => new Promise((resolve, reject) => setTimeout(() => resolve('1초'), 1000));
const promiseTwo = () => new Promise((resolve, reject) => setTimeout(() => resolve('2초'), 2000));
const promiseThree = () => new Promise((resolve, reject) => setTimeout(() => resolve('3초'), 3000));

Promise.all([promiseOne(), promiseTwo(), promiseThree()])
  .then((value) => console.log(value))
  // ['1초', '2초', '3초']
  .catch((err) => console.log(err));

위 코드를 Promise chaining으로 구현하면 코드들이 순차적으로 작업이 수행돼 총 6초가 걸리지만, Promise.all()을 사용하면 3초 내에 모든 작업을 수행할 수 있다.
또한 Promise.all()은 인자로 받는 배열에 있는 Promise 중 하나라도 에러가 발생하게 되면 그 즉시 실행을 중단하고 에러를 반환한다.



Async/Await

async/await는 ES8에서 도입된 문법으로, 해당 문법을 사용하면 Promise를 좀 더 편하고 간결하게 사용하고 가독성을 향상시킬 수 있다. try..catch를 사용해 에러 핸들링도 할 수 있다.

async

  • 함수 앞에 사용해 Promise를 반환한다.
  • Promise가 아닌 값을 반환해도 프로미스로 감싸서 반환한다.

await

  • Promise를 앞에 사용해 Promise가 처리될 때까지 기다렸다가 결과를 반환한다.
  • async 함수 내에서만 사용할 수 있다.
  • promise.then의 역할을 수행한다.

async function f() {

  try {
    const response = await fetch('http://유효하지-않은-주소');
    const user = await response.json();
  } catch(err) {
    alert(err);
  }
}

f();

참고

profile
starter

0개의 댓글