기술 포스트

자바스크립트 엔진은 코드를 어떻게 실행할까?


Article

퍼블리셔로 일하면서 자바스크립트를 써본 적은 있을 거다. 클릭 이벤트 달고, 탭 메뉴 만들고, 슬라이더 라이브러리 붙이고. 그런데 자바스크립트가 내부적으로 어떻게 돌아가는지는 잘 모르는 경우가 많다.

React를 배우기 전에, 자바스크립트 엔진의 동작 원리를 한번 짚고 넘어간다.

자바스크립트는 한 번에 한 가지만 한다

자바스크립트는 싱글 스레드 언어다. 한 번에 하나의 작업만 처리할 수 있다는 뜻이다.

식당에 요리사가 한 명뿐인 것과 같다. 주문이 아무리 많이 들어와도 한 접시씩 순서대로 만들 수밖에 없다.

그런데 우리가 실제로 웹사이트를 쓸 때는 여러 가지가 동시에 일어나는 것처럼 보인다. 이게 어떻게 가능한 걸까? 그 답이 이벤트 루프에 있다.

콜 스택 — 접시 쌓기

자바스크립트 엔진에는 콜 스택(Call Stack) 이라는 게 있다. 실행할 함수들이 쌓이는 곳이다.

접시를 쌓는 걸 상상해보자. 새 접시는 맨 위에 올리고, 꺼낼 때도 맨 위부터 꺼낸다. 나중에 들어온 게 먼저 나가는 구조다.

function first() {
  second();
  console.log("첫 번째");
}

function second() {
  console.log("두 번째");
}

first();

이 코드가 실행되면 콜 스택에서 이런 일이 벌어진다.

1. first()가 스택에 올라감
2. first() 안에서 second()를 호출 → second()가 스택 맨 위에 올라감
3. second() 실행 완료 → "두 번째" 출력 → second()가 스택에서 빠짐
4. first() 나머지 실행 → "첫 번째" 출력 → first()가 스택에서 빠짐

단순하다. 함수가 호출되면 쌓이고, 실행이 끝나면 빠지고. 이게 콜 스택의 전부다.

문제 — 오래 걸리는 작업이 있으면?

콜 스택에서 하나의 작업이 오래 걸리면 어떻게 될까? 그 뒤에 있는 모든 작업이 기다려야 한다.

서버에서 데이터를 가져오는 데 3초가 걸린다고 해보자. 그 3초 동안 버튼 클릭도 안 되고, 스크롤도 안 되고, 화면이 완전히 멈춘다. 사용자 입장에서는 브라우저가 죽은 것처럼 보인다.

이걸 해결하기 위해 비동기 처리가 등장한다.

비동기 처리 — 카페 주문 시스템

카페에서 커피를 주문하는 걸 생각해보자.

동기 방식: 주문하고 카운터 앞에서 커피가 나올 때까지 서서 기다린다. 그동안 아무것도 못 한다.

비동기 방식: 주문하고 진동벨을 받는다. 자리에 앉아서 다른 일을 하다가 벨이 울리면 가서 커피를 받는다.

자바스크립트의 비동기 처리가 바로 이 진동벨 방식이다.

console.log("주문 시작");

setTimeout(() => {
  console.log("커피 완성!");
}, 2000);

console.log("자리에 앉아서 책 읽는 중");

출력 순서:

주문 시작
자리에 앉아서 책 읽는 중
커피 완성!          ← 2초 후에 출력

setTimeout은 비동기 함수다. 2초를 기다리는 동안 콜 스택을 차지하지 않고, 나머지 코드가 먼저 실행된다.

이벤트 루프 — 진동벨을 확인하는 직원

그러면 setTimeout의 콜백 함수는 어디에 가 있다가 언제 실행되는 걸까? 여기서 이벤트 루프가 등장한다.

전체 구조를 정리하면 이렇다.

[콜 스택]  ←  이벤트 루프가 여기로 밀어넣어줌
                    ↑
              [이벤트 루프] ← 콜 스택이 비었는지 계속 확인
                    ↑
              [콜백 큐]  ← 비동기 작업 완료된 콜백이 여기서 대기
  1. setTimeout이 호출되면, 타이머는 브라우저(Web API) 가 관리한다.
  2. 2초가 지나면 콜백 함수가 콜백 큐에 들어간다.
  3. 이벤트 루프는 콜 스택이 비어있는지 계속 확인한다.
  4. 콜 스택이 비면, 콜백 큐에서 대기 중인 함수를 꺼내서 콜 스택에 넣어준다.

이벤트 루프는 카페에서 "진동벨 울렸는데 손님이 아직 다른 일 하고 있으면 기다렸다가, 손님이 한가해지면 알려주는 직원"과 같다.

setTimeout(fn, 0)이 즉시 실행되지 않는 이유

이걸 알면 재미있는 현상이 이해된다.

console.log("A");

setTimeout(() => {
  console.log("B");
}, 0);

console.log("C");

출력 순서:

A
C
B

0밀리초로 설정했는데 왜 B가 마지막에 나올까?

setTimeout의 콜백은 시간이 0이든 1000이든 무조건 콜백 큐를 거친다. 이벤트 루프는 콜 스택이 완전히 빌 때까지 기다렸다가 콜백을 넣어주기 때문에, 동기 코드(A, C)가 전부 실행된 후에야 B가 실행된다.

왜 이걸 알아야 할까?

퍼블리셔 시절에는 몰라도 괜찮았던 개념이다. 하지만 React를 쓰기 시작하면 상황이 달라진다.

  • 상태가 바뀌었는데 화면에 바로 반영이 안 되는 경우 — React의 상태 업데이트가 비동기로 처리되기 때문이다.
  • API에서 데이터를 가져와서 화면에 보여주는 패턴 — 비동기 처리의 이해가 필수다.
  • useEffect 안에서 일어나는 일들 — 이벤트 루프와 실행 타이밍의 이해가 필요하다.

콜 스택과 이벤트 루프를 이해하고 있으면, React에서 마주치는 "왜 이게 이렇게 동작하지?" 하는 순간에 답을 찾기가 훨씬 쉬워진다.

다음 글에서는 jQuery 방식의 DOM 조작과 React의 선언적 UI가 어떻게 다른지 비교해본다.

자바스크립트 엔진은 코드를 어떻게 실행할까? — 디코드랩(DCODELAB)