신입 개발자에서 시니어 개발자가 되기까지

section2 비동기프로그래밍 - 비동기 코드의 작동 원리 본문

코드스테이츠/section2

section2 비동기프로그래밍 - 비동기 코드의 작동 원리

Jin.K 2022. 9. 28. 00:48

1. 개념

    a. 현재 실행되고 있는 함수가 종료되지 않았더라도, 다음 함수를 실행시키는 것.

2. 사례

DOM Element 이벤트 핸들러

    i. click, keydown
    ii. 페이지 로딩(DOMContentLoaded 등)

타이머

    i. 타이머 API(setTimeout)
    ii. 애니메이션 API(requestAnimationFrame)

서버 자원 요청 및 응답

    i. fetch API
    ii. AJAX (XHR)

 


3. Promise

a. 쓰는 이유

        i. 비동기를 동기적으로 처리하기 위해서(그냥 생각없이 이게 끝나면 프로미스 객체를 받아와서 그 객체 데이터를 이용하는 식으로 사용만 해왔는데, 드디어 개념이 좀 구체화 된 것 같다)
        ii. callback 지옥에서 빠져나오기 위해서(가독성). callback으로도 비동기 함수들을 동기적으로 처리할 수 있지만 가독성이 매우 안좋다.

b. MDN 예제

    let myFirstPromise = new Promise((resolve, reject) => {
      setTimeout(function () {
        resolve("성공!"); // 와! 문제 없음!
      }, Math.floor(Math.random() * 100) + 1);
    });
    console.log(myFirstPromise); //pending 이라는 의미
    myFirstPromise
      .then((successMessage) => {
        console.log("와! " + successMessage);
      })
      .then(() =>
        myFirstPromise.then((successMessage) => {
          console.log("두 번째 성공 : ", successMessage);
        })
      );

예제에서는 변수에 new Promise객체를 할당해줬기 때문에 소비함수?인 then으로 이 프로미스 객체 내의 콜백 함수를 호출 할 수 있다. (또는 resolve나 reject의 결과를 받아올 수 있다. 정확한 용어를 모르겠으므로 일단 이렇게 적어 놓는다. 어쨌든 기본 컨셉은 이 예제에서 promise 객체 내의 setTimeout 함수를 불러오려면, then을 써야한다는 것이다.) 이렇게 then으로 원하는 결과값을 얻고 난 후에 또 다시 then을 사용하면, 이 때는 이 프로미스 객체의 반환이 끝난 후에 그 다음 동작을 then 다음에 오는 콜백함수에서 결정하겠다는 뜻이다.
프로미스 자체는 비동기 함수지만 then을 사용해서 순서를 만들어주는 것이다. 만약 then을 사용하지 않으면, 반환을 받는 순서가 그때그때 바뀐다.

c. Promise의 return 값

        i. Promise 객체를 반환한다(pending, fulfilled, rejected 값 중 하나가 나옴)
        ii. Promise.then 의 return 값
            1) array.push()가 length를 반환하는 것처럼, then은 Promise를 반환한다.
            2) then의 callback함수의 return 값이 Promise의 promiseResult의 값이 된다.
            3) then의 callback함수의 인자는 Promise의 promiseResult 값을 받는다.
        iii. catch를 사용하는 이유
            1) 코드 강제 종료를 막기 위해(앱 강제 종료를 막아준다)

d. 함수 안에서 프로미스 객체 리턴 해주는 예제

물론 이 경우에도 return printSuccessMessage()를 해주지 않으면 then이 필요하다.

    function printSuccessMessage(string) {
      return new Promise((resolve, reject) => {
        setTimeout(function () {
          console.log(string, "함수 내에서 promise 객체 만들기");
          resolve();
        }, Math.floor(Math.random() * 100) + 1);
      });
    }
    printSuccessMessage("첫 번째")
      .then(() => printSuccessMessage("두 번째"))
      .then(() => printSuccessMessage("세 번째"));
    //첫 번째 함수 내에서 promise 객체 만들기
    //두 번째 함수 내에서 promise 객체 만들기
    //세 번째 함수 내에서 promise 객체 만들기

e. 에러처리

  • 기존의 callback함수에서는 호출할 때마다 에러 처리를 해줘야 했지만 promise는 마지막 체인에서만 catch 메서드를 사용하면 된다.

 

4. async / await

a. promise의 가독성문제를 해결하기 위한 문법적 설탕.

b. 함수 선언에 async 키워드를 붙이고, 비동기 함수 할당에 await 키워드를 붙여 할당하면, 마치 비동기 코드를 동기적 코드처럼 사용할 수 있다.

c. then으로 데이터를 받아올 때

    const readAllUsersChaining = () => {
      return getDataFromFilePromise(user1Path).then((data) => {
        return getDataFromFilePromise(user2Path)
          .then((data2) => {
            return [JSON.parse(data), JSON.parse(data2)];
          })
          .catch((error) => console.log(error));
      });
    };

d. async / await

    const readAllUsersAsyncAwait = async () => {
      const user1Data = await getDataFromFilePromise(user1Path).then((data) =>
        JSON.parse(data)
      );
      const user2Data = await getDataFromFilePromise(user2Path).then((data) =>
        JSON.parse(data)
      );
      return [user1Data, user2Data];

};

e. async / await의 단점

        i. 병목현상 발생(느려질 수 있음.) 관련없는 데이터들 간에는 굳이 동기적으로 작동할 필요가 없으니 await 남발하지 말고, Promise.all 해도 된다.

 


5. 동기적 코드의 작동 원리

a. 개념

    - 현재 실행중인 태스크가 종료할 때까지 다음에 실행될 태스크가 대기하는 방식. 실행 순서가 보장된다는 장점이 있지만, 태스크들이 블록킹된다는 단점도 있다.

b. Blocking : 앞에 있는 함수가 실행 중일 때 함수가 실행되지 못하는 것.

c. 함수를 호출 했을 때 처리 과정

        i. 함수 코드가 평가되고, 함수 실행 컨텍스트가 생성 됨.
        ii. 스택 자료구조(후입선출)인 실행 컨텍스트 스택(콜 스택)에 함수가 푸시 됨.
        iii. 함수 코드 실행
        iv. 함수 코드 실행이 종료되면 실행 컨텍스트 스택에서 pop되어 제거 된다.
        v. 이렇게 하나의 함수가 실행 종료되어 pop되면 비로소 다음 함수가 다시 실행 컨텍스트 스택에 푸시된다.
        vi. 실행 중인 실행 컨텍스트(함수 실행 컨텍스트)를 제외한 모든 실행 컨텍스트는 실행 대기 중인 태스크다.
        vii. 이 실행 대기중인 태스크들은 현재 실행 중인 함수가 종료되어 실행 컨텍스트 스택에서 제거되면 실행된다.

6. 비동기처리의 작동 원리(이벤트 루프와 브라우저 환경)

a. 자바스크립트 엔진은 단하나의 실행 컨텍스트 스택을 가지고 있기 때문에(싱글 스레드) 동시에 2개 이상의 함수를 실행할 수 없다. 비동기 처리는 브라우저에서 하는 것. 브라우저는 멀티 스레드 환경을 갖는다.

b. 자바스크립트 엔진의 영역

        i. 콜 스택 : 함수가 실행되는 공간
        ii. 힙 : 객체가 저장되는 메모리 공간

c. 자바스크립트 엔진이 아니라 브라우저 내장 기능이 자바스크립트를 비동기적으로 동작하게 해준다. ex) HTML 요소가 애니메이션 효과를 통해 움직이면서 이벤트 처리를 하는 경우, HTTP 요청을 통해 서버로부터 데이터를 가지고 오면서 렌더링하는 경우

d. 브라우저 환경

        i. 태스크 큐 : setTimeout, setInterval과 같은 비동기 함수의 "콜백 함수" 또는 "이벤트 핸들러"가 일시적으로 보관되는 영역
        ii. 마이크로 태스크 큐 : 프로미스 후속 처리 메서드의 "콜백 함수"가 일시적으로 보관되는 영역. 태스크 큐보다 우선순위가 높다.
        iii. 이벤트 루프 : 콜스택에 실행 중인 실행 컨텍스트가 있는지, 태스크 큐에 대기중인 함수가 있는지 반복해서 확인한다. 콜 스택이 비어 있고 태스크 큐에 대기 중인 함수가 있으면 선입선출 방식으로 태스크 큐에 있는 함수를 콜 스택에 푸시한다.

e. 코드 예제

i. 해당 코드를 실행시켰을 때의 작동 순서, 작동 원리(결론적으로 foo보다 bar함수가 먼저 실행 됨)

        1) 전역 코드가 평가되어 전역 실행 컨텍스트가 생성되고 콜 스택에 푸시 된다.
        2) 코드가 실행되어 setTimeout이 호출되면 setTimeout의 실행 컨텍스트가 생성되고, 콜 스택에 푸시 됨.
        3) setTimeout는 호출 스케줄링 후에 종료되어 콜 스택에서 제거된다. 호출 스케줄링 이란 콜백함수를 태스크 큐에 푸시할 타이머 설정을 말한다.
        4) 설정해놓은 시간이 되면(예를 들어 1000을 설정하면 1초 후에) 브라우저는 콜백 함수를 태스크 큐에 푸시한다. 0일 때는 자동으로 4ms로 설정 됨(이 단계는 자바스크립트 엔진이 아니라 브라우저가 수행하는 것이다)
        5) 여기서 setTimeout 함수는 콜 스택에서 제거되고, 콜 스택은 비어 있기 때문에 bar함수가 호출되어 콜 스택에 푸시 된다. 
        6) 전역 코드 실행이 종료되고, 전역 실행 컨텍스트가 콜 스택에서 제거된다. (콜 스택이 비어있음)

이벤트 루프가 콜 스택이 비어있음을 감지하고, 태스크 큐에 foo함수가 있다면(4ms가 지났다면) 그 때 비로소 foo 함수를 콜 스택에 푸시한다.

'코드스테이츠 > section2' 카테고리의 다른 글

[Section2] Rest API  (0) 2022.10.06
[section2] React State & Props  (1) 2022.10.04
[section2] SPA(Single Page Application)  (0) 2022.09.30
[section2] React intro  (0) 2022.09.30
[section2] 객체지향 프로그래밍 Class, prototype  (0) 2022.09.21