javascript

실행 컨텍스트와 동기/비동기 처리 방식

Bittersweet- 2022. 8. 2. 14:20
728x90

함수를 호출하면 함수 코드가 평가되어 함수 실행 컨텍스트가 생성된다. 이 때 생성된 함수 실행 컨텍스트는 콜스택에 추가되고 함수 코드가 실행된다. 함수 코드의 실행이 종료되면 함수 실행 컨텍스트는 콜스택에서 제거된다.

함수의 실행 순서는 실행 컨텍스트 스택으로 관리하며 이는 뒤이어 설명할 동기, 비동기와도 밀접하게 연관되어 있다.

 

실행 컨텍스트(execution context)

실행 컨텍스트란?

실행할 코드에 제공할 환경 정보를 모아놓은 객체이다.

실행 컨텍스트가 활성화되는 시점에는 다음과 같은 현상이 발생한다.

  • 호이스팅이 발생한다.(선언된 변수(var)를 위로 끌어올린다)
  • 외부 환경 정보를 구성한다.
  • this의 값을 설정한다.

실행 컨텍스트의 구성

실행 컨텍스트는 코드를 실행할 때 필요한 환경 정보를 모아 컨텍스트(객체)를 구성하고, 이를 콜 스택에 쌓아올렸다가, 가장 위에 쌓여있는 컨텍스트와 관련있는 코드들을 실행하고 완료 후 콜스택에서 제거된다.

 

실행 컨텍스트를 구성할 수 있는 방법으로는

  • 전역공간(자동으로 생성)
  • 함수 실행 
  • eval 함수(보안상의 이유로 거의 사용 안함)

가 있는데 일반적으로 함수를 이용한 실행 컨텍스트를 많이 사용한다.

 

💡 콜스택(call stack)

코드가 실행되면서 생성되는 실행 컨텍스트를 저장하는 자료 구조로 a,b,c,d의 순서로 데이터를 저장했다면 꺼낼때는 반대로 d,c,b,a의 순서로 꺼낼 수 밖에 없다.(Last In First Out, 후입선출) 

 

실행 컨텍스트의 실행 순서

// --------- (1)
var a = 1; // 전역 컨텍스트
function outer() { // outer 컨텍스트
  function inner() { // inner 컨텍스트
    console.log(a);// undefined
    var a = 3;
  }
  inner(); // --------------(2)
  console.log(a); // 1
}
outer(); // ----------------(3)
console.log(a); // 1

 

실행 컨텍스트와 콜스택

 

 

 


 

 

 

동기와 비동기 처리

자바스크립트 엔진은 단 하나의 콜스택(실행 컨텍스트 스택)을 갖는다. 이는 함수를 실행할 수 있는 창구가 단 하나이며, 동시에 2개 이상의 함수를 실행할 수 없다는 것을 의미한다. 

콜스택의 최상위에 실행중인 실행 컨텍스트를 제외한 모든 실행 컨텍스트가 모두 실행 대기 중인 태스크(task)들인 셈이다. 즉, 현재 실행중인 함수가 종료하면 다름 순서의 함수가 실행되게 된다.

 

이처럼 자바스크립트 엔진은 한번에 하나의 태스크만 실행할 수 있는 싱글 스레드(single thread) 방식으로 동작한다.

싱글 스레드는 한번에 하나의 태스크만 실행할 수 있기 때문에 처리에 시간이 걸리는 태스크를 실행하는 경우 작업 중단(블로킹, blocking)이 발생한다. (예시. setTimeout)

// Sleep 함수는 일정 시간(delay)이 경과한 후 콜백 함수(func)를 호출한다.
function sleep(func, delay) {
  // Date.now()는 현재 시간을 숫자(ms)로 반환한다.
  const delayUntil = Date.now() + delay;
  
  // 현재 시간(Date.now())에 delay를 더한 delayUntil이 현재 시간보다 작으면 계속 반복한다.
  while(Date.now() < delayUntil);
  // 일정시간(delay)이 경과한 이후에 콜백 함수(func)를 호출한다.
  func();
}
  
function foo() {
  console.log('foo');
}

function bar() {
  console.log('bar');
}

// sleep 함수는 3초 후 실행된다.
sleep(foo, 3 * 1000);
// bar 함수는 sleep 함수의 실행이 종료된 이후에 호출되므로 3초 이상 블로킹 된다.
bar();
// 3초 경과 후 foo 호출 -> bar 호출

sleep 함수는 3초 후에 foo 함수를 호출하는데 이때 bar 함수는 sleep함수의 실행이 종료된 이후에 호출되므로 3초 이상(foo함수의 실행 시간 + 3초) 호출되지 못하고 블로킹된다.

 

이렇게 현재 실행 중인 태스크가 종료할 때까지 다음 실행되는 태스크가 대기하는 방식을 동기(synchronous) 처리라고 한다.

 

동기 처리 방식은 태스크를 순서대로 하나씩 처리하므로 실행 순서가 보장되지만 앞선 태스크가 종료할 때까지 이후 태스크가 모두 블로킹된다는 특징이 있다.

 

sleep 함수 대신 setTimeout 함수를 사용하게 되면 블로킹을 피할 수 있다.

// setTimeout은 일정 시간이 경과한 이후에 foo를 호출한다.
// setTimeout은 bar()를 블로킹 하지 않는다.
setTimeout(foo, 3*1000);
bar();
// bar 호출 -> (3초 경과 후) foo 호출

setTimeout 함수는 3초 이후에 콜백함수를 호출하지만 bar() 함수를 블로킹하지 않고 바로 실행한다. 이처럼 현재 실행 중인 태스크가 종료되지 않은 상태라 해도 다음 태스크를 곧바로 실행하는 방식을 비동기(asynchronous) 처리라고 한다.

 

비동기 처리 방식으 태스크의 순서를 보장되지 않지만 블로킹이 발생하지 않는다는 특징이 있다.

 

비동기 처리를 수행하는 비동기 함수는 콜백 패턴을 사용해왔다.

비동기 처리를 위한 콜백 패턴은 콜백 지옥(callback hell)을 발생시켜 가독성을 떨어뜨리고, 비동기 처리 중 발생한 에러의 예외 처리가 곤란하며, 여러 개의 비동기 처리를 한번에 처리하는 데도 한계가 있다. 이에 대해서는 나중에 프로미스(promise)를 살펴보기를 바란다.

 

타이머 함수인 setTimeout과 setInterval, HTTP 요청, 이벤트 핸들러는 비동기 방식으로 동작한다.

 

💡 이벤트 핸들러(비동기 방식)

커스텀 이벤트를 디스패치하거나 click, blur, focus 메서드 등을 호출하면 해당 이벤트 핸들러가 태스크 큐를 거치지 않고 즉시 호출된다. 즉, 동기 방식으로 처리된다.

 

 

 

이벤트 루프와 태스크 큐

자바스크립트는 싱글스레드로 동작하지만 브라우저가 동작하는 것을 보면 많은 태스크가 동시에 처리되는 듯한 느낌을 받는다.

예를 들어, HTML 요소가 애니메이션 효과를 통해 움직이면서 이벤트를 처리하기도 하고, HTTP 요청을 통해 서버로부터 데이터를 가지고 오면서 렌더링하기도 한다. 이처럼 자바스크립트의 동시성을 지원하는 것이 이벤트 루프이다.

 

이벤트 루프는 브라우저에 내장되어 있는 기능 중 하나다. 이쯤에서 브라우저 환경을 이미지로 확인해보자.

이벤트 루프와 브라우저 환경

대부분의 자바스크립트 엔진은 크게 콜스택의 영역으로 구분할 수 있다. 자바스크립트 엔진은 단순히 태스크가 요청되면 콜 스택을 통해 요청된 작업을 순차적으로 처리할 뿐이다. 

 

💡 힙(heap)

힙은 객체가 저장되는 메모리 공간이다. 콜 스택의 요소인 실행 컨텍스트는 힙에 저장된 객체를 참조한다. 객체는 원시값과 달리 크기가 정해져 잇지 않으므로 할당해야할 메모리 공간의 크기를 런타임에 결정(동적 할당)해야 한다. 따라서, 객체가 저장되는 메모리 공간인 힙은 구조화되어있지 않다는 특징이 있다.

 

비동기 처리에서 소스코드의 평가와 실행을 제외한 모든 처리는 런타임 환경인 브라우저 또는 Node.js가 담당한다. 예를 들어, 비동기 함수인 setTimeout의 콜백 함수의 평가와 실행은 자바스크립트 엔진이 담당하지만 호출 스케줄링을 위한 타이머 설정콜백 함수의 등록은 브라우저 또는 Node.js가 담당한다. 이를 위해 브라우저 환경은 태스크 큐이벤트 루프를 제공한다.

 

💡 태스크 큐(task queue/event queue/callbak queue)

setTimeout이나 setInterval과 같은 비동기 함수의 콜백함수 또는 이벤트 핸들러가 일시적으로 보관되는 영역이다. 

 

💡 이벤트 루프(event loop)

이벤트 루프는 콜 스택에 현재 실행중인 실행 컨텍스트가 있는지, 그리고 태스크 큐에 대기중인 함수(콜백함수, 이벤트 핸들러 등)가 있는지 반복해서 확인한다. 만약 콜스택이 비어있고 태스크 큐에 대기중인 함수가 있다면 이벤트 루프는 순차적으로 태스크 큐에 대기 중인 함수를 콜 스택으로 이동시킨다. 이때 콜 스택으로 이동한 함수는 실행된다. 즉, 태스크큐에 일시 보관된 함수들은 비동기 처리 방식으로 동작한다.

 

function foo() {
  console.log('foo');
}

function bar() {
  console.log('bar');
}

setTimeout(foo, 0); // 0초(실제는 4ms)후에 foo 함수 호출
bar();

비동기 함수인 setTimeout의 콜백함수(foo 함수)는 태스크 큐에 푸쉬되어 대기하다가 콜스택이 비게되면, 비로소 콜 스택에 푸쉬되어 실행된다.

 

브라우저에 내장된 자바스크립트 엔진은 싱글 스레드 방식으로 동작하지만 브라우저는 멀티 스레드 방식로 동작하기 때문에 비동기 처리가 가능하다는 것을 꼭 기억하기를 바란다.

 

브라우저는 자바스크립트 엔진 외에도 렌더링 엔진과 Web API를 제공한다. Web API는 ECMAScript 사양에 정의된 함수가 아니라 브라우저에서 제공하는 API이며, DOM API와 타이머 함수, HTTP 요청(ajax)과 같은 비동기 처리를 포함한다.

 

브라우저의 Web API인 setTimeout 함수가 호출되면 자바스크립트 엔진의 콜 스택에 푸쉬되어 실행된다.

 

하지만 setTimeout함수의 두가지 기능인 타이머 설정과 타이머가 만료하면 콜백함수를 태스크 큐에 등록하는 처리는 자바스크립트 엔진이 아닌 브라우저가 실행한다. 자바스크립트 엔진과 브라우저 엔진의 실행이 병행 처리된다. 브라우저와 자바스크립트 엔진이 협력하여 비동기 함수 setTimeout을 실행하는 셈이다.