넷플릭스 "지금 우리 학교는"으로 이해하는 Javascript Promise (+동기, 비동기)

자기개발자·2022년 3월 11일
1

다음과 같은 상태에서 읽는 것을 추천합니다.

  • Javascript Promise에 대해 접하기는 했지만, 개념이 안 잡힌 상태
  • 동기, 비동기에 대해서 들어보기는 했지만 헷갈리는 상태

Promise, Promise.all, async, await 등 문법에 대해서 자세하게 다루지는 않습니다.
문법에 대한거는 나중에 따로 찾아보시고 일단 감만 캐치하시는것을 추천합니다.
(그래도 promise의 기본원리, then, resolve,reject, pending, fullfill등 기본원리에 대한 내용은 곧 추가 예정)

무언가에 대해서 비유를 들 수 있다는 것은 적어도 그 개념에 대한 큰 그림, 숲은 🌳 볼 수 있게 되었다는 것이다.

물론 이는 "정확하게 이해했는가" 와는 또 다른 영역이다..


숲을 보게 되었다고 해서 나무 하나 하나의 모양, 잎의 구체적인 색깔, 나무의 종 까지 알게 된 것은 결코 아니다.

하지만 깊게, 정확하게 이해하려면, 일단 제일 먼저 앞에 있는 낮은 허들부터 넘어야 한다.


이 허들을 넘고나면, 속력이 붙고, 감이 생겨서 다음 허들, 또 그 다음 허들을 넘는 거는 점점 쉬워진다.

우선, Promise라는 단어를 순수하게 떠올려보자.

Promise는 약속이다.


우리는 일상생활에서 왜 약속이라는 것을 할까?

약속을 안했을 때 치뤄야 할 대가가 있기 때문일것이다.


가장 연결시키기 쉬운 예는, 지우학에서 친구들이 교실을 나가기 전에 순서를 정해서 나가는 식이다.

좀비를 앞장 서서 싸워줄 친구가 최전방에 서주고 (ex. 대수),
상대적으로 달리기가 느린 친구 뒤쳐져서 좀비한테 당하지 않도록 또 다른 친구가 맨 뒤에 서주는 식이다 (ex. 수혁).

만약, 힘이 센 대수가 먼저 앞장 서서 밀고 나가주지 않고 뒤쳐진다던가

수혁이가 맨 뒤에서 커버쳐주는게 아니라 여자 아이들을 제치고 가버린다던가

하면 아래와 같은 사단이 빈번하게 났을 것이다.


약속을 만들어봅시다 (틀린 방법)

물론 멤버가 더 많지만 가장 메인 캐릭터인 청산, 수혁, 온조, 남라로 예시를 들어 코드를 옮겨보겠다.

남라는 다리가 다쳐서 제일 속도가 느리다는 시나리오를 생각해보자.

도망갈 때 가장 느린 남라의 속도에 맞춰주기 위해서, 느린 순으로 ‘남라 → 온조 → 수혁 → 청산’ 순으로 코드를 짜보자”가 가장 첫번째로 떠오른 접근방식일 것이다. (코드는 위에서 아래로 실행되니까)

//달리기 빠른 순서
const 청산_달리기_속도 = 1000;
const 수혁_달리기_속도 = 2000;
const 온조_달리기_속도 = 3000;
const 남라_달리기_속도 = 4000;

//느린 사람 먼저 가는 작전
setTimeout(() => {
  console.log("남라 안전 지역 도착");
}, 남라_달리기_속도);

setTimeout(() => {
  console.log("온조 안전 지역 도착");
}, 온조_달리기_속도);

setTimeout(() => {
  console.log("수혁 안전 지역 도착");
}, 수혁_달리기_속도);

setTimeout(() => {
  console.log("청산 안전 지역 도착");
}, 청산_달리기_속도);

//  예상 결과
// "남라 안전 지역 도착"
// "온조 안전 지역 도착"
// "수혁 안전 지역 도착"
// "청산 안전 지역 도착"

첫 4번째 라인까지는 특정 지점까지 달리는데까지 걸리는 시간을 정해준 것이다. 숫자가 작을 수록 빠르다.

하지만 이렇게 되면 결과는 어떻게 될까?

예상한 것과 반대로, 다들 남라는 뒷전이 되었는지, 달리기가 빠른 순서대로 청산 -> 수혁 → 온조 → 남라 순으로 도착한다.

이러한 예상 못한 일이 일어나는 이유는 바로 Javascript의 비동기 방식 때문이다.

자바스크립트는 기본적으로 극중 이나연식의 마인드를 탑재하고 이해하자.

기본적으로는 위에서 아래로 순차적으로 코드가 실행되지만,
시간이 오래 걸리는 코드가 있다면 “일단 본인부터 살고 보자”마인드로
그 코드를 건너뛰고 본인 먼저 냅다 실행한다.


그래서 달리기 가장 느린 순서대로 위에서부터 코드를 짰음에도, 달리기가 가장 빠른 순서대로 도착한 것이다.

자, 그럼 이런 사단이 나지 않게 위해서는 이제 제대로 된 ‘약속'을 만들어야 한다.

정리하자면, Promise 늦게 실행되는 코드를 내팽개쳐두고 먼저 실행되는 것 (비동기 방식) 이 아니라, 순차적으로 실행될 수 있도록 (동기 방식) 그 안에서 통용되는 약속 (Promise)을 정하는 것이다.


약속 만들기 (Sequential: 연속적)

const 청산_달리기_속도 = 1000;
const 수혁_달리기_속도 = 2000;
const 온조_달리기_속도 = 3000;
const 남라_달리기_속도 = 4000;

//도망 계획

const 도망계획 = new Promise((성공, 실패) => {
  setTimeout(() => {
    console.log("남라 안전 지역 도착")
    성공();
  }, 남라_달리기_속도);
})
  .then((res) => {
    return new Promise((성공, 실패) => { // (*)
      setTimeout(() => {
        console.log("온조 안전 지역 도착")
        성공();
      }, 온조_달리기_속도);
  })
})
.then((res) => {
  return new Promise((성공, 실패) => { // (*)
    setTimeout(() => {
      console.log("수혁 안전 지역 도착")
      성공();
    }, 수혁_달리기_속도);
})
})
.then((res) => {
  return new Promise((성공, 실패) => { // (*)
    setTimeout(() => {
      console.log("청산 안전 지역 도착")
      성공();
    }, 청산_달리기_속도);
})
})

이제 예상한 결과가 나왔다.

근데 현재는 코드가 너무 가독성이 떨어진다.

Promise를 return해주는 코드를 함수화하고, async, await으로 더 간단하게 만들어보자.

async-await은 비동기 동작(Promise)의 상태가 완료될 때까지 기다린 후, 다음 코드를 순차적으로 읽어 나가며 실행하기 때문에 비동기 동작들의 순서를 보장할 수 있다.

간략하게 바꾼 코드

  1. '도망'을 함수화하고 재사용해서 가독성을 높인 코드
// 
const 도망 = (이름, 달리기_속도) => {
  const promiseData = new Promise((성공, 실패) => {
    setTimeout(() => {
      성공(이름 + " 도망 완료");
    }, 달리기_속도);
  });
  return promiseData;
};

const 도망계획 = new Promise((성공, 실패) => {
  도망("남라", 남라_달리기_속도)
    .then((res) => {
      console.log(res);
      return 도망("온조", 온조_달리기_속도);
    })
    .then((res) => {
      console.log(res);
      return 도망("수혁", 수혁_달리기_속도);
    })
    .then((res) => {
      console.log(res);
      return 도망("청산", 청산_달리기_속도);
    })
    .then((res) => {
      console.log(res);
    });
});
  1. Async, Await을 사용한 코드
// async await를 사용해서 가독성을 높인 코드

const 도망 = (이름, 달리기_속도) => {
  return new Promise((성공, 실패) => {
    setTimeout(() => {
      console.log(이름 + " 도망 완료");
      성공();
    }, 달리기_속도);
  });
};

async function 도망계획() {
  await 도망("남라", 남라_달리기_속도);
  await 도망("온조", 온조_달리기_속도);
  await 도망("수혁", 수혁_달리기_속도);
  await 도망("청산", 청산_달리기_속도);
  console.log("모두 대피 완료");
}

도망계획();


약속 만들기 (Concurrent: 병행적)

근데 여기서 한 가지 의문점이 든다.

‘달리기가 느린 남라가 가장 먼저 도착’이라는 전제만 충족된다면, 나머지 셋 중 누가 먼저 도착하는지가 그렇게 중요할까?

위 코드에는 온조는 남라가 도착해야 출발할 수 있고, 수혁이는 온조가 도착해야 출발할 수 있고,
마찬가지로 청산이도 수혁이가 도착해야 출발할 수 있는 ‘의존성’이 작용되고 있다.

만약 남라만 제일 먼저 도착하는것이 보장된다면, 굳이 느린순서 (남라→온조→수혁→청산)대로 도망치는것은 뭔가 비효율적이다.

그래서 남라를 제외한 나머지 셋 사이의 의존성은 제거해서 이 셋은 순서 상관없이 처리되도록 한다.

이때 사용하는게 Promise.all인데, 이는 여러 비동기 동작(프라미스)을 하나로 묶어 하나의 Promise처럼 관리하게 해준다. 다만 이 안에서 순서는 보장되지 않는다.

async function 도망계획() {
  await 도망("남라", 남라_달리기_속도);

  const 온조도망 = 도망("온조", 온조_달리기_속도);
  const 수혁도망 = 도망("수혁", 수혁_달리기_속도);
  const 청산도망 = 도망("청산", 청산_달리기_속도);

  const 나머지셋도망 = [온조도망, 수혁도망, 청산도망];
  Promise.all(나머지셋도망);
}

도망계획();


실제로 굳이 청산, 수혁, 온조간의 의존성을 만들어 줄 필요가 없는 상황에서 이렇게 수행한다면 속도를 단축시킬 수 있다.

이는 마치 자바스크립트가 ‘병렬적 (Parallel)'으로 처리되는게 아닌가 하는 의문이 든다.

하지만 자바스크립트는 Single Thread이고, 일반적으로 Parallel작업을 수행하지 못한다.

그래서‘엄밀히' 따지면 병렬이 아닌 병행적 (Concurrent)으로 처리되어서, 동시에 처리된 것처럼 ‘보이는'것이다.

사진 출처: https://javascript.plainenglish.io/does-promise-all-execute-in-parallel-how-promise-all-works-in-javascript-fffc2e8d455d

예시를 들어 설명하자면 ‘Parallel’은 A와 B 작업을 ‘실제로’ 동시에 수행하는 것이다.

반면 ‘Concurrent’는 A와 B를 동시가 아닌 순서대로 실행하지만, A 작업을 실행하고 그 작업이 끝나기도 전에 B를 수행하고, 두 결과를 반환하는 것이다.


약속의 범위

물론, 이 약속의 범위에도 대해서 짚고 넘어갈 필요가 있다.

이 약속은 이들 외, 다른 학교 학생들, 양궁팸 등은 전혀 모르는 일이다.

이건 오로지 그 자리에 있던 ‘이 친구들’ 사이에서 통용되는 약속이다. (청산, 수혁, 남라 온조)

양궁팸인 미진이와 장하리는 해당 약속과 무관하므로 그냥 먼저 가버릴 것이다. (달리기 속도 빠르다고 가정)


async function 도망계획() {
  await 도망("남라", 남라_달리기_속도);

  const 온조도망 = 도망("온조", 온조_달리기_속도);
  const 수혁도망 = 도망("수혁", 수혁_달리기_속도);
  const 청산도망 = 도망("청산", 청산_달리기_속도);

  const 나머지셋도망 = [온조도망, 수혁도망, 청산도망];
  Promise.all(나머지셋도망);
}

// 양궁팸
console.log("미진이 도착");
console.log("장하리 도착");

똑같이 공부하는 입장이라서, 만약 틀린 점이나 모호한 점이 있다면 댓글로 알려주시길 바랍니다!

profile
Self Refiner

1개의 댓글

comment-user-thumbnail
2022년 3월 20일

잘 봤어요 ^^

답글 달기