[JS] 비동기의 구체적인 내부 동작 과정을 알아보자

Pakxe·2023년 1월 10일
2

JavaScript

목록 보기
13/16
post-thumbnail

https://velog.io/@pakxe/JS-%EB%8F%99%EA%B8%B0%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90
위 글을 먼저 읽고 이 글을 읽으면 더 이해가 잘됩니다!

우리는 동기 비동기 글에서 비동기가 어떻게 실행되는지 정말 간략하게 배웠었다. 많이 생략해서 설명한 이유는 내가 비동기를 처음 공부했던 글이 내부 구조와 함께 설명된 글이었는데 하나도 이해하지 못했기 때문이다. 그래서 일단 비동기 초벌을 해놓고 심화 내용을 들어가는게 내 방식에서는 맞았기 때문에 글을 이렇게 분리해서 적었다.

시작

(내부 구조는 아래에서 설명)

js는 싱글 스레드이고 비동기는 싱글 스레드에서 실행될 수 없기 때문에 node.js라는 멀티 스레드 환경에서 비동기가 실행된다. 그러면 드는 생각이 js자체를 멀티 스레드로 만들면 js 엔진 만으로도 비동기를 실행할 수 있는게 아닌가 생각이 든다.

왜 JS는 싱글 스레드를 택했을까?

원래 자바스크립트는 웹페이지의 동적인 기능을 수행하기 위해 탄생된 언어이다. 이 동적인 기능이라함은 웹페이지의 보조적인 기능일 뿐이므로 그렇게 복잡한 기능이 필요하지 않았다. 그리고 그 당시의 멀티 스레드 언어인 자바는 무겁고 어렵다는 인식이 있었으므로 자바스크립트는 가벼운 싱글 스레드를 택했다.

번외 - 과거의 웹페이지

이 동적인 기능이 뭐길래 자바스크립트를 만든걸까? 동적인 기능을 추가하고 싶었다고 하니 과거의 웹페이지는 정적인 모습이었겠구나 예상이 된다. 자바스크립트가 탄생하기 전의 과거의 웹페이지가 어땠는지 봐보자.


(HTML 로만 구성이 된 최초로 만들어진 웹 페이지(WWW) (출처 : info.cern.ch) 사이트에 들어가 개발자도구(f12)를 켜보면 정말 기본적인 HTML 태그로만 구성된 것을 볼 수 있다.)


(HTML, CSS 로만 구성이 된 웹 페이지 (출처 : https://www.w3.org/TR/CSS1/
))

현대의 웹페이지와는 많이 다른 모습이다. 첫번째 사진의 페이지는 정말 순수하게 HTML태그로만 구성되어 있다. 두번째 사진의 페이지는 약간의 CSS 스타일링이 되어 있다.

자바스크립트가 탄생해 이런 정적인 페이지가 동적으로 동작할 수 있게 됐다. 동적인 기능이라 함은 특정 상황(onClick과 같은 이벤트)에 따라 동적으로 작동하는 것을 말한다. 이를 통해 우리는 웹페이지를 재밌게 만들어갈 수 있게 됐다.

싱글 스레드.. 이제 제대로 알아보자

스레드?

스레드(thread)는 이라는 뜻을 갖고 있다. 이는 한 가지 작업을 실행하기 위해 순차적으로 실행할 코드를 실처럼 이어 놓는다는 의미에서 유래한 이름이다. 따라서 스레드는 하나의 코드 실행 흐름 이다.

이 스레드는 프로세스 안에서 작업을 수행하게 된다. 실질적으로 프로세스 내부의 작업 수행 주체는 스레드라는 뜻이다.

프로세스?

프로세스란 간단하게 말하면 실행중인 애플리케이션(프로그램)을 말한다. 윈도우를 한번이라도 사용해봤다면, 작업관리자에서 프로세스탭을 많이 봤을 것이다.

(윈도우의 작업관리자. 프로세스를 보여준다 = 실행중인 것들을 다 보여준다는 의미다
(https://answers.microsoft.com/ko-kr/windows/forum/all/작업관리자/450f4247-3a7b-4822-a5a2-af26d4dbd6aa))

이 프로세스는 운영체제에 의해 만들어진다. 내가 크롬을 켰다면 운영체제는 이 크롬에 메모리를 할당해 실행시켜 준다. 이것이 프로세스다. 사진에 있는 메모리 탭이 얼마만큼의 메모리를 해당 프로그램에 할당해줬는지를 볼 수 있다.

내가 크롬도 키고 롤도 켰다면 운영체제는 적당히 메모리를 분류해 크롬 프로세스, 롤 프로세스를 생성할 것이다.

다시 스레드로

이 프로세스에는 한 개 이상의 스레드가 존재해 주어진 작업들을 수행한다. 만약 두 개 이상의 스레드를 가졌다면 멀티 스레드라고 부른다.


(https://miracleground.tistory.com/entry/자바스크립트는-왜-싱글-스레드를-선택했을까-프로세스-스레드-비동기-동기-자바스크립트-엔진-이벤트루프)

자바스크립트는 싱글 스레드이다. 이 싱글 스레드는 앞서 공부했듯이 동시에 일을 수행할 수 없다. 한번에 하나의 일만 수행할 수 있으므로 싱글 이라고 하는 것이다. 그런데 왜 하나의 일만 할 수 있는걸까? 내부적으로는 어떻게 되어 있는걸까?

자바스크립트 엔진의 싱글 스레드

자바스크립트 엔진은 아래 사진처럼 생겼다. memory heap, call stack 이 보인다. 지금 중요한건 call stack이니 여기에 집중하자.

memoey heap?

js의 참조형 데이터(객체, 배열, 함수 등)는 memory heap에 저장된다.


(이런 모습이다 (https://charming-kyu.tistory.com/19))

다시 돌아가서

콜 스택은 코드를 호출하며 스택으로 쌓는다. 함수를 실행하면 콜스택에 쌓이고 LIFO 방식으로 나중에 들어온 함수부터 처리된다. 제일 마지막으로 들어온 함수를 실행 후 제거 → 그 함수 바로 이전에 들어온 함수를 실행 후 제거 → … → 콜스택에 아무것도 남지 않을 때 까지 반복된다.


(https://joshua1988.github.io/web-development/translation/javascript/how-js-works-inside-engine/)

자바스크립트 엔진의 메인 스레드가 저 하나의 콜스택이므로 자바스크립트가 싱글 스레드라고 불리는 것이다. (콜스택이 어떻게 작업을 처리하는지는 실행 컨텍스트에서 다뤄질 내용 같으므로 생략하겠습니다. 그래도 지금 알고싶다고 한다면 https://youtu.be/8aGhZQkoFbQ영상의 4:30초 참고)


(싱글 스레드 = 하나의 콜 스택 = 한번에 하나의 일만 할 수 있다!
(https://youtu.be/8aGhZQkoFbQ))

그렇다면 싱글 스레드인 자바스크립트가 어떻게 비동기 코드를 실행하는 것일까?

자바스크립트 실행 환경

자바스크립트의 실행 환경은 크게 두 가지가 있다. 브라우저와 node.js이다. 실행 환경이란 자바스크립트가 잘 돌아갈 수 있도록 조성해준 환경이다. 이 환경 안에서 자바스크립트는 맘껏 실행될 수 있다. 그런데 잘 돌아간다 의 기준이 무엇일까? 왜 자바스크립트 엔진 만으로는 자바스크립트를 실행할 수 없는 걸까?

자바스크립트는 웹 환경에서 실행된다. 우리는 이 환경에서 서버에 데이터를 요청해 받아오고 다룬다. 이 데이터를 받아오는, 대다수 시간이 소요되는 작업들은 비동기라고 배웠었다. 자바스크립트 엔진 자체만으로는 비동기를 수행할 수 없다. 싱글 스레드니까! 그런 자바스크립트 엔진을 위해 실행 환경이라는 친구들이 자바스크립트 엔진과 함께 동작한다.

이 실행 환경에 비동기 코드 실행을 도와주는 도구들이 있는 것이다. 브라우저 실행 환경에도 있고, node.js 실행 환경에도 있다. 물론 각 환경의 비동기를 도와주는 도구들이 완벽히 똑같이 동작하는 것은 아니지만 비슷하게 동작한다. 이 글에서는 브라우저 실행 환경을 기준으로 비동기를 설명한다.

  • 브라우저 실행 환경 vs node.js 실행 환경 둘의 차이는 용도다. 브라우저 실행 환경은 HTML, CSS, JS를 읽고 실행해 화면에 렌더링 하는 것이 주된 목적이다. 반면에 node.js는 브라우저가 아닌 환경에서 자바스크립트 실행 환경을 조성하는 것이 목적이다. 이렇게 말하니 와닿지 않을 수도 있다. 각 실행 환경이 무엇을 지원하지 않는지를 보면 조금 이해가 될 것이다. node.js 실행 환경은 브라우저 환경이 아니다. 그렇다는 말은 컴포넌트들을 렌더링해 화면에 보여줄 필요가 없다는 뜻이다. 애초에 화면이 없으니까 말이다. 우리가 js 코드를 작성하는 vsc에서 node.js로 코드 실행하기를 누르면 콘솔만 보이지 화면은 없다. 따라서 node.js 실행환경은 DOM api를 지원하지 않는다.
    • DOM API?

      DOM api 는 쉽게말해 태그의 정보를 받아오는 것이다.
      js가 HTML과 소통하기 위해선 DOM api를 이용해야 한다.
      예를 들어 js에서 id가 ‘pakxe’인 HTML 태그에 접근하고 싶다면 document.getElementById('pakxe') 라고 작성해 접근할 수 있다.
      이런 기능이 DOM api 인 것.

      그런데 콘솔에는 DOM이라고 할 것들이 없다. 콘솔에는 그냥 명령만 있다. 그러니 node.js에 포함되어있어도 쓸 수 없는 필요없는 API이므로 지원하지 않는 것이다.

      그리고 브라우저는 파일 시스템을 지원하지 않는다. 이건 무슨 말이냐면 js로 파일명이 pakxe.txt인 파일을 추가하는 코드를 작성하고 웹페이지에서 실행시켜도 브라우저는 아무 일도 일어나지 않는다는 뜻이다. 파일 시스템이란 쉽게 말해 파일을 만들고 지우고 수정하는 것이다. 우리가 파일 관리자(맥은 finder)에서 하는 일을 말한다.

      그런데 왜 브라우저는 파일 시스템을 지원하지 않을까? 그건 해킹의 위험이 있기 때문이다. 만약 특정 웹사이트에 들어가기만해도 나의 모든 파일이 다 pakxe.txt로 바뀌어버린다면 정말 끔찍할 것이다..

      그러니 이런 일을 방지하기 위해 브라우저는 파일 시스템을 지원하지 않는 것이다.

실행 환경은 어떤 모습일까?

싱글 스레드인 자바스크립트 엔진이 비동기 코드를 실행할 수 있도록 도와주는 것이 실행 환경의 역할이다.

브라우저 실행환경은 web api, 이벤트 루프, 콜백 큐가 추가된 환경이다. 아래와 같은 구조라고 생각하면 된다.


(브라우저의 자바스크립트 실행 환경
(https://wookgu.tistory.com/21))

짙은 회색으로 네모낳게 감싸져있는 부분이 자바스크립트 엔진이다. 크롬 브라우저라면 V8 엔진일 것이다.

엔진 외에도 web api, callback queue, event loop가 보인다 뭔지 하나씩 알아가보자.

web api

web api는 이미지에 써있듯이 DOM 트리 접근을 위한 DOM api, 서버와 통신하기 위한 AJAX(이건 다른 페이지로 분리되어 있으므로 자세한 설명은 생략하겠습니다.), Timeout등 웹 브라우저에서 제공하는 기능들을 말하는 말이다. (이 setTimeout은 동기 비동기의 예시로 사용됐던 함수이다. web api에서 제공한다. 그리고 node.js안에도 있다. https://nodejs.org/dist/latest-v18.x/docs/api/timers.html#settimeoutcallback-delay-args)

저 3가지 외에도 정말 무수한 기능들이 있으므로 심심하면 읽어보자. 익숙한 이름의 기능이 있을 수도 있다. https://developer.mozilla.org/ko/docs/Web/API

이 web api는 이름에서도 알 수 있듯이 웹페이지와 관련된 기능을 제공한다. 앞에서 말했던 DOM api도 보인다! 따라서 node.js 실행환경에서는 제공되지 않는다. 필요없으니 말이다.

callback queue

사진의 오른쪽 하단에 callback queue가 보인다. 이건 뭐하는 기능일까? 일단 큐인걸 보니 FIFO으로 작동한다는 것은 예측할 수 있다.

이건 콜백함수를 담아두기 위해 사용된다. 예시와 함께 이해해보자.

사용자의 이름을 name 변수에 입력받고 이 이름을 console.log(name)으로 출력하려고 한다.

사용자 이름 입력을 받는 코드는 동기일까 비동기일까? 시간이 좀 걸리는 코드이므로 비동기이다. 그런데 console.log는 동기 코드이다. 그러면 동기 비동기에서 배웠듯이 동기 코드가 다 실행되고나서 비동기 코드가 실행될 텐데, 이름을 입력받는 코드보다 이름을 출력하는 코드가 먼저 실행되어 버린다. 그러면 name에 할당된 값이 아무것도 없으므로 undefined를 출력하게 된다.

그러면 이걸 어떻게 해결 해야할까? 답은 콜백 함수를 전달해주면 된다. 이 콜백 함수라는 건 이걸 argument로 전달받은 함수가 이 콜백 함수를 언제 실행할 지 결정할 수 있는 걸 말한다. 말이 아주 어렵다.

더 쉽게 이해하자면 이름을 입력 받는 코드는 비동기라고 했다. 이름을 출력하는 코드는 동기다. 이 이름을 출력하는 코드는 이름을 입력받는 작업 후에 실행되어야 한다. 작업 순서가 지켜져야 한다는 뜻이다. 그러면 이 작업 순서를 어떻게 정해주냐. 동기 후 비동기 실행은 자바스크립트 실행환경의 규칙인데…

그런데 이 작업 순서를 콜백 함수로 정해줄 수 있다!

비동기 함수는 콜백 함수라는 argument를 받아서 함수 내부에 어디서든지 실행할 수 있다. 그 말인 즉, 이 콜백 함수라는 것의 호출 순서는 비동기 함수의 내부 ⇒ 비동기 함수가 실행 된 후에 콜백함수가 실행된다는 것이다. 동기 → 비동기 였던 원래의 순서에서 비동기 → 동기 순서로 바뀌었다.
작업 순서를 정할 수 있게 됐다! 야호

이름을 출력하는 함수는 이름을 입력받는 함수를 먼저 실행하고 나서 실행해야한다. 그러면 이름을 입력받는 함수에게 argument로 이름을 출력하는 함수를 전달한다. 그리고 내부에서 전달받은 함수를 실행하면 우리가 의도 했던 대로 이름 입력받기 ⇒ 이름 출력하기 라는 작업 순서가 고정되어 예쁘게 완성된다.

// name paremeter에 입력받은 이름이 들어간다.
getName((name) => {
	console.log(`hi! ${name}`);
});

예시를 설명해서 좀 길었다. 결국 비동기 함수는 콜백함수를 인자로 받아 자신(비동기)실행 후 콜백 함수를 실행하도록 작업 순서를 정할 수 있는 것이다.

이전 동기 비동기에서 비동기는 동기 코드가 모두 실행된 후에 실행된다고 했다. 동기 코드가 모두 실행되기까지 비동기 코드는 어딘가에 저장되어 있다가 자바스크립트 엔진의 콜스택으로 들어가 실행된다. 이 어딘가가 콜백 큐였던 것이다! (위 코드 예시에서는 console.log 부분이 콜백 큐에 들어가게 되는 것)

정리하자면 비동기로 실행되어야하는 코드(콜백)들은 이 콜백 큐에 저장되어 있다가 콜스택으로 돌아가 실행된다.

event loop

사진을 잘 보면 가운데 하단에 원 모양으로 되어있는 event loop가 보인다. 이건 뭐하는 기능일까?

loop는 반복을 의미하니 뭔갈 반복하는 일을 하는걸까? 계속해서 얘기하는 말이지만 비동기는 동기가 모두 실행된 후에 실행된다. 이 말은 자바스크립트 엔진의 콜스택이 비어야 비동기 코드가 실행된다 라는 말과 똑같다. 비동기 또한 코드이다. 이 코드도 실행이 되야한다. 이 코드는 콜백 큐에 저장되어 있다가 콜 스택이 비면 콜백 큐에서 콜스택으로 이동되어 실행된다.

그러면 의문이 들어야하는게, 콜 스택이 비어있는건 어떻게 확인하느냐이다. 이걸 이벤트 루프가 해준다.

이벤트 루프는 콜스택을 항상 보고있다. 그러다가 콜스택이 비면 콜백 큐에서 작업을 뽑아 콜스택으로 집어넣어주는 역할을 한다.

드디어 비동기 동작 과정을 공부해보자.

이제 정말 많은 것을 알았다.

시작은 동기코드가 전부 실행된 후에 비동기가 실행 된다는 것에서 출발됐다. 그러면 동기 코드가 실행될 동안 비동기 코드는 어디에 있느냐? 콜백 큐에 저장된다. 그리고 동기 코드가 다 실행됐다는건 어떻게 인식하느냐? 이벤트 루프가 인식하고 콜백 큐에 있는 작업을 알아서 콜스택이 빌 때마다 넣어준다.

이걸 실제 자바스크립트의 비동기 코드 예시와 함께 알아보자.

실행할 코드는 아래와 같다.

  • setTimeout의 보충 설명 이 코드를 이해하기 위해 setTimeout의 동작 과정을 조금 알고 가야한다. setTimeout이라는 비동기 함수는 콜백 함수를 첫번째 인자로 받고, 두번째 인자로 딜레이할 시간을 받는다. 아마 이전에 설명했어서 알 것이다.
    이 setTimeout은 web api에서 제공하는 함수다. 자바스크립트 엔진은 코드를 쭉 읽다가 이 setTimeout을 만나면 이 콜백을 web api로 던진다. 이 web api에는 타이머라는 것도 있는데 setTimeout의 콜백을 입력받은 시간만큼 딜레이 시키고 콜백 큐로 작업을 옮기는 역할을 한다. 간단하게 말하면
    1. 자바스크립트 엔진이 코드를 쭉 실행한다
    2. setTimeout 비동기 함수를 만난다.
    3. web api로 던져놓고, 이 비동기는 제외하고 계속해서 동기 작업을 한다.
    4. 이때 web api에서는 타이머에 7초(예시 코드는 7000이니까)를 눌러놓고 시간이 지나가길 기다린다.
    5. 7초가 지나면 web api는 이 콜백 함수를 콜백 큐로 던진다.

결과를 예상해보자. 이제는 내부 과정이 어떻게 될지까지 생각해보면 더 좋다.

  • 내가 생각한 것 전역 컨텍스트(실행 컨텍스트에서 설명될 내용이므로 생략)가 콜스택에 들어간다.
    1줄의 print2라는 함수를 만난다. 함수를 메모리 힙에 저장한다.
    6줄의 동기 코드를 실행한다. 1이 콘솔에 찍힌다.
    7줄의 비동기 코드를 만난다. 콜백 함수인 print2를 web api로 던진다.
    8줄의 동기 코드를 실행한다. 3이 콘솔에 찍힌다.
    그 사이 web api는 7초를 세고 콜백 큐에 콜백을 던진다.
    모든 코드가 끝났다. 콜스택이 비었으므로 이벤트 루프가 작동해 콜백 큐의 print2를 콜스택으로 되돌린다.
    자바스크립트 엔진이 print2를 실행한다. 2가 콘솔에 찍힌다.

결과는 영상으로 봐보자.

▲ 빨라서 잘 안보일 수도 있으니 하나하나 살펴보자.

▲ 1줄의 함수 선언식같은 경우는 콜스택이 담당하는 것이 아니므로 동작 과정에는 생략됐다.

바로 5줄의 console.log(1) 이 콜스택에 들어간다.

▲ 콘솔에 1이 출력됐다. 실행이 완료됐으니 console.log(1)이 콜스택에서 제거된다.

▲ 6줄의 setTimeout이 콜스택에 들어간다. 그렇지만 바로 web api로 쫓겨났다. 자바스크립트 엔진이 실행할 수 없는 함수이기 때문이다.

▲ setTimeout에 전달된 콜백 함수인 print2가 web api의 타이머에 의해 7초를 기다리고 있다. print2옆에 있는 초록색 동그라미가 7초 타이머를 나타낸다.

▲ 타이머는 web api내부에서 계속 흘러가고, 자바스크립트는 던져놓은 일이니 아무 신경도 쓰지않고 다음 코드인 8줄 console.log(3) 을 콜스택에 넣는다.

▲ 3이 콘솔에 출력됐다. console.log(3) 이 콜스택에서 제거된다.

▲ 모든 동기 코드가 실행되어 콜스택이 비어있다. 하지만 아직 약속의 7초가 오지 않았다… 3초정도만 더 기다려보자.

▲ 7초가 지나면 콜백함수인 print2가 콜백 큐로 이동한다. 만약 이때 콜 스택이 비어있지 않았다면 print2는 콜백 큐에서 계속 콜스택이 빌때까지 기다렸을 것이다.

하지만 지금은 비어있으니 이벤트 루프가 동작한다.

▲ 이벤트 루프가 빙글 돌더니 print2를 콜스택에 집어넣는다. 여기서부터는 자바스크립트 엔진의 영역이다.

print2에는 console.log(2) 작업이 있으므로 콜스택에 올라간다.

▲ 2가 콘솔에 출력된다. console.log(2) 가 콜 스택에서 제거된다.

print2 함수 스코프의 모든 작업을 수행했다. print2 함수가 콜스택에서 제거된다.

콜스택, web api, 콜백 큐 모두 비었다. 모든 작업이 끝났다.

이런 과정을 통해 비동기 작업이 수행되는 것이다. 글로 읽는 것과 직접 눈으로 동작 과정을 보는 것은 많은 차이가 있으므로 부득이하게 길게 작성하게 됐다.

추가로 중간에 말했던 콜백 큐에서 콜백이 콜스택이 빌 때까지 기다리고 있는 모습을 추가하며 글을 마친다.

(print2가 얌전히 콜백 큐에서 콜 스택이 빌 때까지 기다리고 있다.)


추가로 공부하면 좋을 내용


참고 자료

[Javascript] 자바스크립트는 싱글 스레드인가?

자바스크립트(JavaScript) 기초 및 문법ㅣ정의, 기본 문법, 변수, 함수 - 코드스테이츠 블로그

프로세스(Process)와 스레드(Thread)

코딩교육 티씨피스쿨

[Node.js] 비동기 개념에 익숙해지기

profile
내가 꿈을 이루면 나는 또 누군가의 꿈이 된다.

0개의 댓글