비동기 통신(1): Callback, Promise

최건우·2023년 1월 31일
0

자바스크립트

목록 보기
1/3

비동기 작업이란

출처: https://i.imgur.com/hh3Mawr.png

비동기 작업이란 어떤 한 작업이 완료될 때까지 기다리지 않고 다른 작업을 동시에 시작하는 것을 의미한다. 이렇게 함으로써 이전 작업이 진행되는 동안에도 다음 작업이 시작하거나 완료될 수 있다.

반대로 동기 작업이란, 어떤 한 작업이 완료된 후에야 다음 작업을 시작하는 것이다. 작성된 코드를 한 줄씩 순서대로 실행하는 자바스크립트에서는, 이전 함수가 값을 반환할 때까지 기다려야 함수 이후의 코드를 실행할 수 있다.

비동기 작업이 등장한 이유는, 동기 프로그래밍에 의해 작성된 코드가 진행되는 동안 브라우저에서는 다른 행동(클릭, 입력 등)을 할 수 없다는 불편이 야기되기 때문이다.

Callback

어떤 함수 doStep1()이 비동기적으로 작동하도록 만들어졌고, 만들고자 하는 기능이나 애플리케이션의 로직 상 doStep1()이 끝난 후 이어서doStep2() 함수를 실행해야 한다고 하자. 이때, doStep1() 함수에 doStep1(callback_function)와 같이 '함수'의 형태로 doStep2()를 전달한 뒤, doStep1(callback_function) 함수 내부에서 원하는 위치에서 doStep2()를 실행한다.

이와 같이, 연쇄되는 두 가지 작업이 각각 함수로 표현될 때, 앞선 작업을 담당하는 함수에 인자로서 전달되는 '이후 작업을 담당하는 함수'를 콜백(또는 콜백함수)라고 부른다.

그러나 세 가지 이상의 작업을 연쇄하여 진행하는 경우, 콜백 내부에서 콜백을 호출해야 하는 경우가 생긴다. 이렇게 되면 코드의 깊이가 깊어지기 떄문에 디버깅 및 에러 처리가 어려워지는 단점이 있다. 이를 "콜백 지옥" 이라고 부르기도 한다.

이 같은 이유 때문에, 최신 자바스크립트 비동기 API에서는 콜백 대신 Promise를 사용한다.

Promise

Promise는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타낸다. Promise를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있다.
다만 최종 결과를 반환하는 것이 아니고, 미래의 어떤 시점에 결과를 제공하겠다는 '약속'(Promise)을 반환한다.
Promise를 사용하면 Callback을 사용할 때처럼 코드가 깊어지는 것을 방지할 수 있다.

let promise = new Promise(function(resolve, reject) {
  // ...do something
});

Promise에는 executer(resolve, reject)라는 함수가 전달되는데, 이는 Promise가 생성될 때 즉각적으로 자동 실행되는 함수이다. 이 executer()안에서 수행하고자 하는 작업을 코드로 작성하면 된다.

작업이 끝나면, executer()는 성공/실패 여부에 따라 resolve 혹은 reject라는 자체 콜백을 호출한다(작업이 끝나면 반드시 둘 중 하나를 호출한다).

  • resolve(value): 일이 성공적으로 끝난 경우 그 결과를 나타내는 value와 함께 호출
  • reject(error) — 에러 발생 시 에러 객체를 나타내는 error와 함께 호출

이 콜백이 호출될 때, Promise 객체의 상태가 다음과 같이 변화한다.

Promise의 내부 프로퍼티 (1): status

Promise의 내부 프로퍼티인 status는 다음 중 하나의 상태를 가진다.

  • 대기(pending): 이행하지도, 거부하지도 않은 초기 상태.
  • 이행(fulfilled): 연산이 성공적으로 완료됨.
  • 거부(rejected): 연산이 실패함.

Promise의 status는 처음에는 pending이었다가 resolve가 호출되면 "fullfilled" 상태로, reject가 호출되면 "rejected"로 변한다.

참고로 이행 혹은 거부된 상태의 프라미스를 처리된(settled) 프라미스라고 부른다.

또한, 한 번 변경된 상태는 더 이상 변하지 않는다. 즉, resolve나 reject 중 하나의 인수가 한 번 호출되고 나면, 나머지 인수는 무시된다.

Promise의 내부 프로퍼티 (2): result

Promise의 내부 프로퍼티인 result는 다음과 같은 값을 가진다.

  • undefined: 이행하지도, 거부하지도 않은 초기 상태.
  • value: 연산이 성공적으로 완료됨.
  • error: 연산이 실패함.

처음에 undefined이었다가 resolve가 호출되면 "value" 로, reject가 호출되면 "error"로 변한다.

then, catch, finally

Promise 객체는 executer()와 결과나 에러를 받을 소비함수를 이어주는 역할을 한다. 소비함수는 then, catch, finally 메서드를 통해 등록(구독)된다.

then

.then은 다음과 같이 사용한다.

promise.then(
  function(result) { /* 결과(result)를 다룸. */ },
  function(error) { /* 에러(error)를 다룸. */ }
);
  • 첫 번째 인수 function(result)는 프라미스가 이행(fullfilled)되었을 때 실행되는 함수이고, 여기서 실행 결과를 받는다. 이때, 두 번째 인수는 무시되어 실행되지 않는다.

  • 두 번째 인수 function(error)는 프라미스가 거부되었을 때 실행되는 함수이고, 여기서 에러를 받는다. 이때, 첫 번째 인수는 무시되어 실행되지 않는다.

만약 작업이 성공적으로 처리된 경우만 다루고자 한다면, .then에 인수를 하나만 전달하면 된다.

let promise = new Promise(resolve => {
  setTimeout(() => resolve("완료!"), 1000);
});

promise.then(alert); // 1초 뒤 "완료!" 출력

작업이 실패(=에러 발생)한 경우만 다루고 싶다면, then을 사용하되 .then(null, errorHandlingFunction)같이 null을 첫 번째 인수로 전달하거나, catch를 사용하면 된다. .catch(errorHandlingFunction)

catch

let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});


promise.catch(alert); // 1초 뒤 "Error: 에러 발생!" 출력

.catch(f)promise.then(null, f)과 동일하게 작동함.

finally

  • 프라미스의 이행(fullfilled)과 거부(rejected) 여부에 상관없이 항상 실행되는 부분을 나타내는 메서드이다. 결과가 어떻든 간에 마무리를 해야 하는, 즉 '보편적 동작'을 해야 하는 상황(ex. 로딩 인디케이터를 멈추거나, 모달을 숨겨야 하는 경우)에 사용할 수 있다.

  • .then(fullfilled, rejected)와 기능이 비슷하지만, 그와 달리 finally에는 인수가 없다. 또한 finally에서는 프라미스가 이행되었는지, 거부되었는지 알 수 없다.

  • Promise의 결과를 '처리하는' 부분이라기보단, Promise의 결과를 '전달받아' 어떻게 할 지를 결정하는 부분으로 사용하면 된다.

  • finally 메서드는 반드시 맨 마지막에 사용되어야 하는 것은 아니다. finally를 먼저 쓰고, 결과나 에러를 then 혹은 catch 메서드를 사용한 다음 핸들러에 전달하여 사용해도 된다.

new Promise((resolve, reject) => {
  setTimeout(() => resolve("결과"), 2000)
})
  .finally(() => alert("프라미스가 준비되었습니다."))
  .then(result => alert(result)); // <-- .then에서 result를 다룰 수 있음

----------------------------------------

new Promise((resolve, reject) => {
  throw new Error("에러 발생!");
})
  .finally(() => alert("프라미스가 준비되었습니다."))
  .catch(err => alert(err)); // <-- .catch에서 에러 객체를 다룰 수 있음





출처

profile
부족한 경험을 채우기 위한 나만의 기록 공간

0개의 댓글