Call Stack

contability·2024년 4월 17일
0

javascript개념

목록 보기
1/1

js의 범위는 점점 확장 되고 있고 프론트엔드 뿐만 아니라 백엔드, 임베디드 프로그래밍까지 영향을 미치고 있다.

하지만 실제로 js의 내부 동작을 이해하는 사람은 그렇게 많지 않다. 예를 들어, 함수의 호출이나 이벤트 루프의 동작 원리와 같은 부분이다.
필요한 부분이라고 생각하기에 스스로도 정리하고 넘어가고 싶어 포스팅한다.

자바스크립트 엔진


가장 유명하고 보편적인 js 엔진은 구글의 V8 엔진이다.
V8 엔진은 크롬과 노드 안에서 동작한다.

그리고 js 엔진은 두 가지 주요 구성 요소로 이루어진다.

  • 메모리 힙(Memory Heap) - 객체는 힙, 대부분 구조화되지 않은 메모리 영역에 할당되며 변수와 객체에 대한 모든 메모리 할당은 여기서 발생한다.
  • 호출 스택(Call Stack) - 코드가 실행될 때 호출 스택이 쌓인다.

실행 환경(Runtime)


브라우저에는 js 개발자가 사용하는 대부분의 API가 있다. ex) setTimeout
하지만 이런 API를 엔진이 제공해주지는 않는다. 이것들은 어디서 나오는 걸까?

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

호출 스택(Call Stack)


js는 싱글 스레드 프로그래밍 언어이므로, 단일 호출 스택이 있다. 그러므로 한 번에 하나의 일(Task)만 처리할 수 있다는 뜻이 된다.
호출 스택이란 프로그램에서 우리가 어디에 있는지를 기본적으로 기록하는 데이터 구조다.

동작 방식

함수를 실행하면 해당 함수의 기록을 push(스택 맨 위에 추가)하고 함수 결과 값을 반환하고 나면 스택에 쌓여있던 함수는 pop(제거) 된다.

const multiply = (x: number, y: number) => {
	return x * y;
}

const printSquare = (x: number) => {
    const s = multiply(x, x);
    console.log(s);
}

printSquare(5);

엔진이 이 로직을 실행하기 전에는 호출 스택이 비어있다가
printSquare 함수를 실행하게 되면 콜 스택의 상태는 다음과 같이 변화한다.

이런 흐름이 되며 호출 스택의 각 항목을 스택 프레임이라고 한다.

예외 처리 시 동작


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

const foo = () => {
	throw new Error('session stack will help you resolve crashes');
}
const bar = () => {
	foo();
}
const start = () => {
	bar();
}
start();

위 코드를 실행하면 이런 순서로 에러가 발생하게 된다.

Stack Overflow


스택의 사이즈를 초과 했을 때 발생하는 오류다.
재귀 호출과 같은 상황에서 흔히 발생한다.

const foo = () => {
	foo();
}
foo();

마지막에 foo 함수가 실행되는데 foo 함수 내부에 종료 조건 없이 자신을 계속 호출하는 재귀 호출이 일어나게 된다.
즉, 함수의 스택 프레임이 계속해서 호출 스택에 쌓이게 되는 상황이다.

그렇게 계속 쌓이다가 호출 스택의 함수 호출 수가 호출 스택의 실제 크기를 초과하게 되면
브라우저는 다음과 같은 오류를 발생 시키며 함수를 종료 시킨다.

단일 호출 스택의 문제점


멀티 스레드 환경에서의 복잡한 시나리오 (ex. deadlocks)에 비해
단일 스레드의 코드 실행은 매우 쉬운 편이지만 제한적이기도 하다.

하나의 호출 스택만 존재하는 이유로
처리 속도가 오래 걸리는 하나의 함수 때문에 다른 함수 실행에 지장을 줄 때는 어떻게 처리 해야 할까?

예로 브라우저에서 복잡한 이미지 처리를 한다고 생각해보면
앞서 얘기한 호출 스택 동작 방식으로 볼 때, 이미지 처리 작업 스택 때문에 후속 작업을 처리할 수가 없다.

또 다른 문제로 호출 스택에서 많은 작업을 처리하기 시작하면 오랜 시간 응답을 멈출 수도 있다.
브라우저는 이럴 때 웹 페이지를 종료할지 여부를 묻는 메시지를 표시한다.

비동기 콜백(Asynchronous callbacks)


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

Event Queue와 비동기 콜백의 처리


js 실행환경(Runtime)은 이벤트 큐(Event Queue)를 가지고 있다.
이는 처리할 메시지 목록과 실행할 콜백 함수들의 리스트다.

과정을 살펴보면 먼저 버튼 클릭과 같은 이벤트가 발생하면 DOM 이벤트, http 요청, setTimeout과 같은 비동기 함수는 C++로 구현된 web API를 호출하게 되고, web API는 콜백 함수를 이벤트 큐(Callback Queue)에 밀어 넣는다.
그럼 이벤트 큐는 대기하다가 스택이 텅 비는 시점에 이벤트 루프를 돌리게 된다(스택에 들어감).

이벤트 루프의 기본 역할은 큐와 스택, 두 부분을 지켜보다가 스택이 비는 시점에 콜백을 실행시켜 주는 것이다.

각 메시지와 콜백은 다른 메시지가 처리되기 전에 완전히 처리된다.

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

0개의 댓글