내일배움캠프

230525 자바스크립트 동기함수 사용 중 렌더링이 멈추는 이유와 비동기 이벤트 루프

Neda 2023. 5. 25. 14:29

 

230525 자바스크립트 이벤트 루프

자바스크립트 문법반 4주차를 들으면서 동기와 비동기에 대해서 공부를 했는데 3주차에 들었던 콜스택만으로는 비동기 작업에 대해 이해할 수 없었다. 콜스택은 차례대로 들어오고 마지막으로 들어온 것부터 다시 나가는 녀석인데, 비동기라는 말과도 어울리지 않고, 콜스택에 비동기 함수가 들어오면 따로 갈 곳이 없다. 그래서 동기와 비동기를 이해하기 위해서는 콜 스택과 함께 태스크 큐를 알아 볼 필요가 있다.

콜스택에 대한 이해

콜스택

  1. 자바스크립트에는 후입선출 구조를 가지는 콜 스택(call stack)이라는 저장소가 있다.
  2. 또한 자바스크립트는 싱글 스레드(single thread)로 동작하므로 한 번에 하나의 동작만 수행할 수 있다.
  3. 자바스크립트의 일반적인 코드는 동기적으로 동작한다.
  4. 코드를 실행하다 함수 호출을 만나면 함수 실행 컨텍스트를 콜 스택에 넣는다.
  5. 그 함수에서 다른 함수를 호출 하면 다시 콜 스택에 쌓는 식이다.
  6. 그리고 함수 실행이 종료되면 콜 스택에서 마지막 함수(실행 컨텍스트)가 제거된다.
  7. 일반적으로는 콜 스택이 너무 쌓아 문제가 없다.
  8. 다만 어떤 함수를 계속 재귀호출을 한다면 콜 스택가 계속 쌓일 것이다. 
  9. 콜 스택은 용량이 무한하지 않으며 정해진 용량이 넘게 쌓이면
    Maxmum call stack size exceeded 에러나
    Uncaught InternalError: Too much recursion 에러를 발생시킨다.

동기

이러한 동기적 작업의 특성은 작업이 수행인 동안 무조건 기다려야 하는 단점이 있다. 브라우저에서 볼 때는 렌더링이 멈추고 클릭과 같은 이벤트도 수행할 수 없다. 만약 동기 작업이 시간이 오래 걸린다면 화면이 멈춰 있는 시간은 길어져 사용자 입장에서는 답답할 것이다. 

비동기

이러한 단점을 보완하는 것이 비동기 작업이다. 비동기 작업이 수행중 일 때에는 다른 이벤트를 수신할 수 있다. 비동기 작업을 수행하는 코드에 의해 콜 스택에 비동기 작업 함수가 들어가면 자바스크립트에서는 webapis로 비동기 함수의 콜백 함수를 보내고 태스크 큐에 쌓이게 된다. 그리고 콜스택이 비어있을 때 이벤트 루프가 콜 스택에 비동기 함수를 올려 실행한다. 결국 비동기 작업 또한 콜 스택에서 수행되지만, 콜 스택이 비어 있을 때만 동작한다.

 

예시

아래는 func()를 실행하는 코드이다.

func()에서는 updateList()라는 비동기 함수를 실행하고 delay()라는 동기 함수를 실행한다.

delay()는 이중 for문으로 수행 시간이 몇 초 정도 걸리는 함수이다.

과정

  1. 호출 스택에 func() 추가
  2. func()  함수 내부에 updateList()를 만나 호출 스택에 추가
  3. updateList() 내부에서 task(0)를 만나 호출 스택에 추가
  4. task(0)에서 delay()를 만나 호출 스택에 추가하고 실행
  5. task(0)가 끝나면 updateList()로 돌아가지 않고 동기 코드인 console.log('delay start')로 넘어간다.
  6. delay()가 실행되고 끝나면 console.log("function end")가 마지막으로 실행되고 func()가 종료된다.
  7. 콜스택이 비어있으므로 updateList()가 계속 수행된다.
      function task(i) {
        console.log(`task [${i}] start`);
        delay();
        console.log(`task [${i}] end`);
        return new Promise((resolve) => setTimeout(resolve, 0));
      }

      async function updateList() {
        console.log("updateList start");
        for (let i = 0; i < 10; i++) {
          await task(i);
          list.children[i].innerHTML = i;
        }
        console.log("updateList end");
      }

      function delay() {
        for (let i = 0; i < 100000; i++) {
          for (let j = 0; j < 100000; j++) {}
        }
        return;
      }
      function func() {
        updateList();
        console.log("delay start");
        delay();
        console.log("delay end");
        console.log("function end");
      }
      func();

결과

결론적으로 비동기 함수를 먼저 실행하고 바로 다음 줄에서 동기 함수를 실행하면, 비동기 함수가 먼저 실행 중이더라도 동기 함수가 콜 스택으로 들어옴으로써 콜스택이 차게 되고,  콜 스택이 비어있지 않게 되므로 비동기 함수의 동작이 멈추고 동기 함수의 동작을 먼저 수행하게 된다. 

  • 코드의 순서에 따라 함수를 콜 스택에 쌓인다.
  • 비동기 함수는 webapis로 이동하고 태스크 큐에서 기다린다.
  • 동기 함수가 모두 실행되어 콜 스택이 비어 있으면 태스크 큐의 비동기 함수가 차례대로 실행된다.
  • 비동기 함수가 실행 중일 때 동기 함수로 인해 콜 스택이 차면 비동기 함수의 동작이 멈추고 동기 함수가 먼저 처리된다.
  • DOM 변경에 따른 리렌더링 행위는 렌더링 큐에서 따로 관리하며 비동기 함수처럼 콜스택이 비었을 때만 실행되고, 비동기함수의 콜백 보다는 높은 우선순위를 가진다.

 

 

Browser Rendering Queue in-depth.

An in-depth full explanation on how browsers manage to push pixels on the screen, and how to improve performance.

medium.com

한글자료:

 

이벤트 루프와 매크로태스크, 마이크로태스크

 

ko.javascript.info

 

Event Loop로 화면멈춤 현상을 막아보자!

예를 들어, TodoApp을 만든다고 했을 때 (그럴리는 없겠지만) 서버로부터 십만 개의 Todo들을 받아 한 번에 렌더링한다고 해봅시다. 아래 예시는 실제로 서버로부터 십만 개의 데이터를 받지는 않고

medium.com

 

자바스크립트와 이벤트 루프

자바스크립트는 큰 특징 중 하나는 '단일 스레드' 기반의 언어라는 점이다. 스레드가 하나라는 말은 곧, 동시에 하나의 작업만을 처리할 수 있다라는 말이다. 하지만 실제로 자바스크립트가 사

ui.toast.com