Asynchronous(비동기)

MaxlChan·2020년 7월 31일
2

오늘은 자바스크립트에서 말하는 '비동기'에 대해 공부한 내용을 정리해보고자한다.

🚨 올바르지 않은 내용이 있을 경우 댓글로 남겨주시면 감사드리겠습니다.

우선 용어 그대로를 해석해보자

비동기(Asynchronous) - 동시에 존재(발생)하지 않는
동기(synchronous) - 동시에 발생(존재)하는

초심자에게 모든 자바스크립트 용어가 그렇듯이
나에게도 비동기(Asynchronous), 동기(synchronous)...
뭔 🐶소리인지 용어부터 너무 이해하기 힘들었다.
뭐 동시에 발생하든 안하든 어쩌라고..?

지금은 이해했다 치더라고 위 뜻을 보면 초심자는 절대 이해할 수 없다고 생각한다...

다른 사이트에서는 비동기에 대한 이해를 돕기 위해 음식점 대기번호 예시를 많이 들던데..
나는 괜히 다른 예시를 들고싶다.

방금 전 나는 축구를 하고 와서 샤워를 했다.(실제 상황임)
휴대폰으로 음악을 들으면서 샤워를 하고 싶었지만, 휴대폰이 꺼져있는 상황이었다.(진짜 이랬음)

이때,

휴대폰을 켜고, 켜질 때까지 기다린 다음, 음악을 켜놓고 샤워를 시작한다 => 동기
휴대폰을 켜놓고, 켜지는 동안 샤워를 하다가, 켜졌다는 알림이 오면 그 때 음악을 튼다 => 비동기

결국 나는 동기적인 방법을 택했다.(핸드폰에 물 묻을까봐..)

예시로만은 완벽한 이해가 어려우니 정의를 찾아보자.
괜찮은 정의가 없을까 하고 그나마 마음에 들었던 네이버 컴퓨터인터넷IT용어대사전 정의를 찾았다.
이 정의가 제일 마음에 들었다.

비동기(Asynchronous)

동기(synchronous)의 상대어.

규칙적인 시간 관계가 없는 것으로, 랜덤한 사상(事象)의 출현을 말하고,
프로그램 실행에서는 명령(실행) 순서 예측이 불가능한 것.

앞에 첫 줄은 무슨 의미인 줄 대략 알겠으나, 중요한 것은
비동기식은 실행 완료의 순서 예측이 불가능한 것이다.

그럼 동기식은? 실행 완료의 순서 예측이 가능한 것이다.

아래 코드를 보자

var a = "fisrt";

console.log(a); // "first"
console.log("second"); // "second"
console.log("third"); // "third"

코드는 위에서부터 아래로 순차적으로 실행된다.

우리는 a에 담긴 "first", "second", "third"가 순서대로 콘솔에 출력된다는 것을 예측할 수 있다.

즉, 위 코드는 동기식으로 처리된 코드이다.

이번에는 비동기식 처리 코드를 살펴보자

var a = "fisrt";

console.log(a);

setTimeout(function second() {
  console.log("second")
}, 3000);
setTimeout(function third() {
  console.log("third")
}, 1000);

console.log("fourth");

분명 함수 호출 순서는 위에서부터 아래인데 콘솔에는 순서대로가 아닌,
"first", "fourth", "third", "second" 순으로 뒤바뀌어 출력된다.
그 이유는 setTimeout함수가 비동기식으로 작동하기 때문이다.
다시 말해, 함수 실행 순서 예측이 불가능하다.

사실 지금은 우리가 함수 안에 두번 째 인자로 명시되어있는 3000, 1000을 통해
각각 3초, 1초 뒤에 콜백함수가 호출된다는 것을 예측할 수 있지만,

실제로 서버에 데이터를 요청하는 방식의 경우
언제 데이터 요청이 완료되는 지 예측할 수 없기 때문에, 콜백 함수 실행도 언제 일어날지 예측할 수 없다.
(즉, 실제로는 setTimeout 두번째 인자가 가려져있어 우리가 예상 할 수 없다고 생각하면 된다.)

사실, 자바스크립트 언어 자체가 비동기 동작을 지원하는 것은 아니다.
비동기로 동작하는 핵심요소는 자바스크립트 언어가 아니라 브라우저가 가지고 있다.

비동기 작동 방식을 이해하기 위해서는 브라우저의 Event Loop에 대한 이해가 선행되어야 한다.

이벤트 루프(Event Loop)


출처 - Poiemaweb

이벤트 루프를 이해하기 위한 브라우저의 구성 요소부터 간단히 살펴보자.

Javascript Engine

자바스크립트 언어를 이해하고 실행할 수 있는 소프트웨어로서
브라우저에 따라 여러 엔진이 있겠지만, 대표적으로는 크롬의 V8 엔진이 있다고 한다.

엔진에는 Heap Call stack이 존재하는데, 간단하게 말하자면
Heap은 함수, 배열, 객체를 새로이 만들 때 메모리 할당이 발생하는 곳이고
Call stack은 함수 호출 실행이 쌓이는 곳이다. 호출이 끝나면 콜스택에서 소멸된다.
LIFO(Last In, First Out) 방식 동작하기 때문에, 가장 늦게 호출된 함수가 가장 일찍 소멸된다.

자바스크립트는 단일 스레드 프로그래밍 언어이므로, 하나의 호출 스택만 있다.
하나의 호출 스택이 있다는 뜻은 한 번에 하나의 일(Task)만 처리할 수 있다는 뜻이다.

Web API

DOM 관련 작업 (document), Timer 관련 작업 (setTimeout, setInterval 포함),
AJAX 관련 작업 (fetch) 등 Web API는 브라우저에서 제공하는 API라고 이해하면 된다.

Call Stack에서 비동기 함수(fetch 등) 혹은 setTimeout, setInterval등의 호출이 있을 때,
Web API로 이동한다.

그리고 해당 함수 호출은 Call stack에서 즉시 소멸한다.
데이터 요청이 완료되거나, 정해진 시간에 도달(완료)하면 완료된 순서대로 Web API에서 Callback Queue로 이동한다.
Web API에 진입한 순서가 아니라 완료된 순서이기 때문에 Web API에 먼저 들어왔다 한들, Callback Queue로 이동하는 순서는 완료 시간에 따라 뒤바뀔 수 있다.

Callback Queue

Web API에서 데이터 요청이나 시간이 완료되어 넘어온 콜백 함수들의 대기열이다.
Call stack과 달리 FIFO(First In, First Out) 방식이기 때문에,
무조건 먼저 들어온 콜백 함수가 차후에 먼저 실행되는 구조이다.

Event Loop

드디어 이벤트 루프이다. 이벤트 루프는 크게 두가지 역할을 한다.

1. Call Stack과 Callback Queue를 수시로 감시
2. Call Stack이 비어있을 경우, Callback queue에서 대기하고 있는 함수를 꺼내 Call Stack에 추가

여기서 중요한 것은 Call Stack이 비었다고 Callback queue에 있는 함수들을 주구장창 Call Stack 추가하는 것이 아니라 대기 순서대로 하나씩 추가한다는 점이다.

var a = "fisrt";

console.log(a);

setTimeout(function second() {
  console.log("second")
}, 3000);

setTimeout(function third() {
  console.log("third")
}, 1000);

console.log("fourth");

그럼 위의 비동기 함수가 어떻게 Event Loop를 통해 실행되는 지 정리해보자

  1. Call stack에 console.log(a)가 쌓인 후 콘솔에 "fisrt" 출력 후 소멸
  2. Call stack에 1번째 setTimeout함수가 쌓인 후 곧장 소멸하고 Web API로 이동(3초 카운트 다운 시작)
  3. Call stack에 2번째 setTimeout함수가 쌓인 후 곧장 소멸하고 Web API로 이동(1초 카운트 다운 시작)
  4. Call stack에 console.log("fourth")가 쌓인 후 콘솔에 "fourth" 출력 후 소멸
    (1초 후 2번 setTimeout함수의 콜백함수(third)가 callback Queue에 첫번째 순서로 대기,
    3초 후 1번 setTimeout함수의 콜백함수(second)가 callback Queue에 두번째 순서로 대기)
  5. Event Loop은 Call stack이 비어있는 것을 확인 후, callback Queue에 첫번째 순서로 대기 중인 third 콜백함수를 Call stack에 추가, 그 스택 위에 console.log("third")가 쌓이고 콘솔에 "third" 출력 후 소멸, 이후 아래 있던 third 콜백함수 또한 Call stack에서 소멸.
  6. 다시 Event Loop은 Call stack이 비어있는 것을 확인 후, callback Queue에 두번째 순서로 대기 중인 second 콜백함수를 Call stack에 추가, 그 스택 위에 console.log("second")가 쌓이고 콘솔에"second" 출력 후 소멸, 이후 아래 있던 third 콜백함수 또한 Call stack에서 소멸.

글로 설명하면 이해가 잘 안될 수 도 있으니 해당 링크에 들어가서
위 함수를 실행해보면 어떻게 Event Loop가 작동하여 비동기 함수를 호출하는 지에 대해 시각적으로 확인 할 수 있다.

중요한 것은 setTimeout의 delay인자는 Web API 진입한 다음 delay후에 Callback Queue에 들어간다는 것을 보장하는 것이지, delay후에 콜백함수가 실행되는 것을 보장하지 않는다 점이다.(콜 스택이 완전히 비워질 때까지의 시간이 있기 때문에)

따라서

setTimeout(function one() {
  console.log(1)
}, 0);

console.log(2);

위 코드도 직관적으로는 봤을 때는 0초 후에 콜백함수 one이 바로 호출되는 것 처럼 보이지만 , 실제로는 위 Event Loop원리에 따라 2, 1 순서로 출력되는 것을 확인할 수 있다.

비동기 작동 방식이 중요한 이유

그럼 대체 우리는 이 비동기 작동 방식을 왜 쓰는가?

아래 예시를 보자.

for (let i = 0; i < 50000; i++) {
  console.log(i);
}

console.log("When I run...");

컴퓨터 성능에 따라 다르겠지만, 위 for문이 끝나기 전까지 꽤나 오랫동안 콘솔에 "When I run..."이 출력되지 않는다.

위에서 말했듯이 자바스크립트는 단일 쓰레드이기 때문에, 하나의 콜스택만을 가지고 있므로,
한 번에 하나의 작업만을 처리할 수 밖에 없기 때문이다.

코드는 위에서부터 아래로 실행되지 않는가?
즉, 위쪽의 코드 실행문이 종료되어야지만 아랫쪽인 다음 코드가 실행되고 동작 할 수 있다.

그런데 만약 위 for문이
서버에 이미지를 요청한 후 홈페이지 메인 이미지로 출력하는 함수라고 가정해보자.
(소요되는 시간 또한 위 for문 만큼 걸린다고 가정)

그러면 실제 홈페이지를 방문하는 사용자 입장에서는 메인 이미지가 홈페이지에 출력되기 전까지
for문 아래 코드에 구현 있는 기능들을 전혀 사용하지 못한다는 것이다.

요즘에 이런 홈페이지를 본적이 있던거? 거의 없을 것이다.

그 이유는 다 비동기 방식을 활용한 코드를 구현했기 때문이다.

setTimeout(function() {
    console.log("get main image from server");
}, 5000); // 서버에서 데이터를 가지고 오는 시간이 5초 정도 걸린다고 가정.

console.log("run immediately..."); // 데이터 가지고 오는 실행문과 별개로 실행됨.

위 코드처럼 비동기 방식으로 구현하면 우리는 메인 이미지가 출력되기 전에
홈페이지에 다른 기능들을 사용할 수 있게 된다.
마치 내가 휴대폰을 켜놓으면서, 샤워를 할 수 있었던 것처럼 말이다.

만약 비동기라는 것이 이 세상에 존재하지 않았다면,
사람들은 메인 이미지가 뜰 때까지 기다려야했을 것이고
나는 휴대폰이 켜질 때까지 샤워를 하지 못했을 것이다.

이후에 비동기의 흐름을 처리하기 위한 방법(콜백함수, Promise, async/await)에 대해서
더 블로깅을 하려했으나 글이 길어진 관계로 다음 글에서 다루도록 하겠다.

참고

profile
한가지를 알아도 제대로 알자

1개의 댓글

comment-user-thumbnail
2021년 6월 25일

너무 잘 읽었습니다 :)

답글 달기