모카스터디/JavaScript

동기와 비동기 [싱글스레드, 논블로킹, Promise, async&await]

softmoca__ 2024. 2. 29. 23:39
목차

자바스크립트는 싱글스레드이지만 논블로킹으로 동작

자바스크립트는 비동기 처럼 사용할 수 있지만, 브라우저api나 Node api(settimeout..)의 도움을 받아서 비동기처럼 사용할 수 있다.

 

 

 

 

 

동기 방식

자바스크립트는 코드가 작성된 순서대로 작업을 처리한다.

이전 작업이 진행 중일 떄는 다음 작업을 수행하지 않고 기다린다.

먼저 작성된 코드를 먼저 다 실행하고 나서 뒤에 작성된 코드를 실행한다.

동기적 처리의 단점은 하나의 작업이 너무 오래 걸릴 시, 모든 작업이 오래 걸리는 하나의 작업이 종료되기 전까지 올 스탑 되기 때문에, 전반적인 흐름이 느려진다.

 

ex) 웹사이트에서 버튼 하나를 눌렀지만 이전 다른 작업이 다 처리 되지 않아 30초 후에 버튼이 클리이 되면 속이 터진다.

 

멀티 쓰레드

코드를 실행하는 일꾼인 쓰레드를 여러개 사용하는 방식인 멀티쓰레드 방식으로 작동을 시키면 위와 같이 작업을 분할 해서 할 수 있다. 오래 걸리는 일은 다른 일꾼 쓰레드에게 실행시키게 지시하면 된다.

 

그러나 자바스크립트는 싱글 쓰레드로 동작해서 여러개의 일꾼쓰레드를 사용할 수 없다.

비동기 작업

 

싱글 쓰레드 방식을 이용하면서, 동기적 작업의 단점을 극복하기 위해 여러개의 작업을 동시에 실행시킨다.

즉, 먼저 작성된 코드의 결과를 기다리지 않고 다음 코드를 바로 실행한다.

만약 비동기 요청이 여러 개 있을 때 하나의 요청이 다른 요청의 결과에 의존한다면?

아래의 소스코드에서 처럼 둘 다 비동기 요청을 보내는데 두 번째 요청이 첫 번째 요청의 결과가 필요할 수가 있다. 하지만 둘 다 병렬적으로 요청을 보내기 때문에 response1을 가지기 전에 두 번째 요청이 보내지게 된다. 이런 부분은 어떻게 처리해줘야 할까?

const res1=resquest('http://aabbcc.com');
const res2=resquest('http://zzxxtt.com',res1);

 

위와 같은 문제를 해결 하는 세가지 방법이 있다.

1. 콜백 함수   2. Promise객체   3. Async/Awiat

 

 

1. 콜백 함수

콜백 함수는 특정 함수에 매개변수로 전달된 함수를 의미한다.

그리고 그 콜백 함수는 함수를 전달받은 함수 안에서 호출된다.

 

동시에 실행한 task의 끝남을 알리기 위해 비동기적으로 실행한 task가 끝난뒤 실행할 콜백 함수인자로 준다.

자바스크립트는 Call Stack이 하나여서 싱글스레드로 동작한다.

function taskA(a, b, cb) {
  setTimeout(() => {
    const res = a + b;
    cb(res);
  }, 3000);  // 3초후
}

function taskB(a, cb) {
  setTimeout(() => {
    const res = a * 2;
    cb(res);
  }, 1000);  //  1초후
}

function taskC(a, cb) {
  setTimeout(() => {
    const res = a * -1;
    cb(res);
  }, 2000);    //2초후
}

taskA(3, 4, (res) => {
  console.log("A TASK RESULT:", res);
});

taskB(7, (res) => {
  console.log("B TASK RESULT:", res);
});

taskB(14, (res) => {
  console.log("C TASK RESULT:", res);
});

console.log("코드끝"); //

출력 결과 

 

코드끝

B TASK RESULT : 14

C TASK RESULT : 7

A TASK RESULT : -14

 

 

비동기 적으로 3개의 task가 동시에 시작하고 끝날 때마다 각각의 콜백함수를 실행 하였다.

A는 3초 B는 1초 C는 2초로  B,C,A순서로 실행 되었다.

 

 

자바스크립트 엔진은 어떻게 위와 같은 동기적인 코드와 비동기적인 코드룰 구분하여 실행 할까 ?

 

WebAPIs는 브라우저가 제공하는 API들이며 대표적으로 아래와 같은 3개가 있다.

Heap은 변수나 상수들의 메모리를 저장하는 영역으로 동기/비동기 이해에서는 크게 중요하지 않다.

콜 스택은 실제 코드가 실행되는 영역이다.

 

동기 방식으로의 Call Stack 예제

main Context가 콜스택에 들어오는 순간이 프로그램 실행 순간이고 main Context가 콜 스택에서 나가는 순간이 프로그램이 종료되는 순간이다.

함수 one,two,three는 함수 생성만 하고 실행은 되지 않고 넘어 간다.

 

1을 리턴하고 바로 종료(콜스택에서 빠진다.)

콜스택 하나가 스레드(코드를 실행)이다.

자바스크립트 엔진은 콜스택이 하나이며 그래서 싱글스레드라고 한다.

 

비동기 방식으로의 Call Stack 예제

 

 

빨간색의 setTimeout함수는 콜백 함수를 지닌 비동기 함수 이다.

하지만 비동기 함수로 처리를 하지 않고 콜스택에 그대로 넣고 진행이 되면 3초를 실제 기다린 뒤 다음 콜백 함수를 실행 하게 되어 동기적으로 수행 하게 된다.

 

 

그래서 위와 같이 비동기 함수가 오면  Web APIs로 넘긴다.

그리고 setTimeout은 실행이 멈추는게 아니라 3초 WebAPIs에서 기다리게 된다.

setTimeout함수가 콜스택에 머무리지 않기 때문에 다음 함수가 바로 실행 되어 asyncadd함수가 끝나게 되고 실행을 다 마쳤기 때문에 콜스택에서 제거가 된다.

setTimeout의 3초가 끝나면 setTimeout는 제거가 되고 WebAPIs에 있는 콜백 함수는 콜백큐로 이동이 된다.

그리고 이벤트 루프에 의해 콜스택으로 다시 옮겨진다.

콜백 큐에서 콜 스택으로 옮겨 진다는 의미는 콜백 함수가 실제로 수행이 이루어 진다는 뜻이다.

 

 

콜백 큐에서 콜백 스택으로 cb함수를 넘겨줄때

이벤트 루프는 콜스택의 Main Context를 제외한 다른 함수가 남아 있는지 자꾸 확인을 한다.

만약 아무것도 남아 있지 않다면  그때 콜백함수를 실행할 수 있게 되니깐 콜백함수를 콜 스택으로 넘겨 콜백함수가 수행한다.

 

 

 

 

 

0초 있다가 실행하라는 setTimeout도 우선 WebAPIs로 이동한 뒤 콜백큐를 거쳐 다시 콜스택에 와서 3번째로 실행이 된다.

 

 

콜백지옥 예시

function taskA(a, b, cb) {
  setTimeout(() => {
    const res = a + b;
    cb(res);
  }, 3000);  // 3초후
}

function taskB(a, cb) {
  setTimeout(() => {
    const res = a * 2;
    cb(res);
  }, 1000);  //  1초후
}

function taskC(a, cb) {
  setTimeout(() => {
    const res = a * -1;
    cb(res);
  }, 2000);    //2초후
}

taskA(4,5,(a_res) => {
	console.log("A RESULT : ",a_res);
    taskB(a_res,(b_res) => {
    console.log("B RESULT : ",b_res);
    taskC(b_res,(c_res) => {
    console.log("C RESULT : ",c_res);
   });
  });
 });
    



console.log("코드 끝"); //

taskA가 실행이되고

코드 끝 이 나온 뒤

3초 후  A RESULT가 나온다.

그리고 그  A RESULT를 인자로 taskB가 실행이되고 

1초 후   B RESULT가 나온다.

그리고 그  B RESULT 를 인자로 taskC가 실행이 되고

2초 후 C RESULT가 나온다.

 

이렇게 비동기 처리의 결과 값을 또다른 비동기 처리의 인자로 전달하는 상황이 많아 질 수 있다.

이렇게 3개뿐 아니라 5개,10개가 되는 경우 콜백 지옥이라고 부른다.

그럼 가독성이 매우 좋지 않아진다.

 

function firstFunction(parameters, callback) {
  // .......다른작업

  const response1 = request('http://abc.com?id=${parameters.id}');
  callback(response1);
}

function secondFunction(response1, callback) {
  // do something

  const response2 = request('http://bcd.com', response1);
  callback(response2);
}

firstFunction(para, function (response1) {
  secondFunction(response1, function (response2) {
    thirdFunction(para, function () {
      // 다른 로직....
    });
  });
});

또한 에러 처리를 한다면 모든 콜백에서 각각 에러 핸들링을 해줘야 한다.

 

 

 

그래서 나온 객체가 Promise이다.

 

2. Promise

function fetchData() {
    return new Promise((resolve, reject) => {
        // 비동기 요청 
        const success = false;
        if (success) {
            resolve('성공');
        } else {
            reject('실패');
        }
    })
}

 fetchData()
     .then((response) => {
         console.log(response);
     })
     .catch((error) => {
         console.error(error);
     })
     .finally(() => {
     	console.log('---모든 작업 끝 ---');
     })

Promise 객체는 new 키워드와 생성자를 사용해 만든다.

생성자는 매개변수로 "실행함수"를 받는다.

이 함수는 매개 변수로 두가지 함수를 받아야 한다.

첫번째는 resolve라는 비동기 작업을 성공적으로 완료해 결과를 값으로 반환할 때 호출하는 함수이고 두번째는 reject라는 작업이 실패하여 오류  원인을 반호나할 때 호출되는 함수 이다. 두번째 함수는 주로 오류 객체를 받는다.

 

비동기 작업이 가질 수 있는 3가지 상태

비동기 작업은 위와 같이 3가지 상태를 가지고 있으며 해당 상태에 따라 반환해줘야하는 응답 값이 달라지게 된다.

즉, 그 해당 상태에 따라 다른 작업을 편리하게 해주는 객체가 Promise이다.

 

 

Promise없이 콜백함수들인 resolve와 reject를 사용한 비동기 예제

function isPositive(number, resolve, reject) {
  setTimeout(() => {
    if (typeof number === "number") {
      // 성공 -> resolve
      resolve(number >= 0 ? "양수" : "음수");
    } else {
      // 실패 -> reject
      reject("주어진 값이 숫자형 값이 아닙니다");
    }
  }, 2000);
}

isPositive(
  10,
  (res) => {
    console.log("성공적으로 수행됨: ", res);
  },
  (err) => {
    console.log("실패 하였음:", err);
  }
);

위 예제는 Promise객체를 사용하지 않고 Promise객체의 실행을 구현한 코드이다.

 

 

Promise를 반환하는 비동기 작업을 하는 함수 예제

 

function isPositiveP(number) {
  const executor = (resolve, reject) => {
    // 실행자

    setTimeout(() => {
      if (typeof number === "number") {
        // 성공 -> resolve

        console.log(number);
        resolve(number >= 0 ? "양수" : "음수");
      } else {
        // 실패 -> reject

        reject("주어진 값이 숫자형 값이 아닙니다");
      }
    }, 2000);
  };

  const asyncTask = new Promise(executor);
  return asyncTask;
}

비동기 작업 자체인 Promise를 저장할 상수 asynTask를 만들어준 다음에 new키워드를 사용해 Promise객체를 생성하면서

Promise객체의 생성자로 비동기 작업의 실질적인 실행자 함수 executor를 넘겨주면 자동으로 executor함수가 실행된다.

 

어떤 함수가 Promise를 반환 한다는 것은 그 함수는 비동기 작업을 하고 그 작업의 결과극 Promise객체로 반환받아서 사용할수 있는 함수이다.

 

프로미스 사용 예시

const res=isPositiveP(101);
const res=isPositiveP(-101);


res
  .then(res) => {
    console.log("작업 성공" : ",res);
    })
    .catch(err) => {
    console.log("작업 실패: " ,err);
    });

 

Promise객체인 then과 catch를 사용하면

reslove를 수행 했을떄의 결과 값을 then 콜백 함수에서 사용할 수 있고

reject를 수행 했을 때의 결과값을 catch 콜백 함수에서 사용할 수 있다.

 

 

위에서 작성된 콜백 지옥을 Promise로 해결해 보자.

 

앞으로는 위와 같이 실행자 함수를 바로 promise객체의 생성자로 넣어코드를 작성.

 

프로미스를 반환하는 비동기 함수로 코드 수정

function taskA(a, b) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = a + b;
      resolve(res);
    }, 3000);
  });
}

function taskB(a) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = a * 2;
      resolve(res);
    }, 1000);
  });
}

function taskC(a) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = a * -1;
      resolve(res);
    }, 2000);
  });
}

 

 

콜백 지옥을 못 벗어난 비효율적인 Promise 사용 예시

taskA(5, 10).then((a_res) => {
  console.log("A RESULT: ", a_res);
  taskB(a_res).then((b_res) => {
    console.log("B RESULT: ", b_res);
    taskC(b_res).then((c_res) => {
      console.log("C RESULT: ", c_res);
    });
  });
});

 

 

then 체이닝을 사용한 올바른 Promise 사용 예시

taskA(5, 1)
  .then((a_res) => {
    console.log("A RESULT:", a_res);
    return taskB(a_res);
  })
  .then((b_res) => {
    console.log("B RESULT:", b_res);
    return taskC(b_res);
  })
  .then((c_res) => {
    console.log("C RESULT:", c_res);
  });

가독성히 월등히 올라 갔다.

또한 아래 코드와 같이 then 체이닝 사이에 다른 로직을 수행할수 있다는 장점 또한 있다.

const bPromiseResult = taskA(5, 1).then((a_res) => {
  console.log("A RESULT: ", a_res);
  return taskB(a_res);
});

console.log("bladfskladfjsldfjslkd");
console.log("bladfskladfjsldfjslkd");

console.log("bladfskladfjsldfjslkd");

bPromiseResult
.then((b_res) => {
  return taskC(b_res);
  console.log("B RESULT: ", b_res);
})
.then((c_res) => {
  console.log("C RESULT: ", c_res);
});

 

 

 

 

 

 

3.async & await

- 직관적인 비 동기 처리 코드 작성하기

함수 선언식 앞에 async라는 키워드만 붙여주면 반환값이 Promise가 된다.

콘솔을 찍어 보면 프로미스가 반환되는 것을 확인할 수 있다.

그리고 then을 사용해보면 hello Asyync 문자열이 res로 전달되어 콘솔로 잘 찍히는것을 알 수 있다.

 

즉, async 키워드를 붙여준 함수의 리턴값은 비동기 작업 Promise의 resolve의 결과 값이 된다.

 

 

 

 

function delay(ms) {
	return new Promise((reslove) => {
    	setTimeout(reslove,ms);
        });
   }

async function hellowAsync() {
	await delay(3000);
    return "hello async";
 }

awiat이라는 키워드를 비동기 함수의 호출 앞에 붙이면 비동기적인 함수가 마치 동기적인 함수인것 처럼 동작을 한다.

즉, delay함수의 작업이 끝나기 전까지 아래의 return 문이 작동 하지 않는다.

 

 

async/await 예시 2

async function makeRequests() {
    try {
        const response1 = await fetch('http://jsonplaceholder.typicode.com/todos/1')
        const jsonResponse1 = await response1.json();
        console.log(jsonResponse1);
    } catch (error) {
        console.error(error);
    } finally {
        console.log('---모든 작업 끝---')
    }
}

makeRequests();

 

 

 

 

 

 

 

 

 

 

 

 

 

자료 참조

https://www.inflearn.com/course/lecture?courseSlug=%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8&unitId=103477&tab=curriculum

 

학습 페이지

 

www.inflearn.com

 

https://www.youtube.com/watch?v=v67LloZ1ieI

 

 

 

https://www.youtube.com/watch?v=zi-IG6VHBh8

 

 

https://medium.com/sessionstack-blog/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with-2f077c4438b5

 

How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with…

Welcome to post # 4 of the series dedicated to exploring JavaScript and its building components. In the process of identifying and…

medium.com

https://storage.googleapis.com/static.fastcampus.co.kr/prod/uploads/202308/174702-717/[%ED%8C%A8%EC%8A%A4%ED%8A%B8%EC%BA%A0%ED%8D%BC%EC%8A%A4]-%EA%B5%90%EC%9C%A1%EA%B3%BC%EC%A0%95%EC%86%8C%EA%B0%9C%EC%84%9C-10%EA%B0%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A1%9C-%EB%81%9D%EB%82%B4%EB%8A%94-node.js%EC%9D%98-%EB%AA%A8%EB%93%A0-%EA%B2%83-express---nest.js--%EC%B4%88%EA%B2%A9%EC%B0%A8-%ED%8C%A8%ED%82%A4%EC%A7%80-online..pdf