본문 바로가기

웹개발/javascript

자바스크립트의 비동기 처리 기본 1

 

비동기란? 

기본개념

일반적으로 코드는 순차적으로 진행된다. 만약 어떤 함수의 결과가 다른 함수에 영향을 준다면, 그 함수가 끝나고 값을 산출할 때까지 기다려야한다(동기). 유저 입장에서 보면 이건 마치 전체 프로그램이 멈춘것처럼 느껴진다(blocking). 이렇게 기다리지 않고 동시에 다른 작업을 수행할 수 있는 것이 비동기의 기본적인 개념이다. 

 

blocking

웹 앱이 브라우저에서 특정한 코드를 실행하느라 브라우저에게 제어권을 돌려주지 않아 브라우저가 정지된 것처럼 보이는 현상, 즉 사용자의 입력을 처리하느라 웹 앱이 프로세서에 대한 제어권을 반환하지 않는 현상을 말한다. (이벤트 처리가 일어나는 동안 다음작동을 막아버림)

 

왜 이런 일이 일어날까?

기본적으로 자바스크립트는 싱글 스레드이기 때문이다. 

 

스레드(Threads)

기본적으로 프로그램이 작업을 완료하는 데 사용할 수 있는 단일 프로세스, 각 스레드는 한번에 하나의 일을 수행한다. 

 

현재는 많은 컴퓨터들이 여러개의 CPU코어를 가지고 있기 때문에 멀티 스레드를 지원하는 언어는 여러개의 코어를 가지고 한번에 여러 일을 수행할 수 있다. 하지만 자바스크립트는 싱글 스레드로 컴퓨터가 여러개의 CPU를 가지고 있어도 main thread라고 불리는 싱글 스레드에서만 작업할 수 있다. 

 

 블로킹의 문제 때문에 많은 웹 API기능은 현재 비동기 코드를 사용하여 실행한다. 특히 외부 디바이스에서 어떤 리소스를 가져오거나 하는 처리하는 데에 시간이 얼마나 걸리는지 예측할 수 없는 기능들에 많이 쓰인다.

 

하지만 blocking과 non-blocking 그리고 asynchronous와 synchronous는 같은 개념이 아니다. 둘은 관심사가 다르다. 

 

blocking/non-blocking 는 호출되는 함수가 바로 리턴하는지 아닌지가 관심사다. 

호출된 함수가 바로 리턴하여 브라우저에게 제어권을 넘겨주고 호출한 함수가 다른 일을 할 수 있는 것이 non-blocking이다. 

 

asynchronous/synchronous 는 호출된 함수의 작업 완료 여부를 누가 신경쓰느냐가 관심사이다. 

호출되는 함수에게 callback을 전달해서 호출되는 함수의 작업이 완료되면 호출받은 함수가 전달받은 callback을 실행하고 호출하는 함수의 작업 완료 여부를 신경쓰지 않는다면 asynchronous이다. 

반면 호출하는 함수의 작업 완료를 함수가 호출되는 함수의 작업 완료를 기다리거나 호출되는 함수로부터 바로 리턴을 받더라도 작업 완료여부를 계속해서 확인한다면 synchronous이다.

 

이 네가지가 조합된 모델로 처리를 하는데 이 중 최악으로 NonBlocking-Async 방식을 쓰는데 그 과정 중에 하나라도 Blocking으로 동작하는 놈이 포함되어 있다면 의도하지 않게 Blocking-Async로 동작할 수 있다 (Nodejs, MySQL 조합- Node.js 쪽에서 callback 지옥을 헤치면서 Async로 전진해와도, 결국 DB 작업 호출 시에는 MySQL에서 제공하는 드라이버를 호출하게 되는데, 이 드라이버가 Blocking 방식).

 

결국 성능과 자원의 효율적 사용 관점에서 가장 유리한 모델은 Async-NonBlocking 모델이다.

 


 

이벤트 루프

자바스크립트는 이벤트 루프를 이용해서 비동기 방식으로 동시성을 지원한다.

동시성이란 입출력 처리는 시작만 해둔 채 완료되지 않은 상태에서 다른 처리 작업을 계속 진행할 수 있도록 멈추지 않고 입출력 처리를 기다리는 방법을 말한다. 위에서 말한Async-NonBlocking 모델이다. 

 

실제 자바스크립트 엔진은 메모리가 할당되는 콜스택으로만 구성되어 있다. 

콜스택: 브라우저 스펙이 아닌 자바스크립트 엔진의 한 부분이다. FIFO, 함수를 실행시키면 콜스택에 쌓이고 다시 하나씩 꺼내서 실행하게 된다.

 

그래서 자바스크립트에는 이벤트 루프가 없다. 자바스크립트 엔진은 단일 호출 스택으로 그 순차적으로 처리가 될 뿐이다. 

비동기 요청은 엔진을 구동하는 런타임 환경(브라우저나 Nodejs)에서 담당한다. 

런타임 환경에서 제공하는 것은 Web API(DOM(document),AJAX(XMLHttpRequest),Timeout(setTimeout))와 이벤트 루프 그리고 task Queue이다.

 

이벤트 루프task Queue에 태스크가 들어오길 기다렸다가 태스크가 들어오고 호출 스택이 비어있다면 첫번째 태스트를 호출 스택에 넣어 이를 처리하고, 태스크가 없는 경우 잠드는 끊임 없이 돌아가는 자바스크립트 내의 루프이다. 

  • 자바스크립트가 돌아가는 알고리즘: 처리할 테스크가 있는 경우 -> 호출 스택이 비어있는지 확인 -> 먼저 들어온 태스크부터 처리 -> 없는 경우 -> 잠듦 -> 새로운 테스크가 들어오면 이 과정 반복

이러한 반복을 이벤트 루프에서는 tick이라고 한다.

이렇게 자바스크립트는 스크립트나 핸들러, 이벤트가 활성화 될 경우에만 돌아간다. 

  • 외부 스크립트 <script src="...">가 로드될 때, 이 스크립트를 실행하는 것
  • 사용자가 마우스를 움직일 때 mousemove 이벤트와 이벤트 핸들러를 실행하는 것
  • setTimeout에서 설정한 시간이 다 된 경우, 콜백 함수를 실행하는 것

태스크는 엔진이 바쁠때도 추가될 수 있다. 이 때의 태스크는 task Queue에 들어간다. 테스크가 들어가는 큐를 매크로태스크 큐라고 한다. 

 

추가로 효율적인 비동기 처리를 위해 자바스크립트는 PromiseJobs라는 내부 큐(internal queue)인 '마이크로태스크 큐(microtask queue)'를 명시한다.

  • 마이크로태스크 큐는 코드를 이용해서만 생성하는데 주로 프로미스를 이용해 만든다. 프로미스와 함께 쓰이는 핸들러(then,catch,finally)가 마이크로태스크가 된다. 여기에 더해 프로미스를 핸들링하는 다른 문법인 await로도 만들어 진다. 
  • 자바스크립트 엔진은 매크로태스크 하나를 처리하고 난 직후, 다른 매크로태스크나 렌더링 작업을 하기 전에 마이크로태스크 큐에 있는 마이크로태스크 전부를 처리한다.
  • 즉, 다른 이벤트 핸들링이나 랜더링 작업 전, 다른 매크로태스크가 실행전에 처리된다. 
  • 이런 처리순서가 아주 중요한 이유는 마이크로태스크 간 동일한 환경에서 처리할 수 있다는 것을 보장하기 때문이다.(랜더링이 안됨과 다음 마이크로태스트 실행이 안됨을 보장하기 때문에).

 

그래서 이벤트 루프의 총 실행 순서로는 

  1. 매크로태스크 큐에서 가장 오래된 태스크를 꺼내 실행한다(FIFO).
  2. 마이크로태스크 큐가 빌 때까지 모든 마이크로태스크를 실행한다(FIFO).
  3. 렌더링할 것이 있으면 처리한다.
  4. 매크로태스크 큐가 비어있으면 새로운 매크로태스크가 나타날 때까지 기다린다.
  5. 1번으로 돌아가서 반복한다.

이렇게 진행된다. 

 

 

여기서 순차적으로 처리되는 태스크들에서 특징이 몇가지 있다. 

  • 엔진이 태스크를 처리하는 동안에는 랜더링이 일어나지 않는다. 태스크 처리 시간이 짧으면 끝나고 바로 DOM랜더링을 하면 됨으로 문제가 되지 않는다. 하지만 태스크 처리 시간이 길면 브라우저 처리 시간동안 발생한 사용자 이벤트등 새로운 태스크를 처리하지 못하는 문제가 생긴다. 이런 상황이 blocking 현상을 만든다.
  • 이러한 불가피한 상황들을 태스크를 여러 조각으로 쪼개어 예방할 수 있다. 

효과적인 비동기 처리란 스택에 필요없는느린 코드를 쌓아서 브라우저가 할 일을 못하게 막지 마라 유동적인 ui를 만들어야 한다는 것이다. 

 

 

 

 

콜백을 사용한 비동기적 프로그래밍 

콜백함수를 인자로 받아 함수 실행이 끝난후 콜백함수를 실행할 수 있게 하는것

어떤 함수의 결과값이 다음함수에 끼치는 영향이 크고 그것들이 여러개로 이어져 있다면 콜백함수가 여러개가 중첩되는 콜백지옥에 빠진다.