📝

비동기 처리

1. 동기와 비동기

프로그래밍을 공부해본 사람은 한 번 쯤 “싱글 스레드”, “멀티 스레드”, “동기”“비동기”같은 알쏭달쏭한 단어들을 들어보게 된다. 자바스크립트를 학습하면서도 비동기 처리라던가, 비동기 통신이라던가 하는 말들을 접하게 되는데 대체 이 비 동기라는 것이 무엇을 의미하는 것이고, 개발에 있어 어떤 고려사항이 있는 걸까?
이 장에서는 먼저 동기비동기가 무엇을 의미하는지 알아보고, 자바스크립트 상에서의 그 예시를 살펴보자.

1.1. 동기

자바스크립트를 학습하면서 “비동기”와 관련된 테크닉을 많이 접하게 되지만, 의외로 자바스크립트는 동기식 언어이다. 동기(Syncronous)순차적인 흐름을 가진다. 하나의 작업이 실행되는 동안, 뒤의 다른 작업들은 그대로 멈춘 채 자신의 차례가 오기까지를 기다린다.
손님이 새우버거를 먹기 위해 패스트푸드점에 방문하는 상황을 떠올려보자. 손님은 카운터로 가서 점원에게 “새우버거 주세요”라고 주문하며 카드를 건넬 것이다. 점원은 주문을 듣고 ‘새우 재고가 충분히 있었던가?’ 잠시 생각해본 뒤 햄버거 값을 결제한다. 그동안 손님은 결제가 완료될 때 까지 카운터에서 멍하니 기다릴 것이고, 결제가 완료되면 주문번호가 적힌 영수증과 손님의 카드를 점원이 돌려주며 주문이 완료된다.
1-1. 동기식 방식에서의 처리 흐름1-1. 동기식 방식에서의 처리 흐름
1-1. 동기식 방식에서의 처리 흐름
이 일련의 과정 중에서 동기식 프로그램의 특징을 엿볼 수 있다. 손님이 주문을 요청(새우버거)하면, 점원은 그 요청에 대한 처리를 거쳐 응답(영수증과 카드)을 내놓는다. 그리고 손님은 응답이 돌아오기까지 카운터를 떠나지 않고 기다린 뒤, 응답을 받고 나서야 다음 행동을 할 수 있다.
한 번에 하나의 태스크를 수행하는 것은 싱글 스레드(Single Thred)에서 코드가 동작할 경우이다. 요청에 대한 처리를 할 수 있는 주체가 단 하나만 존재하기 때문에 위와 같이 순차적인 흐름을 가질 수 밖에 없다.
싱글 스레드를 사용하는 동기식의 작업은, 작업의 순서가 보장되어있어 설계가 단순하고 안전하다는 장점이 있다. 하지만 응답이 돌아오기까지 다음 동작으로 넘어가지 못하기 때문에, 자원을 효율적으로 사용하지 못한다는 점이 단점.

1.2 비동기

비동기(Astnchronous)는 반대로, 어떠한 요청을 보내면 해당 요청의 응답에 관계 없이 바로 다음 동작이 실행된다. 즉, 실행중인 작업이 끝나기를 기다리지 않고 바로 다음 작업으로 넘어갈 수 있다는 뜻이다.
아까의 패스트푸드점에 이번에는 점장이 방문했다. 매장에 손님이 없는 것을 확인하고는 점원들에게 오늘 해야할 업무를 지시내린다. 점원1에게는 청소를, 점원2에게는 설거지를 해 달라고 요청한 뒤 점장은 구석에서 식사를 한다. 그럼 점장이 식사를 하는 동안 점원들은 각각 청소와 설거지를 하며 매장을, 접시를 구석 구석 닦는다.
1-2. 동기식 방식에서의 처리 흐름1-2. 동기식 방식에서의 처리 흐름
1-2. 동기식 방식에서의 처리 흐름
점원들의 작업은 비동기식으로 처리되고 있다. 점장은 점원1에게 요청을 보낸 후, 응답을 받지 않은 상태에서 점원2에게 다시 다른 요청을 보낸다. 둘 모두 완료 보고라는 응답이 돌아오지 않았지만 점장은 신경쓰지 않고 다음 작업으로 넘어갈 수도 있다.
멀티 스레드(Multi Thred) 방식에서는 요청을 처리할 수 있는 작업 단위가 여러 개이기 때문에, 하나의 태스크가 끝나기를 기다리지 않아도 다음 실행을 할 수 있게 된다.
멀티 스레드를 사용하는 비동기식의 작업은, 요청에 대한 응답이 늦어지더라도 그 시간동안 다른 작업을 할 수 있다는 장점이 있다. 하지만 싱글 스레드와 비교하면 설계가 복잡한데다가, 만약 비동기식으로 보낸 요청의 결과가 다음 실행에 영향을 미치는 경우에는 코드가 개발자의 의도와 다르게 동작할 수 있음을 주의해야 한다.
 

1.2.1 비동기식 실행의 예

// 동기식 처리 코드 const 예보 = '👮🏻‍♀️: 폭풍이 몰려오고 있으니 조심하세요🚨'; const 도망 = '🏃🏻‍♀️: 얼른 도망가야겠다... 헉 벌써 왔어!!😱'; const 폭풍 = '👻: 하하 이미 도착했다고~!💨'; console.log(예보); console.log(폭풍); console.log(도망);
notion imagenotion image
// 비동기식 처리 코드 const 예보 = '👮🏻‍♀️: 폭풍이 몰려오고 있으니 조심하세요🚨'; const 도망 = '🏃🏻‍♀️: 얼른 도망가야겠다!! (후다닥)'; const 폭풍 = '👻: 하하 내가 왔다💨 ... 어라 늦었네..😤'; console.log(예보); setTimeout(() => { console.log(폭풍); },1000); console.log(도망);
notion imagenotion image

2. 자바스크립트의 비동기

2.1 자바스크립트 엔진이란?

자바스크립트의 코드를 해석하고 분석하고 실행하는 인터프리터, 즉 해석기이다. 주로 웹 브라우저에서 사용된다(자바스크립트 엔진은 브라우저 안에 내장되어 있다). 엔진에 의해 해석하기 때문에 컴파일 과정이 불필요하다. 웹 브라우저에서 곧바로 해석하고 실행한다.

2.2 브라우저 별 엔진 종류

자바스크립트 엔진은 일반적으로 브라우저 업체들이 직접 개발하며, 브라우저별로 사용하는 엔진이 다르다.
  • V8 : 자바스크립트 대표 엔진이다. 구글에서 개발했으며, 크롬과 노드.js에서 사용한다.
  • 스파이더몽키 : 최초의 자바스크립트 엔진이다. 현재 모질라 재단에서 관리하며 파이어폭스에서 사용한다.
  • 차크라 : 마이크로소프트가 개발했고, 익스플로어와 엣지에서 사용한다.
  • 웹킷 : 애플에서 개발한 오픈소스 엔진이고, 사파리에서 사용한다.

2.3 자바스크립트 엔진 구조

자바스크립트 엔진은 크게 메모리 힙과 콜 스택으로 나눌 수 있다. 이벤트 루프, 콜백 큐와 같은 엔진 외부 요소들과 함께 코드를 실행한다.
notion imagenotion image

2.3.1 메모리 힙

객체, 함수, 참조 타입같은 복잡한 데이터를 저장하는 공간이다.
💡
자바스크립트의 참조 타입으로는 Array, Function, Object 등이 있다.
일반적으로 메모리에 데이터를 저장할 때, 저장할 메모리의 공간 크기를 정한다. 객체는 변수이기 때문에 원시 타입의 데이터와는 달리 데이터의 크기가 정해져 있지 않다. 할당 메모리의 공간 크기는 런타임에 의해 결정하기 때문에 객체를 저장하는 공간, 즉 힙은 넓은 영역의 공간이자 구조화되지 않은 영역이다.

2.3.2 콜 스택

코드를 실행 할 때 코드 안의 실행 순서를 기록하고 순서대로 코드가 실행될 수 있게 도와주는 스택이다. 자세히 말하자면 함수를 호출 시 함수 실행 컨텍스트가 스택에 쌓이면서 순서대로 실행된다. 실행 중인 실행 컨텍스트, 즉 최상위 실행 컨텍스트가 종료되기 전까지 그 어떤 태스크도 실행하지 않는다. 자바스크립트 엔진은 하나의 콜 스택을 사용하기 때문이다.
💡
실행 컨텍스트란? 브라우저가 HTML 문서를 해석할 때, <script> 태그로 감싸진 자바스크립트 코드 또는 onclick과 같은 속성을 가진 태그를 만나면 이것을 자바스크립트에 보낸다. 그리고 자바스크립트 엔진이 브라우저가 넘겨준 코드를 변환하고 실행시키기 위해 특별한 환경을 구성하는데, 이것을 바로 실행 컨택스트라 한다.
 
let 참깨빵 = () => { 순쇠고기패티(); console.log('Call 참깨빵'); } let 순쇠고기패티 = () => { 특별한소스(); console.log('Call 순쇠고기패티'); } let 특별한소스 = () => { console.log('Call 특별한소스'); } // 출력 결과 Call 특별한소스 Call 순쇠고기패티 Call 참깨빵
예제 1-1
예제 예제
예제
예제를 보면 참깨빵을 먼저 호출하므로 콜 스택에 참깨빵이 쌓인다. 참깨빵에서 순쇠고기패티를 호출하므로 순쇠고기패티가 쌓이고, 순쇠고기패티에서 특별한소스를 호출하므로 특별한소스가 가장 마지막에 쌓인다.
콜 스택에서 가장 상단에 놓여진 함수를 먼저 해결하기 때문에 Call 특별한소스, Call 순쇠고기패티, Call 참깨빵 순서로 출력된다.
 
💡
스택 오버플로우(Stack overflow)란? 작업이 완료되지 않고 콜 스택 위로 데이터가 쌓이기만 하는 경우, 정해진 공간의 용량을 넘어서게 될 때 나타나는 현상이다. 주로 자기 자신을 반복하여 호출하는 경우에 나타난다.
 

2.4. 자바스크립트는 어떻게 비동기 실행이 가능할까?

자바스크립트 실행을 이해하려면 우리는 브라우저에 대해서도 어느 정도 알고 있어야 한다. 자바스크립트 코드가 비동기 실행으로 보여지게 만들어 주는 것이 바로 브라우저이기 때문이다. 브라우저는 자바스크립트 엔진 외에도 태스크 큐이벤트 루프 그리고 Web API를 가지고 있다. 태스크 큐는 비동기 함수의 콜백 함수가 임시로 보관되는 공간이다. 이벤트 루프는 자바스크립트 엔진의 콜스택이 비어있는지, 태스크 큐에 대기 중인 함수가 있는지 반복해서 확인하는 프로그램이다.
만약 태스크 큐에 함수가 남아있고 콜스택이 비어있다면, 이벤트 루프태스크 큐에 있는 함수 하나를 꺼내서 콜스택에 보내 실행시킨다. 이때 이벤트 루프는 여러 개의 함수를 한꺼번에 가져오지 않고 단 하나의 함수만 가져온다.

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

자바스크립트 엔진은 싱글 스레드로 동작하지만 브라우저 엔진은 멀티 스레드로 동작한다. 멀티 스레드로 동작하는 브라우저가 제공하는 Web API를 함께 사용하여 비동기 동작을 구현할 수 있는 것이다. 예를 들어 setTimeout 함수로 작성된 코드가 콜 스택으로 들어오면 타이머 작동과 타이머 완료 후 콜백 함수를 큐로 보내주는 역할을 브라우저 Web API가 해주는 것이다. 멀티 스레드로 동작하는 브라우저 엔진이 자바스크립트에서 작성된 비동기 함수의 동작을 처리해주기 때문에 비동기 동작을 구현할 수 있다.

2.4.2 브라우저 Web API란?

API는 복잡한 코드를 추상화하여 개발자들이 쉽게 사용할 수 있도록 만든 것이다. 예를 들어, 우리는 차가 어떻게 만들어지는지, 차의 엔진 시스템은 어떻게 동작하는지 전혀 모르지만 차에 시동을 걸고 기름을 넣고 운전할 수 있다. 이처럼 브라우저 환경에서 제공하는 API를 브라우저 Web API라고 한다. 위에서 언급한 것 처럼, 브라우저 Web API는 작성된 함수의 콜백 함수를 받고 특정 동작을 수행한 뒤 콜백 함수를 큐로 보내주는 역할로써 작동한다.
브라우저에서 제공하는 API의 대표적인 예로는 setTimeout , DOM API, 서버로부터 데이터 받아오기(ajax통신), Canvas 또는 WebGL 또는 오디오나 비디오 API 등이 있다
notion imagenotion image
자바스크립트 엔진이 코드를 읽어 실행하는 과정을 좀 더 자세히 살펴보자.
  1. 자바스크립트 엔진이 실행 컨택스트를 차례대로 콜스택에 쌓는다.
  1. 쌓인 코드들이 순서에 따라 콜스택을 빠져나가며 실행된다.
  1. 코드 중 비동기 함수. 즉, setTimeout 또는 setInterval, 이벤트 핸들러와 같은 Web API 함수를 만나면 자바스크립트 엔진이 이들을 Web API로 보낸다.
  1. Web API는 전달받은 함수의 콜백 함수를 꺼내어 태스크 큐로 보낸다. 이때, 딜레이 인자가 있는 경우는 딜레이 시간만큼 Web API에서 대기하게 된다.
  1. 콜스택에 있던 함수들의 실행이 모두 완료되어 빈 공간이 되면, 이벤트 루프가 태스크 큐에서 대기 중인 콜백함수 하나를 콜스택에 넣어 바로 실행시킨다.
  1. 다시 콜스택이 비면 이벤트 루프가 태스크큐가 빌 때까지 위의 동작을 반복한다.
 
정리하자면, 자바스크립트 엔진 자체는 콜스택에서 한 번에 하나의 일을 수행하는 동기적 동작을 하지만, 브라우저의 태스크 큐, 이벤트 루프, Web API가 자바스크립트 엔진에서 전달받은 일을 엔진 밖에서 독립적으로 수행하기 때문에 브라우저에서 실행되는 자바스크립트 코드가 비동기 방식으로 처리되는 것처럼 보인다.

2.4.3 렌더링 엔진이란?

브라우저는 Web API외에도 렌더링 엔진을 제공한다. 렌더링 엔진은 HTML과 CSS를 Parsing하여 요청한 웹페이지를 시각적으로 변환하는 기능을 담당하는 엔진이다. 레이아웃 엔진이라고도 부르며 예로는 크롬의 블링크, 파이어폭스의 게코, 사파리의 웹킷이 있다.

3. 이벤트 루프

지금까지 자바스크립트 코드가 브라우저가 제공하는 이벤트루프와 Web API를 통해 비동기적으로 실행이 되는 원리를 살펴보았다. 이처럼 이벤트루프는 동기적으로 실행되는 자바스크립트 엔진과 비동기적으로 실행되는 브라우저를 상호 연동하기 위해 사용되는 장치로 콜 스택과 큐를 반복해서 감시하는 루프 역할을 한다.
이 때 유의해야 할 점은 이벤트루프가 큐에서 함수를 꺼낼 때에도 우선순위가 존재한다는 사실이다. 이는 큐의 우선순위와 관련되어 있는데, 이를 제대로 인지하지 못한다면 예상치 못한 순서로 코드가 동작하여 난감한 일이 발생할 수 있다. 원하는 순서로 코드가 동작하게 하려면 이벤트 루프의 동작 원리와 순서에 대해 인지하고, 이에 입각하여 동기, 비동기 코드를 작성해야 한다.
다음으로 이벤트루프의 동작 흐름과 큐의 우선순위에 대해 자세하게 알아보자.

3.1. 이벤트 루프의 동작

이벤트 루프는 현재 실행 중인 태스크가 없는지, 태스크 큐에 태스크가 있는지를 확인하면서 콜 스택이 비워지면 태스크 큐에 있는 콜백 함수를 순차적으로 콜 스택으로 옮겨오는 일을 한다.

3.1.1 이벤트 루프의 동작 예제

아래 예시 코드와 그림을 통해 더 자세한 동작 흐름을 알아보자.
function delay() { for(let i = 0; i < 1000000; i++); } function 도망() { delay(); console.log('🏃🏻‍♀️: 얼른 도망가야겠다!! (후다닥)'); } function 예보() { delay(); console.log('👮🏻‍♀️: 폭풍이 100ms 뒤에 몰려온다고 합니다. 조심하세요🚨'); 도망(); } function 폭풍() { console.log('👻: 하하 내가 왔다💨 ... 어라 늦었네..😤'); } setTimeout(폭풍, 100); 예보();
예제 3-1
  1. 먼저 setTimeout함수가 콜 스택에 추가된다.
    1. 예제 3-2-1 렌더링 흐름예제 3-2-1 렌더링 흐름
      예제 3-2-1 렌더링 흐름
  1. 콜 스택에 있던 setTimeout함수는 브라우저에 타이머 이벤트를 요청한 후 콜 스택에서 제거되고, 다음 코드인 예보함수가 콜 스택에 추가된다.
    1. 예제 3-2-2 렌더링 흐름예제 3-2-2 렌더링 흐름
      예제 3-2-2 렌더링 흐름
  1. 예보함수가 내부적으로 실행하는 도망함수를 콜 스택에 추가하고 함수에 작성된 코드가 실행된다. 동시에 타이머를 완료한 setTimeout함수는 콜백 함수를 태스크 큐에 추가한다.
    1. 예제 3-2-3 렌더링 흐름예제 3-2-3 렌더링 흐름
      예제 3-2-3 렌더링 흐름
  1. 예보함수와 도망함수가 동작을 마치게 되면 콜 스택이 비어있게 된다.
    1. 예제 3-2-4 렌더링 흐름예제 3-2-4 렌더링 흐름
      예제 3-2-4 렌더링 흐름
  1. 이벤트 루프는 콜 스택이 비어있는 것을 확인하고, 폭풍함수를 콜 스택에 추가한다.
    1. 예제 3-2-5 렌더링 흐름예제 3-2-5 렌더링 흐름
      예제 3-2-5 렌더링 흐름
  1. 폭풍함수의 코드가 모두 실행되면 예제 코드가 종료되고, 콘솔창에 보이는 결과는 다음과 같다.
👮🏻‍♀️: 폭풍이 100ms 뒤에 몰려온다고 합니다. 조심하세요🚨 🏃🏻‍♀️: 얼른 도망가야겠다!! (후다닥) 👻: 하하 내가 왔다💨 ... 어라 늦었네..😤
이벤트 루프 동작 흐름으로 인해 폭풍함수가 예보함수보다 먼저 호출되었는데도 도망치는 데 성공한 것을 확인할 수 있다.

3.2 브라우저 환경에서의 큐

3.2.1 큐(queue)란?

콜백 함수를 콜 스택으로 이동시키기 전에 콜백 함수가 대기하는 공간이며, 선입선출(First In First Out) 방식으로 콜백 함수가 저장되고 콜 스택으로 이동된다.

3.2.2 큐의 종류

큐는 태스크 큐(매크로태스크 큐)와 마이크로태스크 큐로 나뉜다.

3.2.3 마이크로태스크 큐란?

마이크로태스크 큐는 콜 스택이 비어 있을 때, 가장 먼저 이벤트 루프가 확인하는 큐이며, 저장되는 대표적인 콜백 함수는 Promisethen, catch, finally 가 있다.
💡
Promise 후속 처리 메소드 Promise 후속 처리 메소드는 then , catch , finally 를 의미하며, 마이크로태스크 큐에는 후속 처리 메소드의 콜백 함수가 저장된다.

3.2.4 태스크 큐란?

태스크 큐(매크로태스크 큐)는 마이크로태스크 큐에 콜백 함수가 없다면 이벤트 루프가 확인하는 큐이며, 태스크 큐에 저장되는 대표적인 콜백 함수는 setTimeout, setInterval, addEventListener 등이 있다. 렌더링 작업 또한 해당 큐에 저장된다.
💡
큐를 나누는 이유 마이크로태스크 큐와 태스크 큐로 나누어 우선순위를 정해 원활한 페이지 동작을 가능케 하는데 목적이 있다. 예를 들어, Promise로 데이터를 전달 받아 웹 페이지에 반영하는 코드가 있을 때, setTimeoutPromise 보다 먼저 실행된다면 페이지에 반영해야 하는 데이터를 늦게 전달 받아 원활한 웹 페이지 동작을 기대할 수 없다. 이러한 문제를 예방하기 위해 큐를 나누고 우선순위를 정하는 것이 필요하다.

3.2.5 태스크 큐 VS 마이크로태스크 큐 예제

// setTimeout의 딜레이 0초 setTimeout(function(){ console.log("👻: 하하 내가 왔다💨 ... 어라 늦었네..😤") }, 0) Promise.resolve() .then(()=>console.log('🏃🏻‍♀️: 얼른 도망가야겠다!! (후다닥')) console.log("👮🏻‍♀️: 폭풍이 몰려오고 있으니 조심하세요🚨");
Promise.resolve() .then(()=>console.log('🏃🏻‍♀️: 얼른 도망가야겠다!! (후다닥)') console.log("👮🏻‍♀️: 폭풍이 몰려오고 있으니 조심하세요🚨"); // setTimeout의 딜레이 0초 setTimeout(function(){ console.log("👻: 하하 내가 왔다💨 ... 어라 늦었네..😤") }, 0)
위 두 예제의 콘솔 출력 결과는 동일하다. 그 이유는 PromisesetTimeout은 비동기 함수이기 때문에 👮🏻‍♀️: 폭풍이 몰려오고 있으니 조심하세요🚨가 먼저 출력 되고 큐 우선순위에 따라 🏃🏻‍♀️: 얼른 도망가야겠다!! (후다닥), 👻: 하하 내가 왔다💨 ... 어라 늦었네..😤의 순서로 출력된다.
큐 우선순위큐 우선순위
큐 우선순위
위 이미지는 👮🏻‍♀️: 폭풍이 몰려오고 있으니 조심하세요🚨가 출력된 이후의 이미지이며, 다음과 순서로 동작한다.
  1. 콜 스택이 비어있어 선순위 큐인 마이크로태스크 큐에 then 콜백 함수를 이벤트 루프가 콜 스택으로 이동시키고 콜 스택에서 해당 콜백 함수가 실행된다.
  1. 콜 스택과 마이크로태스크 큐에 콜백 함수가 없기 때문에 태스크 큐에 있는 setTimeout 콜백 함수를 이벤트 루프가 콜 스택으로 이동시키고 콜 스택에서 해당 콜백 함수가 실행된다.

3.3. 렌더링 엔진

3.3.1 렌더링 엔진이란?

목차 2에서 살펴본 것과 같이 브라우저는 HTML과 CSS를 Parsing하여 브라우저 화면을 표시하는 기능을 담당하는 렌더링 엔진을 제공한다. 브라우저의 렌더링 과정은 다음과 같다.
 
  1. HTML을 Parsing하여 DOM 생성
  1. CSS를 Parsing하여 CSSOM 생성
  1. Render Tree 생성
  1. Render Tree 배치 (Layout)
  1. Render Tree 그리기 (Paint)
 
화면이 최초로 렌더링 되는 경우 위 과정을 전부 실행하며, 화면에 업데이트가 필요할 때에 필요한 과정을 다시 실행한다. 또한 렌더링은 매 16.6 밀리세컨드, 즉 1초에 60프레임을 렌더한다.
 

3.3.2 렌더링 엔진과 이벤트 루프

렌더링은 이벤트 루프에서 다른 일반적인 콜백 함수에 비해 더 높은 우선순위를 가진다. 하지만 렌더링도 하나의 콜백 함수처럼 동작하기 때문에 16.6 밀리세컨드 마다 렌더링 요청을 하고 만약 콜 스택에 코드가 있다면 렌더링은 막히게 된다.
 
다음으로 이벤트 루프에서 렌더링 엔진이 어떻게 동작하는지 알아보자.

3.3.3 렌더링 흐름

  1. 매 16.6 밀리세컨드 마다 큐에 렌더가 들어간다. 콜 스택에 다른 코드가 실행되고 있다면 렌더는 막히게 된다.
    1. 이미지 3-3-1 렌더링 흐름이미지 3-3-1 렌더링 흐름
      이미지 3-3-1 렌더링 흐름
  1. 콜 스택이 비워지면 렌더링을 한다.
    1. 이미지 3-3-2 렌더링 흐름이미지 3-3-2 렌더링 흐름
      이미지 3-3-2 렌더링 흐름
  1. 렌더링은 비동기로 작동하기 때문에 호출을 기다리는 setTimeout 의 콜백 함수가 끼어들 수 있는 기회를 가진다.
    1. 이미지 3-3-3 렌더링 흐름이미지 3-3-3 렌더링 흐름
      이미지 3-3-3 렌더링 흐름
  1. 호출을 기다리던 setTimeout 의 콜백 함수가 호출 되고 콜 스택이 비워지면 렌더링을 한다.
    1. 이미지 3-3-4 렌더링 흐름이미지 3-3-4 렌더링 흐름
      이미지 3-3-4 렌더링 흐름
 

3.3.4 이벤트 루프에서 렌더링 엔진의 우선순위

앞서 살펴 보았듯이 렌더링은 다른 콜백 함수에 비해 더 높은 우선순위를 가진다. 하지만 렌더링 엔진은 태스크큐에 저장되기 때문에 마이크로태스크 큐에 저장된 Promise 콜백 함수보다 우선순위가 낮다.
이미지 3-4 렌더링 우선순위이미지 3-4 렌더링 우선순위
이미지 3-4 렌더링 우선순위
따라서 위 예제의 경우 마이크로태스크 큐에 있는 Promise 콜백 함수가 먼저 실행될 것이다.
마이크로태스크 큐의 동작시간이 길어진다면 렌더링 엔진이 작동하지 못하고 화면이 버벅이는 것 처럼 보이게 된다 . 렌더링이 막힌 시간 동안 클릭 이벤트나 다른 동작을 할 수 없기 때문이다.
이처럼 부하가 걸리는 코드나 동기식 루프 때문에 렌더링이 막히게 되면 브라우저가 제대로 작동하지 못하고 느려지는 현상이 발생할 수도 있다. 따라서 유동적인 UI를 위해서라면 이벤트 루프를 막지 말아야 한다.

3.4. setTimeout(fn, 0)을 활용한 예제

목차 3에서 설명한 이벤트 루프의 동작 원리를 활용해 프론트엔드 환경에서는 종종 특정 상황에서 특정한 타이밍을 정해 코드가 동작하도록 setTimeout(fn, 0)함수가 사용된다.
이에 대한 예제로 동작 시간이 오래 걸리는 함수를 실행 시키고 이에 대한 결과를 보이는 버튼이 있다고 가정하자. 이때, 사용자 경험을 향상시키기 위해 시간이 오래 걸리는 작업이 실행하는 동안 “로딩 중” 이라는 메세지를 보여주도록 할 것이다.
예제에 사용될 각 함수의 명세는 다음과 같다.
  • isLoading: “로딩 중” 텍스트를 화면에 표시해주는 함수
  • someProcessTakeLongTime: 시간이 오래 걸리는 작업을 하는 함수
  • hideIsLoadin: “로딩 중” 텍스트를 화면에서 지우는 함수
  • showResult: someProcessTakeLongTime의 결과를 보여주는 함수
<body> <button>Click me!</button> <div id="loading"></div> <div id="result"></div> </body>
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(); });
예제 3-8
위와 같이 코드를 작성하게 된다면, isLoading 함수가 “로딩 중” 이라는 텍스트를 표시할 것 같지만, “로딩 중” 텍스트는 화면에 표시되지 않고, 잠시 브라우저가 멈췄다가 someProcessTakeLongTime함수가 동작을 마치면 다음과 같이 해당하는 결과값만 보여준다.
예제 3-9-1예제 3-9-1
예제 3-9-1
예제 3-9-2예제 3-9-2
예제 3-9-2
이러한 문제가 발생하는 이유는 “로딩 중” 텍스트를 표시하기 위해서는 isLoading 함수 내부에서 렌더링 요청을 보내야 한다. 이때 렌더링 요청은 비동기적으로 동작하기 때문에 태스크 큐로 들어가게 되고, 콜 스택이 비워지면서 다음 코드인 someProcessTakeLongTime함수가 콜 스택에 추가되어 위의 그림과 같은 상황이 발생한다.
렌더링 요청은 콜 스택이 비워지는 순간 이벤트 루프가 렌더링 요청을 콜 스택에 추가하는 시점에 실행되기 때문에, 나머지 함수들이 실행을 마칠 때까지 태스크 큐에서 대기하게 되고, hideIsLoading이 “로딩 중” 텍스트를 숨긴 이후에 렌더링 요청이 되어 텍스트가 화면에 표시되지 않게 된다.
이를 해결하기 위해서는 isLoading함수가 렌더링 요청이 태스크 큐에 추가되었을 때 콜 스택이 비워져 있어야 한다. 이때, 아래 예제와 같이 setTimeout(fn, 0)을 활용해 코드가 동작하는 특정한 타이밍을 지정하고, 의도한 대로 코드를 구현할 수 있다.
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(); setTimeout(() => { someProcessTakeLongTime(); hideIsLoading(); showResult(); }, 0); });
예제 3-10
예제 코드가 동작하는 흐름은 다음과 같다.
  1. isLoading함수의 렌더링 요청이 태스크 큐에 추가되고, setTimeout이 콜 스택에 추가된다.
    1. 예제 3-11-1예제 3-11-1
      예제 3-11-1
  1. setTimeout이 실행되면서 내부에 있는 함수들이 바로 실행되지 않고, 태스크 큐에 들어가게 된다.
    1. 예제 3-11-2예제 3-11-2
      예제 3-11-2
  1. 이때, 콜 스택이 비워지고 이벤트 루프는 isLoading함수의 렌더링 요청을 콜 스택에 추가한다. 이후에 isLoading함수의 코드가 실행되면 “로딩 중” 텍스트가 화면에 표시된다.
    1. 예제 3-11-3예제 3-11-3
      예제 3-11-3
  1. “로딩 중” 텍스트가 화면에 표시되는 동안 나머지 함수들이 콜 스택에 추가되고, 코드가 의도 대로 동작하게 된다.
    1. 예제 3-11-4예제 3-11-4
      예제 3-11-4
notion imagenotion image
notion imagenotion image

4. 비동기 실행을 제어하는 방법들

4.1 비동기 흐름 제어란?

비동기적인 흐름으로 실행되는 함수의 문제점을 해결하기 위해 동기적으로 흐름을 제어하는 방식을 의미한다.

4.2 비동기 흐름 제어 방식

비동기 흐름 제어에는 총 3가지 방법이 있다.
  1. 콜백 함수
  1. Promise
  1. async / await
Promise는 비동기적으로 수행하는 결과값이 끝난 것을 알려주는 객체이다. Promise 문법은 코드가 복잡해 질수록 가독성이 떨어지거나 에러처리가 어려우며, 콜백 지옥을 만날 수 있다는 콜백 함수의 단점을 개선하기 위해 만들어졌다. Promiseresolve, reject를 인자로 받고, then과 catch로 비동기 처리를 해주는데, 성공 resolve를 호출하고, 실패reject를 호출한다
async / await 은 비동기 함수를 동기적으로 처리할 수 있는 기능이 있다. 이는 가독성이 더 좋고 간결하게 동작할 수 있다는 장점이 있다. function 앞에 async 키워드를 붙여주면 비동기 함수임을 알려주고, 해당 함수를 호출하면 Promise를 반환한다. await 키워드는 async 키워드가 붙은 함수 내부에서만 사용할 수 있으며 Promise로 부터 결과를 반환한다. await키워드를 사용하면 일반 비동기 처리처럼 순서에 상관없이 실행하는 것이 아니라, 결과를 얻을 때까지 기다린다.
const test = () => { const res = fetch('https://jsonplaceholder.typicode.com/posts/3'); const result = res.json(); console.log(result) }
notion imagenotion image
위의 예제를 실행하면 아래와 같은 에러가 발생한다. 그 이유는 fetch를 통해 서버에 요청을 보내고 데이터를 불러오기까지 시간이 걸려서 데이터를 불러오는 동안 다른 공간에 저장해두고 다음 코드들을 먼저 처리하는데, 이 때 res는 처리가 되지않았기 때문에 값이 undefined인 상태이다. 그 다음 줄의 .json( )Promise의 메서드인데 앞의 res 변수에는 값이 할당되지 않았기 때문에 실행을 할 수가 없어 에러가 발생한다.
const test = async () => { const res = await fetch('https://jsonplaceholder.typicode.com/posts/3'); const result = res.json(); console.log(result) }
notion imagenotion image
이를 해결하기 위한 방법으로 비동기 함수를 동기적으로 처리할 수 있게 도와주는 async / await 키워드를 사용할 수 있다. 함수 앞에 asyncfetch 앞에 await을 붙여주면 위처럼 Promise 객체를 반환하는 결과를 확인할 수 있다. Promise 와 async / await 에 관련된 내용은 뒤의 챕터에서 더 자세하게 다룰 예정이다.