자바스크립트 엔진은 코드를 어떻게 실행할까?
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의 콜백 함수는 어디에 가 있다가 언제 실행되는 걸까요? 여기서 이벤트 루프가 등장합니다.
전체 구조를 정리하면 이렇습니다.
[콜 스택] ← 이벤트 루프가 여기로 밀어넣어줌
↑
[이벤트 루프] ← 콜 스택이 비었는지 계속 확인
↑
[콜백 큐] ← 비동기 작업 완료된 콜백이 여기서 대기
setTimeout이 호출되면, 타이머는 브라우저(Web API) 가 관리합니다.- 2초가 지나면 콜백 함수가 콜백 큐에 들어갑니다.
- 이벤트 루프는 콜 스택이 비어있는지 계속 확인합니다.
- 콜 스택이 비면, 콜백 큐에서 대기 중인 함수를 꺼내서 콜 스택에 넣어줍니다.
이벤트 루프는 카페에서 "진동벨 울렸는데 손님이 아직 다른 일 하고 있으면 기다렸다가, 손님이 한가해지면 알려주는 직원"과 같습니다.
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가 어떻게 다른지 비교해보겠습니다.