프린세스 다이어리

자바스크립트는 싱글스레드 언어라면서 어떻게 비동기함수 사용할 수 있나요 본문

FE

자바스크립트는 싱글스레드 언어라면서 어떻게 비동기함수 사용할 수 있나요

개발공주 2021. 10. 29. 23:29
728x90
자바스크립트는 기본적으로 한 번의 한 작업만 수행 가능한 싱글 스레드 언어다. 하지만 자바스크립트는 웹 브라우저나 NodeJS와 같은 멀티 스레드 환경에서 실행된다. 자바스크립트가 멀티 스레드로 실행될 수 있도록 도와주는 장치에 대해 정리해보았다. 

 

1. 콜백 함수가 실행되는 순서

 

console.log('123');

setTimeout(function() {
    console.log('567');
}, 300);

console.log('987');
123
987
567

 

여기 콘솔 로그 중간에 setTimeout이라는 함수가 있다. 이 함수는 첫 번째 인자로 들어간 함수를 두 번째 인자로 들어간 숫자만큼의 ms(밀리세컨드)를 기다렸다가 실행하도록 한다. 이 코드에 setTimeout 함수만 놓고 보면, 300ms를 기다렸다가 콘솔에 '567'이 찍히게 된다. 

 

콜백 함수는 다른 함수의 인자로 넘겨지는 함수다. 위에서는 setTimeout의 첫 번째 인자로, console.log('567')을 실행하는 함수가 넘겨지는데 이 콘솔 찍는 함수가 콜백 함수다. 콜백 함수를 둘러싼 콜백 수신 함수 setTimeout에 의해서 특정 시점에 실행이 된다. 

 

 

function greeting(name) {
    console.log(`${name}, how are you doing?`);
}

function processUserInput(callback) {
    const name = prompt('Type your name...');
    callback(name);
}

processUserInput(greeting);

 

위에서는 processUserInput 함수에 greeting 함수가 인자로 들어가서 실행이 된다. greeting 함수는 여기에서 콜백 함수다. 먼저 processUserInput이 실행되고 이름을 입력받으면, greeting이라는 콜백 함수의 인자로 이름이 들어가고, "Eunjin, how are you doing?"이 콘솔에 찍힐 것이다. 

 

콜백 함수가 비동기 콜백이냐 동기 콜백이냐에 따라서, 나중에 조건을 만족했을 때 실행될 수도 있고, 바로 실행될 수도 있다. 비동기 콜백의 경우에는 위의 setTimeout처럼, 특정 시간이 지난 후 콜백 함수를 실행한다던지, HTTP 요청을 보낼 때 실행한다던지, 특정 이벤트가 발생하면 실행한다던지 등의 경우를 들 수가 있다.

 

일단 자바스크립트 엔진이 코드를 실행하다가 중간에 setTimeout과 같은 비동기 코드를 만나면, 이 코드를 '뒷단'에서 실행을 하도록 보낸다. 그 다음 나머지 코드를 이어서 진행하고, 그렇게 보내버린 비동기 코드가 자바스크립트 '뒷단'에서 실행이 되도록 놔둔다. 이벤트 루프는 자바스크립트 엔진의 '뒷단'에서 일어나는 어떤 문맥의 일부로써 동작하는 하나의 장치다. 자바스크립트 엔진이 무엇인지, 이벤트 루프가 무엇인지 이어 설명하도록 한다. 

 

2. 자바스크립트 엔진이란?

 

자바스크립트 코드를 해석하고 실행시켜 주는 프로그램 혹은 인터프리터를 '자바스크립트 엔진' 이라고 부른다. 자바스크립트 엔진에는 구글에서 개발한 V8, 애플이 사파리를 위해 개발한 JavaScript Core, 모질라 재단에서 운영하는 Rhino 등이 있다. 이런 엔진이 어디 있냐 하면 대표적인 예시가 바로 웹 브라우저다. 

 

자바스크립트 코드만 달랑 작성한다고 해서 이 코드가 실행되는 게 아니라, 이 자바스크립트 엔진을 통해 인터프리팅 되어야만 실행되는 것이다. 자바스크립트 엔진을 구성하는 요소는 메모리 힙과 호출 스택이다.

 

  • Memory Heap : 메모리 할당이 일어나는 곳(변수나 객체들이 저장되는 창고)
  • Call Stack : 코드 실행에 따라 함수의 실행 콘텍스트가 쌓이는 곳

 

 

어떤 언어로 개발을 하든 코드를 읽고 실행하는 과정에서 메모리와 스택은 반드시 필요하다. 자바스크립트 엔진이 구동될 때도 마찬가지로 두 가지 핵심 단계가 있다. 프로그램이 실행되려면 다양한 객체, 배열, 함수의 저장 또는 호출을 위해 당연히 메모리를 사용해야 하므로 메모리 힙이 필요하다. 또 호출 스택(콜 스택)은 현재 실행 중인 코드를 추적해야 하기 때문에 필요하다.

 

호출 스택에 쌓이는 실행 콘텍스트는 변수나 함수의 선언이나 scope, this 등의 정보를 객체 형태로 담고 있고, 호출 스택이 빈다는 것은 더 이상 실행할 함수가 없다는 것을 의미한다. C언어로 따지면 main 함수가 전부 끝나야 호출 스택이 빈다는 의미다. 여기서 이 글의 핵심 주제인 비동기 함수가 실행되는 방법은 메모리 힙보다는 호출 스택을 심도 있게 앎으로써 이해할 수 있다. 

 

3. 호출 스택이 어떻게 동작할까

 

자바스크립트가 싱글 스레드 언어라는 말은 호출 스택이 하나라는 것이다. 호출 스택이 한 개라는 것은 한 번에 한 작업만 처리할 수 있다는 뜻이다. 기본적으로 자바스크립트 프로그램이 한 줄 씩 실행될 때, 현재 어떤 실행 콘텍스트에 위치하는지를 차례로 스택의 최상단에 쌓아가면서 기록한다. 

 

console.log('123');

setTimeout(function() {
    console.log('567');
}, 300);

console.log('987');

 

자 맨 처음에 보았던 setTimeout 메서드가 포함된 코드다. 이런 프로그램이 실행된다고 했을 때, 엔진이 이 코드를 실행하기 전에는 호출 스택이 다 비어 있는 상태였다가 코드가 실행되면서 호출 스택은 다음과 같이 변한다. 

 

호출 스택에 작업이 쌓였다가 제거되는 과정

 

코드가 실행되면 맨 아래에 전역 컨텍스트인 anonymous가 먼저 담긴다. 그다음에 호출 스택에 함수가 호출이 될 때 하나씩 쌓였다가, 함수 실행이 끝나면 스택의 상단에서부터 순차적으로 함수 실행이 끝나면서 작업이 제거된다.

 

한편, 만약 종료 조건 없이 자신을 호출하는 함수를 호출한다면 스택은 어떻게 쌓일까?

function inception() {
    inception();
}

inception();

스택오버플로우가 발생하는 과정

 

함수 inception()이 호출 스택에 쌓이는데, 그 작업을 수행하기 위해서 다시 inception() 을 호출해야 하므로 호출 스택에서 작업이 제거되지 않은 채 inception()이 추가로 쌓이기만 한다. 결국 최대 스택 사이즈를 초과한다면 스택오버플로우 오류가 발생한다. 브라우저에서는 콘솔에 "Uncaught RangeError: Maximum call stack size exeeded" 에러 메시지를 띄운다.

 

브라우저마다, 엔진마다 콜 스택의 한계치는 상이한데 보통 1만 개를 가지고 있고, 크롬은 12만 개라고 한다. 

 

4. 이벤트 루프가 필요해지는 시점

 

자바스크립트 프로그램이 실행될 때 단일 호출 스택을 사용한다고 했다. 스택의 최상단부터 순차적으로 실행하다 보니, 어떤 함수가 처리 속도가 정말 느리거나, 서버에서 응답을 받아와야 한다거나 하는 특수한 상황에서는 스택 안의 다른 함수들이 그 거북이 같은 함수의 종료까지 열심히 기다려야 한다. 이렇게 되면 거북이 함수가 실행되는 동안 브라우저는 페이지를 그리지도 못하고 사용자는 인터랙션도 못 하게 된다. 사용자는 웹 사이트를 접속하자마자 피드백을 즉각 얻길 바라기 때문에, 이런 거북이 함수는 어딘가에 보내서 비동기적으로 처리해주고 일반적인 코드부터 실행해줘야 한다. 

 

function second() {
    setTimeout(function() {
        console.log("how's everything going?")
    }, 2000);
}

function first(){
    console.log("hello it's me");
    second();
    console.log("heyyyy")
}

first();

 

다시 setTimeout이 포함된 코드를 가져와서 이벤트 루프가 어떤 방식으로 거북이 함수 실행에 도움을 주는지 알아보자. 스크립트가 실행되어 first 함수가 호출되면, first()가 호출 스택에 먼저 쌓인 후 콘솔이 쌓인다. 콘솔이 찍히고 종료되면 그다음의 second() 함수가 스택에 쌓인다. 그렇게 되면 second 함수의 실행 문맥에 있는 코드가 쌓이게 되는데 이때 위 코드에서는 setTimeout 함수가 쌓이는 거다.

 

 

setTimeout 함수는 콘솔 로그를 찍는 콜백 함수와 몇 초 후에 실행될지를 알려주는 수를 파라미터로 가진다. 이 setTimeout 함수가 실행이 되면 자바스크립트의 뒷단으로 보내 타이머를 실행시키고 역할을 끝냈기 때문에 second 함수도 리턴을 하고  스택에서 빠진다. 그리고 남아 있는 first 함수에서 다음 코드인 console.log를 실행시킨 후, 이마저도 끝나면 first 함수도 리턴을 한다. 

 

 

이렇게 호출 스택은 비게 되는데, 아까 setTimeout 이 자바스크립트 뒷단으로 보내 타이머를 동작시킨 것이 남아있다. 타이머는 지정된 시간을 모두 카운트하면, 호출 스택이 비는 걸 기다렸다가 콜백함수를 호출한다. 즉 호출 스택이 모두 비고 나면 "how's everything going?" 이 콘솔에 찍힌다. 

 

 

이렇게 자바스크립트 엔진은 호출 스택이 하나임에도 불구하고 뒷단의 다른 환경들과 결합해서 비동기적인 함수 실행을 지원한다. 지금까지 '뒷단'이라고 표현했던 것은 바로 웹 브라우저가 가지고 있는 web API,이벤트 루프, 콜백 큐 등이다. 예시로 들었던 setTimeout은 자바스크립트 엔진이 아닌, 브라우저에서 제공하는 web API다. setTimeout, DOM, Ajax 등이 web API의 예시다.

 

브라우저의 구조

 

그림체는 달라도 위와 같은 브라우저의 구조 그림을 정말 자주 보았을 것이다. 왼쪽 위는 자바스크립트 엔진으로 그 중 v8 엔진의 구조다. web API의 메서드들은 전부 비동기 메서드다. 작동을 마치면 콜백 함수를 콜백 큐에 집어넣는다. 거기서 콜백 함수들이 실행을 대기하게 되는데 이 콜백 큐는 다른 말로 태스크 큐라고도 한다. 

 

  • Call Stack: 자바스크립트에서 수행해야 할 함수들을 순차적으로 스택에 담아 처리
  • Web API: 웹 브라우저에서 제공하는 API로 AJAX나 Timeout등의 비동기 작업을 실행
  • Task Queue: Callback Queue라고도 하며 Web API에서 넘겨받은 Callback함수를 저장
  • Event Loop: Call Stack이 비어있다면 Task Queue의 작업을 Call Stack으로 옮김

 

이렇듯, 자바스크립트 엔진 자체는 싱글 쓰레드이지만, 실제로 자바스크립트가 구동되는 환경인 웹 브라우저에서는 web API가 멀티스레드로 작동하면서 여러 개의 스레드로 작동한다. 그리고 자바스크립트 엔진이 이 web API와 상호 연동을 하기 위해서 필요한 장치가 콜백 큐와 이벤트 루프다.

 

> 이벤트 루프에 대한 글

 

이벤트 루프와 콜백 큐가 작동하는 원리

앞에 내용이랑 이어진다. > 자바스크립트는 싱글 스레드 언어라면서 어떻게 비동기 함수 사용할 수 있나요 자바스크립트는 싱글스레드 언어라면서 어떻게 비동기함수 사용할 수 있나요 자바스

eunjinii.tistory.com

 

 

728x90
Comments