JS | Promise

Autumn·2021년 3월 5일
2

JavaScript

목록 보기
16/18
post-thumbnail

몇 번째 보는지 모르겠는 드림코딩 엘리 - Promise편

아직도 익숙하지 않은 promise와 비동기 ! 간단한 예제 코드를 써보며 동작을 이해해보는 중이다.

Promise 내부의 코드는 곧바로 실행된다.

예제 1

const cb = () => console.log('promise');
const pp = new Promise((resolve, reject) => {
  cb();
})
console.log('프로미스 아님');

// 실행 결과
// promise
// 프로미스 아님

프로미스 내부의 코드는 곧바로 실행된다. new Promise를 하는 순간, 콜백함수가 실행이 되기 때문에 'promise' 가 먼저 찍힌다.


예제 2

const cb = () => console.log('promise');
const pp = new Promise((resolve, reject) => {
  resolve(cb);
})

pp.then(cb);
console.log('프로미스 아님');

// 실행 결과
// 프로미스 아님
// promise

??? 🤷🏻‍♂️
Promise 안에 비동기 로직이 없기 때문에 then의 콜백함수가 바로 실행되어 promise, 프로미스 아님 순서로 출력될 것 같았는데 아니었다. 이게 뭘까?

이 부분을 이해하기 위해서는 이벤트루프, 그 중에서도 microtask queue의 동작에 대한 이해가 필요하다. 일단 남은 예제를 더 살펴보고 이벤트루프에 대해 알아보자.


예제 3

let a = 1;
const cb = () => console.log('promise');
const pp = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('3초 뒤에 오는 응답 (비동기실행)');
    a = 3;
    console.log('비동기 a', a);
  }, 3000);
  if(a === 3) resolve(cb);
})

pp.then(cb);
console.log('프로미스 아님');

// 실행 결과
// 프로미스 아님
// 3초 뒤에 오는 응답 (비동기실행)
// 비동기 a 3

프로미스 내부에는 보통 시간이 좀 걸리는 비동기 로직이 들어있으니까, setTimeout을 이용하여 비동기를 표현해보자.
위 코드는 3초 뒤에 a의 값을 3으로 재할당한다. 프로미스 안의 if문을 보면 setTimeout 바깥에 있다. 즉, a 값이 바뀌기 전에 실행되므로 resolve가 실행되지 않는다. 따라서 then에 전달되는 값도 없으며 '프로미스 아님'이 먼저 찍히고, 3초 뒤에 setTimeout 콜백함수의 내용들이 찍힌다.
resolve를 이렇게 쓰면 안되겠구나. resolve는 비동기 로직의 마지막에 써줘야겠다는 생각이 든다.


예제 4

let a = 1;
const cb = () => console.log('promise');
const pp = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('3초 뒤에 오는 응답 (비동기실행)');
    a = 3;
    console.log('비동기 a', a);
    if(a === 3) resolve(cb);
  }, 3000);
})

pp.then(cb);
console.log('프로미스 아님');

// 실행 결과
// 프로미스 아님
// 3초 뒤에 오는 응답 (비동기실행)
// 비동기 a 3
// promise

코드를 살짝 바꿔 a가 3이면 resolve 함수로 cb를 then에 전달해주도록 했다. 그랬더니 cb가 정상적으로 전달되어 then을 통해 실행되어서 'promise' 가 출력되었다. 아하!! 그럼 살짝쿵 reject도 써보자.


예제 5

let a = 1;
const cb = () => console.log('promise');
const pp = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('3초 뒤에 오는 응답 (비동기실행)');
    a = 2;
    console.log('비동기 a', a);
    if(a === 3) resolve(cb);
    if(a !== 3) reject('실패');
  }, 3000);
})

pp
.then(cb)
.catch(fail => console.log(fail));
console.log('프로미스 아님');

// 실행 결과
// 프로미스 아님
// 3초 뒤에 오는 응답 (비동기실행)
// 비동기 a 2
// 실패

실패 상황을 가정하기 위해 비동기 로직에서 a의 값을 3이 아닌 2로 재할당했다. a가 3이면 성공, 2이면 실패인 상황! 예상대로 '실패' 라는 문자열이 .catch의 인자 fail로 전달되어 마지막에 '실패' 라고 출력되었다.


Event Loop의 전반적인 동작 이해하기

  • 프로미스가 다 수행이 되고 나서 resolve가 되면, then에 등록된 콜백함수가 마이크로태스크큐에 들어간다. (참고: mutation observer (<-웹 API 중 하나임) 라는 것의 콜백함수도 마이크로태스크큐에 들어가는데, 저 놈은 아직 접해본 적이 없어서 패스)

  • 포인트 !!!

  1. 이벤트루프는 매우 빠른 속도로 (한바퀴 도는 데에 1ms도 안걸림) 빙글빙글 돌면서 태스크큐, 콜스택, 마이크로태스크큐, 렌더를 돈다.

  2. 콜스택에서는 콜스택이 모두 비워질 때까지 머물러있는다. 콜스택이 모두 비워지면 이벤트 루프가 다시 돌아간다.

  3. 렌더 쪽에는 갈 수도 있고 안 갈 수도 있는데, 그 이유는 우리 눈에 움직임이 자연스럽게 보이기 위한 최소 조건이 1초에 60프레임, 즉 16.7ms의 속도인데 이벤트루프는 훨씬 빠르게 돌기 때문

  4. 마이크로태스크큐에 then의 콜백함수나 mutation observer의 콜백함수와 같은 아이템이 있으면 이벤트루프가 머무르며 실행한다. 이 때, 마이크로태스크큐가 모두 비워질 때까지 계속 마이크로태스크큐에 머물러 있으며, 마지막 콜백함수가 콜스택에 올라가서 실행되다가 실행이 끝나기 전에 또다른 아이템이 마이크로태스크큐에 들어온다면 마이크로태스크큐를 떠나지 않고 계속해서 실행을 한다.
    즉 간단히 말하면, 마이크로태스크큐에 등록되어 있는 콜백함수가 실행되어 콜스택에 올라갔다가 사라진(함수가 종료된) 그 시점에 마이크로태스크큐가 비어있어야 이벤트루프는 비로소 다시 움직이기 시작한다.

  5. 태스크큐의 함수들은 콜스택이 비어있을 때 하나씩만 가져가서 실행하게 된다. 하나 꺼내서 콜스택에 올려서 실행하고, 이벤트루프가 다시 돌아가는 것. 그 사이에 렌더를 할 때가 되었으면 렌더를 새로 하고, 마이크로태스크큐에 새로운 함수가 들어와있으면 마이크로태스크큐가 완전히 비워질 때까지 머무르며 실행하고.. 이런 식이다.


그럼 아까 봤던 예제 2를 다시 한 번 보자.

const cb = () => console.log('promise');
const pp = new Promise((resolve, reject) => {
  resolve(cb);
})

pp.then(cb);
console.log('프로미스 아님');

// 실행 결과
// 프로미스 아님
// promise

이벤트 루프를 중심으로 동작 과정을 적어보면 다음과 같다.

  • 전역 컨텍스트 실행 (콜스택에 올라감)
  • Promise 실행 (이건 콜스택에 올라가는 건지 잘 모르겠음)
    ** Promise 안에 시간이 좀 걸리는 로직이 들어있다면 동시에 아래 코드를 실행할텐데 이 예제에서는 시간이 오래 걸리는 로직이 없다.
  • resolve되었으므로 cb가 마이크로태스크큐에 들어간다.
  • '프로미스 아님' 출력
  • 전역 컨텍스트 종료 (콜스택이 비었음)
  • 콜스택이 비었으므로, 이벤트루프가 돌다가 마이크로태스크큐에 들어있는 cb 발견, 콜스택에 올려줌
  • cb 실행
  • 'promise' 출력

우왕~~ 이런 거였구나!


추가로...

예제 코드들에서 then안에 콜백함수를 좀 더 일반적인 코드로 바꿀 수 없을까 궁금해서 예제 4를 다음과 같이 바꿔보았다.

let a = 1;
const cb = () => console.log('promise');
const pp = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('3초 뒤에 오는 응답 (비동기실행)');
    a = 3;
    console.log('비동기 a', a);
    if(a === 3) resolve(cb);
  }, 3000);
})

pp.then(func); // ⭐️ 이 부분!
console.log('프로미스 아님');

then 안에 cb라고 되어있던 것을 func라고 바꿨다. 그랬더니 오류가 났다.

ReferenceError: func is not defined

그래서 다시 수정!

let a = 1;
const cb = () => console.log('promise');
const pp = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('3초 뒤에 오는 응답 (비동기실행)');
    a = 3;
    console.log('비동기 a', a);
    if(a === 3) resolve(cb);
  }, 3000);
})

pp.then(func => func()); // ⭐️ 이 부분!
console.log('프로미스 아님');

이렇게 바꿨더니 똑같이 잘 동작한다. cb만 쓰는 것과 func만 쓰는 것은 무슨 차이일까?
그건 바로바로바로... then 안에 cb라고 쓰면, resolve에게서 전달되는 값이 뭐든지 상관없이 내가 정의해놓은 cb 함수가 실행되는 것이었다. 😅 resolve가 전달해주는 값을 인자로 받아서 실행해야하기 때문에 func => func() 와 같이 써주는 게 맞는 표현이다.

그래서 사실 위 예제들에서는 resolve되면 전달해주는 cb가 then에서 쓰이질 않고 있었던 것이다. 여기까지 읽으셨다면, then안의 콜백함수를 func => func() 로 바꿔서 이해해보시길 바란다 !


끝!


아직 학습 단계여서 잘못 알고 있는 부분이 있을 수 있으니, 댓글로 꼭 알려주시면 감사하겠습니다!! 🙏🏻

profile
한 발짝씩 나아가는 중 〰 🍁 / 자잘한 기록은 아래 🏠 아이콘에 연결된 노션 페이지에 남기고 있어요 😎

6개의 댓글

comment-user-thumbnail
2021년 3월 7일

어텀!! 일요일은 쉬어야죠!! 대단해요!!👏

잘 보고 갑니당~

1개의 답글

어텀~~ 이벤트 루프까지 같이 설명해주셔서 훨씬 이해가 잘 가는 것 같아요ㅎㅎㅎ 많은 도움 받고 갑니다!!! 🍂🍂👍

1개의 답글
comment-user-thumbnail
2021년 3월 7일

머싓다 독서실 횽님

1개의 답글