[Javascript] 비동기 함수 & 콜백 & Promise

Yeojin Choi·2022년 8월 2일
2

Javascript

목록 보기
9/11
post-thumbnail

1. 비동기 함수

비동기 함수를 호출하면 함수 내부의 비동기로 동작하는 코드가 완료되지 않았더라도 기다리지 않고 즉시 종료된다.

따라서 비동기로 동작하는 코드에서 처리 결과를 외부로 반환하거나 상위 스코프의 변수에 할당할 수 없다.

let a = 0;

setTimeout(() => {
  a = 100;
}, 0);

console.log(a); // 0

위 예제에서 setTimeout 비동기 함수 내부에서 0 으로 할당되었던 a 변수를 100 으로 변경하였다.

하지만 setTimeout 비동기 함수는 a 변수를 100으로 바꾸는 동작을 기다리지 않고 즉시 종료되고, 다음 코드를 실행하여 console.log(a) 가 실행되어 변경되지 않은 값인 0이 출력된다.

  1. 비동기 함수 setTimeout 호출
  2. 함수 코드 평가 과정에서 함수 실행 컨텍스트가 생성
  3. 실행 컨텍스트 스택(콜 스택) 에 push
  4. setTimeout 함수를 실행하면 Web APIsetTimeout 을 요청하고, 콜백 함수를 전달한다.
  5. setTimeout 함수가 종료되면 실행 컨텍스트가 콜 스택에서 pop (Web API 요청 이후 응답을 기다리지 않음!)
  6. console.log(a) 호출되어 0 출력

Web API 는 요청 받은 setTimeout 을 완료하고, 전달받은 콜백 함수를 태스크큐에 전달된다.
태스크큐에 저장되어 대기하다가 콜 스택이 비면 이벤트 루프에 의해 콜 스택으로 푸쉬되어 실행된다.

따라서 이미 console.log(a) 가 종료 되고 콜 스택이 빈 상태여야 비동기 함수의 콜백 함수가 실행되어 a 변수를 바꾸게 된다.

2. Callback 패턴

비동기 함수는 비동기 처리 결과를 외부로 전달하거나 상위 스코프의 변수에 할당할 수 없기 때문에, 비동기 함수의 처리 결과로 어떤 작업을 다시 수행하고자 할 때는 비동기 함수 내부에서 수행하여야한다.
비동기 처리 결과에 대해 후속 처리를 하기 위해 콜백함수를 전달할 수 있다.

2.1 콜백 헬

콜백 함수를 통해 비동기 처리 결과에 대한 후속 처리를 수행하는 비동기 함수가 비동기 처리 결과를 가지고 또다시 비동기 함수를 호출할 때 콜백 함수 호출이 중첩되어 복잡도가 높아지는 현상

step1(function (value1) {
    step2(function (value2) {
        step3(function (value3) {
            step4(function (value4) {
                step5(function (value5) {
                    step6(function (value6) {
                        // Do something with value6
                    });
                });
            });
        });
    });
});

2.2 에러 처리의 한계

try {
	setTimeout(()=> {throw New Error('Error');}, 1000);
} catch(e) {
	console.error(e); //캐치되지 않는다!!
}
  • setTimeout 호출 → setTimeout 함수 실행 컨텍스트 생성 → Call Stack push → 실행 -> 콜백함수의 종료를 기다리지 않고 종료, 콜스택에서 제거
  • 타이머가 만료되면 setTimeout 함수의 콜백 함수가 Task Queue 로 push → 콜스택이 비워졌을 때 Event Loop 에 의해 Call Stack 으로 push 되어 실행
  • Error 는 호출자 방향(Call stack 의 아랫방향) 으로 전파되는데, 콜백 함수의 실행 컨텍스트는 콜 스택의 가장 하부에 존재하게 되어 에러를 전파할 호출자가 존재하지 않는다.

콜백 헬과 에러 처리가 어렵다는 문제로 인해 ES6 에서 Promise 가 도입되었다.

3. Promise

3.1 Promise 객체

// Promise 는 `Promise` 생성자 함수를 통해 인스턴스화 한다.
const promise = new Promise(function (resolve, reject) { // Promise 생성자 함수는 인수로 콜백 함수(`executor`) 를 전달받는다. 
  
  // executor 는 `resolve` 와 `reject` 함수를 인수로 전달받으며 Promise 가 만들어 질 때 자동으로 실행된다.
  console.log('executer 수행');
  
  // 비동기 처리
  const value = 1;
  
  // 비동기 처리가 성공하면 비동기 처리 결과(value)를 resolve 함수에 인수로 전달하면서 호출 
  if (value > 1) {
    resolve(value);
  } else { // 실패하면 에러(reason)를 reject 함수에 인수로 전달하면서 호출
    reject(new Error('fail'));
  }
});

Promise 는 비동기 처리 상태 정보를 갖는다.

상태 정보의미상태 변경 조건
pending아직 수행 안됨프로미즈 생성된 직후 상태. 아직 resolve 또는 reject 호출 안됨
fulfilled비동기 처리 성공resolve 함수 호출
rejected비동기 처리 실패reject 함수 호출
settled비동기 처리 수행 완료resolve 또는 reject 호출 됨

fulfilled 된 promise
fulfilled 된 Promise 를 만들기 위해 resolve 함수를 호출하는 Promise 객체를 만들어 보자.

PromiseState 에 fulfilled, PromiseResult 에 success 가 담겨 있는 것을 볼 수 있다.

rejected 된 promise
rejected 된 Promise 를 만들기 위해 Error 를 인수로 받는 reject 함수를 호출하는 Promise 객체를 만들어 보자.


PromiseState 에 rejected, PromiseResult 에 Error 객체가 담겨 있는 것을 볼 수 있다.

즉, Promise 는 비동기 처리 상태처리 결과를 관리하는 객체다.

3.2 Promise 의 후속 처리 메서드

비동기 처리가 성공하면 성공한 값을 가지고 어떤 작업을 수행하거나, 비동기 처리가 실패해서 에러 처리를 하는 등의 처리를 위해서 Promise 는 후속 메서드 then, catch, finally 를 제공한다.
Promise 의 비동기 처리 상태가 변화하면 이러한 후속 메서드에 인수로 전달한 콜백 함수가 호출된다.

then
fulfilled 상태가 되면 호출될 콜백 함수와, rejected 상태가 되면 호출될 콜백 함수를 받을 수 있다.

new Promise((resolve, reject) => resolve(1)).then(
  value => {
    console.log(value);
  },
  error => {
    console.log(error);
  }
);

// 1 출력

new Promise((resolve, reject) => reject(new Error('error'))).then(
  value => {
    console.log(value);
  },
  error => {
    console.log(error);
  }
);

// Error: error 출력

catch
rejected 상태가 되면 호출될 콜백 함수만 받을 수 있다.

new Promise((resolve, reject) => reject(new Error('error')))
  .then(value => {
    console.log(value);
  })
  .catch(error => {
    console.error(error);
  });

// Error: error 출력

finally
settled 상태가 될 때 호출될 콜백 함수를 받을 수 있다.

3.3 프로미스 체이닝

후속 메서드 then, catch, finally 에 전달되는 콜백 함수는 항상 Promise 를 반환한다.
만약 콜백 함수가 프로미스가 아닌 값을 반환하면 암묵적으로 Promise 로 생성해서 반환한다.
콜백 함수가 반환한 값은 프로미스 체이닝을 통해 다음 .then 핸들러에 전달되고, Promise 로 생성되어 반환되기 때문에 후속 처리 메서드를 호출할 수 있다.

new Promise((resolve, reject) => resolve(1))
  .then(value => {
    console.log(value);
    return value + 1;
  })
  .then(value => {
    console.log(value);
    return value + 1;
  })
  .then(value => {
    console.log(value);
    return value + 1;
  })
  .catch(error => {
    console.error(error);
  });

// 1
// 2
// 3

만약 콜백 함수가 아무것도 반환하지 않는다면 다음 then 핸들러에서 전달 받을 수 없다.

new Promise((resolve, reject) => resolve(1))
  .then(value => {
    console.log(value);
  })
  .then(value => {
    console.log(value);
    return value + 1;
  })
  .then(value => {
    console.log(value);
    return value + 1;
  })
  .catch(error => {
    console.error(error);
  });

// 1
// undefined
// NaN

첫번째 then handler 에서 아무것도 반환하지 않았기 때문에 두번째 then handler 에서 undefined 가 출력되었다.
두번째 then handler 에서 undefined + 1 한 값을 반환했기 때문에 세번째 then handler 에서 NaN 이 출력된다.

3.4 Promise 의 에러 처리

then 의 두번째 콜백 함수와 catch 를 활용해 에러 처리를 할 수 있다.
두 가지 방식의 가장 큰 차이점은, then 의 두번째 콜백 함수는 첫번째 콜백 함수에서 발생한 에러를 캐치하지 못한다는 것이다.

new Promise((resolve, reject) => resolve('1')).then(
  value => {
    console.log(value);
    throw new Error('error');
  },
  error => {
    console.log(error);
  }
);

// 1

new Promise((resolve, reject) => resolve('1'))
  .then(value => {
    console.log(value);
    throw new Error('error');
  })
  .catch(error => {
    console.error(error);
  });

// 1
// Error: error

catch 메서드를 모든 then 메서드를 호출한 이후에 호출하면 then 메서드 내부에서 발생한 에러까지 모두 캐치할 수 있기 때문에 에러 처리는 catch 메서드에서 하는 것을 권장한다고 한다.

new Promise((resolve, reject) => resolve(1))
  .then(value => {
    console.log(value);
    return value + 1;
  })
  .then(value => {
    console.log(value);
    throw new Error('error');
  })
  .catch(error => {
    console.error(error);
  });


// 1
// 2
// Error: error

new Promise((resolve, reject) => resolve(1))
  .then(value => {
    console.log(value);
    throw new Error('error');
  })
  .then(value => { // 첫번째 then 에서 error 가 발생했기 때문에 resolve 되지 않아 두번째 then 이 호출되지 않는다.
    console.log(value); 
    return value + 1;
  })
  .catch(error => {
    console.error(error);
  });

// 1
// Error: error
profile
프론트가 좋아요

0개의 댓글