[Javascript] 동기와 비동기

seongminn·2022년 11월 9일
0

JavaScript

목록 보기
4/5
post-thumbnail
post-custom-banner

동기와 비동기

동기

자바스크립트는 동기식 언어이다. 동기는 한 번에 하나의 작업을 실행하는 것을 뜻하고, 이는 싱글 스레드에서 코드가 동작하기 때문이다.

동기식 작업은 순서가 정해져 있기 때문에 설계가 단순해지고 안전하다는 장점이 있다. 다만, 자원을 효율적으로 사용하지 못한다는 단점이 있다.

비동기

반대로, 비동기는 요청의 응답에 관계 없이 다음 작업을 기다리지 않고 실행하는 것을 말한다. 멀티 스레드 작업 환경에서는 요청을 처리할 수 있는 작업 단위가 여러 개 존재하기 때문에 하나의 태스크의 실행 결과를 기다리지 않고 다음 태스크를 실행할 수 있다.

비동기식 작업 방식은 요청에 대한 응답이 늦어지더라도 응답 결과를 기다리는 동안 다음 작업을 실행할 수 있다. 하지만, 설계가 복잡해지고, 요청 결과가 다음 요청 결과에 영향을 미치는 경우 코드가 의도와는 다르게 동작할 수 있다.


자바스크립트 엔진

자바스크립트 엔진은 자바스크립트의 코드를 분석하고 실행하는 인터프리터를 말한다. 자바스크립트 엔진은 주로 웹 브라우저에서 사용된다. 웹 브라우저에 내장되어 있기 때문에 웹 브라우저에서 곧바로 코드를 해석하고 실행할 수 있다.

구조

  1. 메모리 힙
    객체, 함수, 참조 타입과 같은 복잡한 데이터를 저장하는 공간

객체는 변수이기 때문에 원시 데이터와 달리 데이터의 크기가 정해져 있지 않다. 할당 메모리의 공간 크기는 런타임 시에 결정되므로, 은 결국 객체를 저장하는 넓은 영역의 공간이자 구조화되지 않은 영역이다.

  1. 콜스택
    코드를 실행할 때, 코드의 실행 순서를 기록하고 순서대로 코드가 실행될 수 있도록 도와주는 스택

자바스크립트 엔진은 하나의 콜스택만을 사용하기 때문에 최상위 실행 컨텍스트가 실행되기 전까지는 다른 어떤 태스크도 실행되지 않는다.


자바스크립트의 비동기 실행

앞서 살펴본 것처럼 자바스크립트는 동기식 언어다. 그렇다면 어떻게 비동기를 처리하는가? 이를 알기 위해서는 브라우저에 대해서도 알고 있어야 한다. 브라우저는 자바스크립트 엔진 외에도 태스크 큐, 이벤트 루프, Web API를 갖고 있다. 태스크 큐는 비동기 함수의 콜백 함수를 임시 보관하고, 이벤트 루프는 자바스크립트 엔진의 콜스택이 비었는지, 태스크 큐에 대기하고 있는 함수가 있는지 확인한다. 콜스택이 비었고, 태스크 큐에 함수가 있다면 이벤트 루프는 큐의 함수 하나를 콜스택에 보내 실행시킨다.

이 때, 이벤트 루프는 항상 함수 하나만을 가져 온다.

비동기 실행에서 브라우저의 역할

브라우저 엔진은 자바스크립트 엔진과 다르게 멀티 스레드로 동작한다. 멀티 스레드로 동작하는 브라우저가 Web API를 제공하여 비동기 실행을 구현할 수 있는 것이다.

브라우저 Web API

브라우저 환경에서 제공하는 API를 말한다.
브라우저 Web API는 작성된 함수의 콜백 함수를 받고 특정 동작을 수행한 뒤 이 콜백 함수를 큐로 넘겨주는 역할을 한다.

  1. 자바스크립트 엔진이 코드를 실행하는 과정을 정리하자면 다음과 같다.1. 자바스크립트 엔진이 실행 컨택스트를 차례대로 콜스택에 쌓는다.
  2. 쌓인 코드들이 순서에 따라 콜스택을 빠져나가며 실행된다.
  3. 코드 중 비동기 함수. 즉, setTimeout 또는 setInterval, 이벤트 핸들러와 같은 Web API 함수를 만나면 자바스크립트 엔진이 이들을 Web API로 보낸다.
  4. Web API는 전달받은 함수의 콜백 함수를 꺼내어 태스크 큐로 보낸다. 이때, 딜레이 인자가 있는 경우는 딜레이 시간만큼 Web API에서 대기하게 된다.
  5. 콜스택에 있던 함수들의 실행이 모두 완료되어 빈 공간이 되면, 이벤트 루프가 태스크 큐에서 대기 중인 콜백함수 하나를 콜스택에 넣어 바로 실행시킨다.
  6. 다시 콜스택이 비면 이벤트 루프가 태스크큐가 빌 때까지 위의 동작을 반복한다.

결국 자바스크립트 엔진은 동기식 동작을 하지만, 브라우저 엔진이 자바스크립트 엔진에서 전달 받은 일을 독립적으로 수행하기 때문에 자바스크립트의 코드가 비동기 방식으로 처리되는 것처럼 보인다.

브라우저 환경에서의 큐

  1. 마이크로태스크 큐
    콜스택이 비어 있을 때 가장 먼저 이벤트 루프가 확인하는 큐로, Promisethen, catch, finally가 있다.

  2. 태스크 큐
    마이크로태스크 큐에 함수가 없을 때 이벤트 루프가 확인하는 큐로, setTimeout, setInterval, addEventListner 등이 있다.

큐를 나누는 이유

Promise로 데이터를 전달 받아 웹 페이지에 반영하는 코드가 있을 때, 큐의 우선순위가 존재하지 않는다면 setTimeout()이 먼저 실행되어 원하는 데이터를 반영하기 전 페이지를 띄워야 하는 불상사가 발생할 수 있다.

렌더링 엔진

브라우저는 HTMLCSSParsing하여 브라우저 화면을 표시하는 기능을 담당하는 렌더링 엔진을 제공한다.

브라우저의 렌더링 과정

  1. HTML을 Parsing하여 DOM 생성
  2. CSS를 Parsing하여 CSSOM 생성
  3. Render Tree 생성
  4. Render Tree 배치(Layout)
  5. Render Tree 그리기(Paint)

이 때, 렌더링 작업은 이벤트 루프에 의해 태스크 큐에 저장된다. 이말은 즉슨, 렌더링이 다른 콜백 함수보다 높은 우선순위를 갖고 있지만, setTimeout의 콜백 함수가 끼어 들거나 Promise 콜백 함수보다 낮은 우선순위를 갖는다는 문제가 있다. 그러면 다른 콜백 함수가 처리되는 동안 렌더링 엔진이 작동하지 못하고, 화면은 버벅이는 것처럼 보일 수 있다. 그래서 유동적인 UI를 위해서라면 이벤트 루프를 막지 말아야 한다.

예시

// HTML
<body>
		<button>Click me!</button>
    <div id="loading"></div>
    <div id="result"></div>
</body>

// Javascript
const testButton = document.querySelector("button");
const loadingBox = document.querySelector("#loading");
const resultBox = document.querySelector("#result");
let result;

function isLoading() {
  loadingBox.innerHTML = "로딩 중";
}

function someProcessTakeLongTime() {
  for (let i = 0; i < 100000000; i++) {
    result += i;
  }
}

function hideIsLoading() {
  loadingBox.innerHTML = "";
}

function showResult() {
  resultBox.innerHTML = "결과입니다.";
}

testButton.addEventListener("click", () => {
  isLoading();
  someProcessTakeLongTime();
  hideIsLoading();
  showResult();
});

위 코드에서 isLoading()이 실행되어 "로딩 중"이라는 문구를 띄운 뒤, someProcessTakeLongTime() 함수를 실행할 것이라 생각하는 분들은 반성해야 한다(그게 바로 나다..).

함께 살펴보도록 하자. 먼저 isLoading 함수가 콜스택으로 이동하고 실행된다. 그러면isLoading 함수에서 문구를 변경하기 위해 렌더링 요청을 보내는데, 렌더링 요청은 비동기적으로 동작하기 때문에 태스크 큐로 이동한다. 이제 콜스택이 비워지고 someProcessTakeLongTime() 함수가 콜스택으로 이동하고 실행된다. 나머지 함수가 실행되기까지 렌더링 요청에 대한 처리는 미뤄지고 결국 "로딩 중"이란 문구는 확인할 수 없다.

이를 해결하기 위해 다음과 같이 코드를 작성할 수 있다.

testButton.addEventListener("click", () => {
  isLoading();
  setTimeout(() => {
    someProcessTakeLongTime();
    hideIsLoading();
    showResult();
  }, 0);
});

isLoading 함수가 콜스택으로 이동하여 실행된다. 그러면 렌더링 요청이 태스크 큐로 이동하고, 콜스택이 비워진다. 다음으로 setTimeout() 함수가 콜스택으로 이동하고 실행되는데, setTimeout은 비동기적으로 동작하기 때문에 콜백 함수는 태스크 큐로 이동한다. 이제 콜스택이 비워지고 태스크 큐에서 대기하던 함수들이 실행되므로 "로딩 중"이라는 문구를 확인할 수 있다.


--

참고 사이트

🙇🏻‍♂️ https://paullabworkspace.notion.site/abfb023f97954ab8ae9d3ec8685a8415

profile
돌멩이도 개발 할 수 있다
post-custom-banner

0개의 댓글