[Day26] Javascript -콜스택(Call Stack)

Validator·2023년 7월 17일
0

자바스크립트 콜 스택(Call Stack)이란?

자바스크립트는 점점 대중화, 확장되고 있으며 그 범위는 프론트엔드 뿐만 아니라 백엔드, 임베디드 프로그래밍까지 영향을 미치고 있다.

그러나 실제로 자바스크립트의 내부 동작을 이해하는 것은 그리 간단하지 않다. 예를 들면, 함수의 호출이나 이벤트 루프의 동작 원리와 같은 부분을 말한다. 자바스크립트를 더 깊게 이해함으로써, 더 나은 코드와 앱을 작성할 수 있게 되므로, 이 부분을 반드시 짚고 넘어가야 한다.

자바스크립트 엔진

가장 대중적인 자바스크립트의 엔진은 구글의 V8 엔진이다. V8 엔진은 크롬과 노드 안에서 동작한다.(Node JS) 자바스크립트 엔진은 다음과 같은 두 가지 주요 구성 요소로 이루어져 있다.

메모리 힙(Memory Heap) — 객체는 힙, 대부분 구조화되지 않은 메모리 영역에 할당된다. 변수와 객체에 대한 모든 메모리 할당은 여기서 발생한다.

호출 스택(Call Stack) — 코드가 실행될 때 호출 스택이 쌓인다.

실행 환경(Runtime)

브라우저에는 자바스크립트 개발자가 사용하는 거의 모든 API가 존재히ㅏㄴ다(예: setTimeout ). 그러나 이런 API 들은 엔진에서 제공해주지 않는다. 그렇다면 이 API들은 어디서 오는 것일까?

사실 브라우저는 단순히 엔진 하나만으로 구성되어 있지 않다. DOM, AJAX, setTimeout 등의 브라우저에서 제공하는 Web API라고 하는 것들이 있다. 또한 이러한 Web API의 호출을 통제하기 위한 Event Queue와 Event Loop도 존재한다.(이벤트 큐는 콜백 큐라고도 불리기도 한다)

사실 브라우저는 단순히 엔진 하나만으로 구성되어 있지 않다. DOM, AJAX, setTimeout 등의 브라우저에서 제공하는 Web API라고 하는 것들이 있다. 또한 이러한 Web API의 호출을 통제하기 위한 Event Queue와 Event Loop도 존재한다.


호출 스택(Call Stack)

자바스크립트는 단일 스레드 프로그래밍 언어이므로, 단일 호출 스택으로 구성돼있다. 단일 호출 스택이 있다는 뜻은 한 번에 하나의 일(Task)만 처리할 수 있다는 뜻이다. (stack은 마지막에 들어온 것이 가장 먼저 처리된다)

호출 스택이란 프로그램에서 우리가 어디에 있는지를 기본적으로 기록하는 데이터 구조이다. 동작 방식은 다음과 같다. 함수를 실행하면 해당 함수의 기록을 스택 맨 위에 추가(Push)하게된다. 우리가 함수를 결과 값을 반환하면 스택에 쌓여있던 함수는 제거(Pop) 된다.

예시)

function multiply(x, y) {
    return x * y;
}
function printSquare(x) {
    var s = multiply(x, x);
    console.log(s);
}
printSquare(5);

엔진이 이 코드를 실행하기 전에는 호출 스택이 비어있는 것을 볼 수 있다. 가장 아랫줄에 printSquare 함수가 실행되면 이후 단계는 다음과 같다.

호출 스택의 각 항목을 스택 프레임이라고 한다.


예외 처리 시 스택의 동작

아래의 코드는 예외가 던져질 때 스택의 호출이 어떤 순서로 일어나는지 알려준다.

예시)


function foo() {
    throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
    foo();
}
function start() {
    bar();
}
start();

만약 위의 코드가 크롬에서 실행된다면 아래와 같은 순서로 에러가 발생할 것이다.

스택 오버플로우

이름 그대로 스택의 사이즈를 초과 했을 때 발생하는 오류이다. 스택 오버플로우는 생각보다 쉽게 일어날 수 있다. 특히 재귀를 호출했을 때 쉽게 확인 가능하다.

function foo() {
    foo();
}
foo();

마지막 줄에서 foo() 함수가 실행되는데, foo() 함수의 내부를 살펴보면 종료 조건 없이 자신을 계속해서 호출하게 된다. 따라서 함수의 스택 프레임이 계속해서 호출 스택에 쌓이게 된다.

그러다가 어떠한 시점에서 호출 스택의 함수 호출 수가 호출 스택의 실제 크기를 초과하게 되고, 브라우저는 다음과 같은 오류를 발생시키는 것으로 함수를 종료 시킨다.


단일 호출 스택의 문제점

단일 스레드에서 코드를 실행하는 것은 멀티 스레드 환경에서 발생하는 복잡한 시나리오(예: deadlocks)를 고려할 필요가 없으므로 이해하기 쉽다. 그러나 단일 스레드에서 실행하는 것에는 다른 제약이 따른다. 자바스크립트에서는 하나의 호출 스택만 있기 때문에, 하나의 함수 처리가 엄청 느려서 다른 함수 실행에 지장을 줄 때가 존재한다.

예를 들어, 브라우저에서 복잡한 이미지 처리를 한다고 가정해보자. 앞서 배운 호출 스택의 동작 방식을 생각 해볼 때, 이미지 처리 작업 스택을 차지하고 있으면 자바스크립트는 후속 작업들을 처리할 수 없게된다. 이것은 단일 스레드, 단일 호출 스택이기 때문이다.

브라우저가 호출 스택에서 많은 작업을 처리하기 시작하면 꽤 오랜 시간 동안 응답을 멈추게 될 수 있다. 대부분의 브라우저는 이 상황에서 웹 페이지를 종료할지 여부를 묻는 오류 메시지를 표시한다. 그렇다면 해결 방법은 어떤 것이 있을까?

비동기 콜백(Asynchronous callbacks)

가장 쉬운 해결책은 비동기 콜백을 사용하는 것이다. 즉, 우리의 코드 일부를 실행하고 나중에 실행될 콜백(함수)를 제공하는 것이다. 비동기 콜백은 즉시가 아닌, 특수한 시점에 실행되므로 console.log와 같은 동기 함수와는 다르게 스택 안에 바로 push 될 필요가 없다. 그런데 스택이 아니라면 이 콜백 함수들은 어디서 관리되고 있는 것인가?

이벤트 큐(Event Queue)와 비동기 콜백의 처리 과정

자바스크립트 실행환경(Runtime)은 이벤트 큐(Event Queue)를 가지고 있다. 이는 처리할 메시지 목록과 실행할 콜백 함수 들을 저장한 곳이다.

그럼 비동기가 처리되는 과정을 살펴보자. 우선 버튼 클릭과 같은 이벤트가 발생하면 DOM 이벤트, http 요청, setTimeout 등과 같은 비동기 함수는 C++로 구현된 web API를 호출하며, web API는 콜백 함수를 이벤트 큐(콜백 큐)에 전달한다. 그럼 이벤트 큐는 대기하다가 콜 스택이 비는 시점에 이벤트 루프를 돌리게 됩니다(스택에 넣음). 이벤트 루프의 기본 역할은 큐와 스택, 두 부분을 지켜보다가 스택이 비는 시점에 콜백을 실행시켜 주는 것.

웹 브라우저에서는 이벤트가 발생할 때마다 메시지가 추가되고 이벤트 리스너가 첨부된다. 따라서 리스너가 없으면 이벤트가 손실됩니다. 콜백 함수의 호출은 호출 스택의 초기 프레임으로 사용되며, 자바스크립트가 싱글 스레드이므로 스택에 대한 모든 호출이 반환될 때까지 메세지 폴링(polling) 및 처리가 중지됩니다. 동기식 함수 호출은 이와 반대로 새 호출 프레임을 스택에 추가합니다.



배열에서 원치 않는 undefined를 제거하는 방법

let items1 = ["banana", "kiwi", "grape"];
let items2 = ["banana", "strawberry", "grape", "strawberry"];

function select(input1, input2) {
  let result = input1.map((item) => {
    if (input2.includes(item)) {
      return item;
    } else {
      return;
    }
  });
  return result;
}

console.log(select(items1, items2)); 
// 출력값은 이렇게 나온다! [ 'banana', undefined, 'grape' ]

로직을 처리하던중 배열에서 undefined를 제외한 후 새로운 배열을 가져와야 하는 상황이 발생했다. array에서 undefined를 제거하는 방법은 2가지를 사용할 수 있다.

undefined 제거

아래와 같은 배열이 있다고 가정해보자.

const data = [21, undefined, undefined, 9, true, false, undefined, null, 'a', 1, 0, '0'];

배열에서 undefined를 제거하는 방법은 간단하게 2가지 방법이 있다.

1. 배열에서 정확히 undefined만 제거
아래와 같이 코드를 실행하면 [21, 9, true, false, null, "a", 1, 0, "0"] 을 반환한다.

const removeUndefinedList = data.filter(data => data !== undefined);

2. 배열에서 undefined를 포함한 0, null, false도 제거
아래와 같이 코드를 실행하면 [21, 9, true, "a", 1, "0"] 을 반환한다.

const removeAllFalse = data.filter(data => data);

결론

일반적으로 배열에서 undefined와 함께 null, 0, false가 들어있는 경우 실제로 필요한 값일 경우가 많다. 특별한 경우가 아니라면 첫번째 방법을 사용하여 혹여나 의도치 않게 누락되는 값이 없도록, 미연에 오류를 방지하도록 하는 것이 좋다.

0개의 댓글