자바스크립트가 비동기를 처리하는 방법 Ⅰ

jinew·2025년 1월 10일

🍎 Javascript

목록 보기
12/22
post-thumbnail

오늘은 예전에도 지금도 앞으로도 나의 숙제가 될 자바스크립트의 비동기 처리에 관해 공부했다. 나의 말로 하나씩 뜯어먹어서 이해해보기 시작!



💭 상상하기


: 우리가 일상에서 여러 가지 일을 동시에 처리하는 상황을 가정해보자.
예를 들어 출근 전 오늘 하루를 견디기 위한 커피☕를 사러 회사 근처 카페에 들렀다.

  • 카페에 들어갔더니 바리스타가 단 한 명뿐이다. 그래서 모든 주문을 순서대로 처리하고 있다.
  • 당신은 커피를 주문하려고 줄을 섰다. 앞에는 5명의 사람이 있고 몇 명은 아주 복잡한 주문을 했다. (예를 들면.. 자바칩 프라푸치노에 모카시럽 세 번 추가하고 헤이즐넛 시럽 두 번 추가하고 휘핑크림에 초코 드리즐 뿌려주시고요 자바칩은 통이랑 갈아서 반반 넣어주세요 같은 주문을 다섯 명 전부 다 했다.)
  • 바리스타가 한 명씩 주문을 받고, 음료를 만들고, 완성될 때까지 기다린 후에 다음 주문을 받는다.
  • 아이스 아메리카노 한 잔이면 되는데 먼저 해주면 안 되냐고 할 수 없으니 꽤 오래 기다려야만 할 것이다.

⛔ 이것이 바로 모든 작업을 순차적으로 처리해야 할 때의 비효율성이다!



그렇지만 현실에서 이런 일은 잘 벌어지지 않는다😅 카페는 ① 주문을 받는 일② 음료를 만드는 일을 분담하여 주문을 처리한다.

  • 카페 직원은 주문을 받고, 음료를 만드는 일을 다른 직원에게 넘기고, 바로 다음 손님의 주문을 받는다.
  • 손님은 자신의 음료가 다 만들어질 때까지 기다릴 필요 없이, 각자 볼 일을 보며 대기하다 음료가 준비되어 직원이 주문 번호를 부르거나 진동벨이 울릴 때 음료를 가져가면 된다.

✅ 이렇게 하면 음료 주문과 음료를 제조하는 작업이 동시에 진행될 수 있다!



💬 이해하기


: 이제 자바스크립트로 돌아와 이해해보자!

그 전에 추가로 알아야 할 것이 있는데 프로그래밍 언어가 처리되는 작업 공간, 즉 '스레드'다.

🚧 스레드(Threads)란?
: 스레드란, 쉽게 말해 작업을 처리하는 일꾼이다.
컴퓨터는 여러 가지 일을 처리하기 위해 CPU라는 뇌🧠를 사용하고, 이 CPU가 일을 처리하는 단위가 바로 '스레드'다.

스레드는 프로그램이 실행되는 동안 작업을 수행하는 역할을 한다. 컴퓨터에서 동영상을 보면서 동시에 문서 작업이 가능한 이유가 바로 여러 스레드가 각 작업을 처리하기 때문이다! 👨‍🔧 일꾼이 여러 명! (신기하죵?)

하지만 우리의 자바스크립트는 '단일 스레드' 언어다. 자바스크립트는 웹 브라우저에서 실행되도록 설계된 언어이며, 웹 브라우저는 사용자의 클릭, 스크롤, 키 입력과 같은 작업을 바로바로 처리해야 한다.
만약 자바스크립트가 멀티 스레드 언어여서 여러 스레드가 동시에 실행되다가 충돌이 나면 💥 웹 페이지가 멈추거나 오작동할 위험이 있다! 그럼 유저들이 굉장히 싫어한다..

그래서 자바스크립트는 단일 스레드로 설계되었다. 그러면 일꾼이 한 명이라는 말이고, 일꾼 한 명이 모든 일을 다 한다는 건데 너무 비효율적이지 않을까? 하는 생각이 들 수 있다.
이러한 단점을 보완하고 효율적으로 업무를 처리하기 위해 자바스크립트 사장님이 채용한 당근 알바가 바로 Web APIs 이다.

  • 자바스크립트는 "야! 이거 좀 대신 처리해줘!" 하고 Web APIs에게 일을 넘겨주고, 바로 자신의 다음 작업으로 넘어간다.
  • Web APIs는 일이 끝나면 "다 됐어!"하고 자바스크립트에게 알려준다.
  • 자바스크립트는 그제서야 결과를 받아서 처리한다.

자바스크립트의 Web APIs를 활용한 비동기 처리 방식은 당근 알바를 데려다 쓰는 것이기 때문에, 이 알바가 업무를 잘 처리할 수 있도록 '비동기 업무를 관리하는 방식'이 있으면 좋을 것 같다.

여기서 등장하는 방식이 바로 Promise다.
Promise는 자바스크립트가 이러한 비동기 작업을 더 잘 관리하도록 돕는 도구로,
Promise를 쓰면 "이 일이 언제 끝날진 모르지만, 끝나면 결과를 받을게!"라고 약속을 정해 둘 수 있다.

: 예를 들어 카페에 가서 음료를 주문했다고 했을 때,

  • 바리스타는 "음료가 준비되면 주문 번호를 불러드리겠습니다!" 하고 약속을 한다.

  • 우리는 그 약속을 믿고, 음료가 나올 때까지 다른 일을 할 수 있다. (못 믿으면 그 앞에서 내 음료 잘 만들고 있는지 계속 감시를 해야하겠지.)

  • 음료가 준비되면 바리스타가 "○○○번 고객님, 음료 나왔습니다!" 하고 알려주고, 우리는 음료를 받아간다.

  • 만약 음료를 준비하다가 그 음료를 제조할 수 없는 문제가 생기면, 바리스타는 "죄송하지만, 오늘은 그 음료 제조가 어렵습니다." 하고 알려준다.

  • Promise는 이런 과정과 비슷하다!

    1. Pending: 음료가 아직 준비되지 않은 상태.
    2. Fulfilled: 음료가 성공적으로 준비된 상태.
    3. Rejected: 음료를 못 만드는 상태.


[더 알아보기]

❓ Promise로 비동기 작업을 관리하면 좋은 점
- Promise를 사용하지 않으면 비동기 작업을 관리하는 코드가 굉장히 복잡해질 수 있다. 
이를 '콜백 지옥'이라고 하는데, 다시 카페 알바로 예를 들면

● 알바1에게 "지금 하는 일 끝나면 알바2한테 다음 업무 넘겨줘"라고 부탁했다.
● 알바1이 일을 끝내고 알바2에게 가서 업무를 전달한다.
● 알바2가 업무를 끝내고 알바3에게 가서 업무를 전달한다.
● 알바3이 업무를 끝ㄴ..

이렇게 되면 알바들이 줄줄이 연결되면서 각각 자신의 업무가 끝날 때마다 계속 다음 알바를 불러야한다.
이렇게 되면 업무 플로우가 불명확해지고, 이전 근무자의 업무 상태를 확인하며 일을 해야해서
서로가 서로에게 얽히며 일이 복잡해지게 되는데 이게 바로 "콜백 지옥"이다.


이 상황을 Promise로 해결을 하게 되면?
● 첫 번째 알바에게 일을 맡길 때, "이 일이 끝나면 결과를 알려줘!" 라고만 전달한다.
● 그 후, Promise는 알바가 일을 끝내고 결과를 자기 혼자 처리할 수 있게 된다.
● 만약 두 번째 알바가 필요하면 then을 사용해서 두 번째 알바에게 일을 맡기고
● 그 다음 알바도 then을 사용해서 이어서 맡길 수 있다.

즉❗❗ 알바들끼리 서로 입에서 입으로 업무 상태를 전할 필요가 없이
카페매니저 역할을 하는 Promise가 직접 업무 상태를 확인하고 그 다음 업무를 지시하는 거다!
그러면 업무가 순차적이고 깔끔하게 처리될 수 있다 🙆‍♀️


✅ 적용하기


// 1. '주문하기'라는 새로운 약속을 한다. 약속의 내용은 아래와 같다. 
// 서로서로 이 약속을 지키자고 약속에 대한 규칙을 적는 거다. 계약서처럼! 📄
const 주문하기 = new Promise((resolve, reject) => {

  if (만약 음료가 성공적으로 만들어졌다면) {
    resolve("음료가 준비됐습니다!"); // 문제 없이 음료가 만들어지면 준비됐다고 말해줄 것
  } else { // 아니라면
    reject("음료를 만들 수 없습니다!"); // 문제가 생기면 음료를 만들 수 없다고 말해줄 것
  }
});

// 2. 이제 잘 적어둔 약속이 여기서 실제로 이행된다! 계약의 효력이 발생하는 곳!
주문하기 // 1️⃣ 나 주문했어. 약속 지켜줄거지?
  .then((메시지) => { // ✅ 음료가 잘 만들어졌으면 이렇게 하기로 했지
    console.log(메시지); // console ==> "음료가 준비됐습니다!"
  })
  .catch((에러) => { // ⛔ 음료를 못 만들었다면 이렇게 말해주기로 했어
    console.error(에러); // console ==> "음료를 만들 수 없습니다!"
  });
  1. Promise 계약서의 구조

    • resolve : 약속을 성공적으로 이행했을 때 미래에 호출할 것
    • reject : 약속을 지키지 못했을 때 미래에 호출할 것
  2. thencatch의 역할

    • then : 약속이 성공적으로 이행됐을 때 처리하는 것
    • catch : 약속이 실패했을 때 처리하는 것


🔎 뜯어보기


: 그럼 이제 이 구성을 하나씩 뜯어서 확인해보자. 결과값의 차이를 이해하면 우리는 Promise를 정복한거다 🔥

const promiseFunction = function () {
  return new Promise((resolve, reject) => {
    const success = true; // 작업이 성공했다고 가정
    if (success) {
      resolve("작업 성공!");
    } else {
      reject("작업 실패!");
    }
  });
}

console.log("1 =====>", promiseFunction);
console.log("2 =====>", promiseFunction());

promiseFunction().then((result) => {
  console.log("3 =====>", result);
});
  • promiseFunction에는 함수가 할당되어 있고, 그 함수는 Promise 를 결과값으로 반환한다.
    즉, promiseFunction() 함수가 '실행'되면 약속이 적힌 계약서가 '반환'된다는 것이다.
  • 첫 번째 콘솔에서는 promiseFunction 자체를 출력하고 있다.
  • 두 번째 콘솔에서는 promiseFunction()을 실행시킨 값을 출력하고 있다.
  • 세 번째 콘솔에서는 promiseFunction()을 비동기로 실행한 후, 그 결과값을 반환하고 있다.

이제 콘솔에 어떻게 나올지 예측이 되거나,
혹 예측까진 되지 않더라도 아래 콘솔을 보고 아 ~~ 이제 이해가 됐다! 싶으면 우리가 이긴거다 ~~ 🛫🛫🛫









🧾 console.log

  1. console.log("1 =====>", promiseFunction);

    : 이 출력은 promiseFunction 함수 '자체'를 출력한다. 즉 함수의 정의! 이 함수가 어떤 함수인지를 소개하는 것이다. 왜냐? 아직 실행되지 않았으니까!
    포스기 근처에 놓여 있는 "주문 이후 음료 전달 방식 매뉴얼" 정도를 들여다 본 거랄까?
    (그 안에는 구구절절 내용이 적혀있는 거다. 주문을 하시면 진동벨을 드릴 거고요, 그 진동벨이 울리면 음료가 나온거니 카운터로 가지러 오시면 됩니다 뭐 이렇게)


  1. console.log("2 =====>", promiseFunction());

    : 이 출력은 promiseFunction()을 실행한 결과가 출력된다.

    이제 이 함수의 return값이 선언된 곳을 쫓아가봤을 때, Promise 객체 자체가 return 값임을 알 수 있다.
    그래서 Promise {<pending>} 처럼 비동기 작업을 나타내는 프로미스 객체가 출력된 것이다.

    Promise 즉, 약속이 이행된 게 아니라 그 "약속" 자체를 반환하는 함수 promiseFunction() 이 실행된 점을 캐치해야 한다 ❗

    promiseFunction()은 음료를 주문한 것일 뿐이고(함수 실행), '이게 울리면 음료가 완성된거에요'라는 의미의 약속이 담긴 진동벨 Promise가 return 값으로 내 손에 들어온 것이다. 아직 내 아이스 아메리카노를 받은 게 아니다 ❗ ❗ ❗


  2. promiseFunction().then((result) => { console.log("3 =====>", result); });

    : 이 출력은 promiseFunction()이 실행된 후, 그 결과가 resolve 되면 then을 통해 result를 보여주는 내용이다. (then() 안에 있는 콜백함수와, 그 콜백함수가 받는 인자는 그냥 문법이다. 이 모양새를 통으로 이해하고 암기해야 한다.)

    함수가 실행된 이후 then이 붙으면서 약속이 이행되고 결과값을 받아볼 수 있게 된 것이다.

    즉 !!!!! then이 붙었다 === 진동벨이 울렸다 인 것이고,
    이제 뚜벅뚜벅 카운터로 걸어가서 기다리고 기다렸던 내 아메리카노를 받을 수 있게 된 것이다 ☕

    이제 우리는 then을 진동 소리라고 인식하면 쉬워진다. 징- 징- 을 이제 덴- 덴- 이라고 부르자 😅(개억지지만 이렇게 공부해야 쉬워요 ᐡ´т ‧̫ т ᐡ)
    진동벨이 울려야만 우리가 원하는 데이터의 결과값을 받을 수 있다는 것 !!!

    그것이 오늘의 핵심 💯



🧺 정리하기


  • Promise 그 자체 === "진동벨"

  • 진동벨 === 추후에 음료를 주겠다는 '약속', 울리는 시점은 미정

  • 우리는 진동벨이 울려야만 음료를 받을 수 있음

  • 진동벨이 울렸다(덴- 덴-) === 음료가 나왔다 (이 음료는 코드 상에서 우리가 필요한 데이터에 해당함)

  • 자바스크립트 문법으로 정리하면, ① Promise가 선언된 것만으로는 데이터가 반환되지 않고,
    ② 선언된 이후에 Promise를 실행하면서 resolve된 데이터를 then을 통해 받아 확인할 수 있다.

  • 만약 진동벨이 고장났다면(reject) 카페에서 "죄송합니다, 재료가 떨어져서 음료 제작이 어렵습니다" 라고 알려주는 것이 catch다!

  • resolve된 데이터 ====> then에서 받는다
    reject된 데이터 ====> catch에서 받는다



끝 !!! 이제 Promise의 본질과 구조에 관해서는 제대로 이해한 것 같다!
나의 말로 이해할 수 있도록 꼭꼭 씹어 삼켜야지 .. 비동기가 어려운 누군가에게도 도움이 됐으면 하면서! 그 다음 이야기는.. to be continued .. . ✨

profile
멈추지만 않으면 도착해 🛫

2개의 댓글

comment-user-thumbnail
2025년 1월 10일

교수님 이해가 쏙쏙 됩니다
JS가 단일 스레드인데도 멀티 스레드처럼 작동하는 이유 잘 알아갑니다

답글 달기
comment-user-thumbnail
2025년 1월 12일

카페가면 덴덴 생각날듯요 이제 ㅋㅋ

답글 달기