브라우저의 주요 기능은, '선택한 자원을 서버에게 요청하고, 전송받은 자원을 브라우저 화면에 표시' 하는 것이다.
간단히 브라우저의 동작 원리를 보자면,
- HTML 파싱 → DOM 트리 생성
- CSS 파싱 → CSSOM 트리 생성
- DOM과 CSSOM을 결합하여 Render Tree를 만들고, 브라우저가 이를 기반으로 화면을 그린다.
HTML을 파싱하던 중 <script> 태그를 만나거나, 비동기적으로 JavaScript 파일을 로드하게 되면, 브라우저는 해당 스크립트를 JavaScript 엔진을 통해 실행한다.
JavaScript 엔진은 이때 토크나이징 → 렉싱 → 파싱 과정을 거쳐 코드를 분석하고, 이를 컴파일한 뒤 실행하게 된다.
코드는 먼저 토크나이징(tokenizing) 과정을 통해 작은 조각들(토큰)로 분리된다.
이후 렉싱(lexing) 단계에서는 각 토큰에 의미가 부여된다.
마지막으로 파싱(parsing) 과정에서 이러한 토큰들을 기반으로 추상 구문 트리(AST, Abstract Syntax Tree)가 생성된다.
토크나이징/렉싱
토크나이징은 소스 코드를 각각의 토큰으로 나누는 과정이다.
토큰이란 변수, 연산자, 키워드, 구분자 등으로 구분되는 코드의 최소 단위를 의미한다.
토크나이징의 주된 목적은 소스 코드를 작고 관리하기 쉬운 조각들로 분리하여, 이후의 렉싱(Lexing)과 파싱(Parsing) 과정에서 이를 보다 효율적으로 처리할 수 있도록 돕는 데 있다.
렉싱은 토크나이징을 포함하면서도 그보다 더 확장된 역할을 수행한다.
이 단계에서는 생성된 각 토큰에 의미를 부여한다. 예를 들어, 예약된 키워드를 식별하거나, 변수 이름을 식별자로 인식하고, 숫자를 상수로 분류하는 등의 작업이 이루어진다.
코드를 보면서 다시 봐보자!
let a = 5;
토크나이징(Tokenizing)
let
a
=
5
;
렉싱(Lexing)
- let : 키워드
- a : 식별자
- = : 할당 연산자
- 5 : 숫자 상수
- ; : 구분자
📌 이처럼 토크나이징은 코드를 작은 단위로 나누는 것에 중점을 두고, 렉싱은 그 단위들에 의미를 부여하는 데 중점을 둔다.
즉, 토크나이징을 통해 코드를 조각낸 후, 렉싱을 통해 각 조각에 의미를 부여하는 순서로 이루어진다.
파싱 (Parsing)
다음 단계인 파싱(Parsing)은 렉싱 단계에서 생성된 토큰 배열을 프로그램의 문법 구조를 반영한 트리 구조로 변환하는 과정이다.
이때 만들어지는 트리를 추상 구문 트리(AST, Abstract Syntax Tree)라고 한다.
AST란 : 추상 구문 트리(Abstract Syntax Tree)
AST는 코드의 계층적 구조(hierarchical structure)를 표현하는 트리이며,
JavaScript 엔진은 이 트리를 바탕으로 코드의 구조와 의미를 이해한다.
추상적이라는 이유는 실제 코드에서 사용된 모든 세부 요소를 그대로 표현하지는 않기 때문이다.
예를 들어, 연산의 우선순위를 표현하기 위해 사용된 괄호 같은 요소는 AST에서 별도의 노드로 표현되지 않는다.
var a = 5;
위 코드는 아래와 같이 구성된다.

1. VariableDeclaration
변수를 선언하는 구문을 나타내는 노드
2. Identifier
변수, 함수, 매개변수 등의 이름(식별자)을 나타내는 노드
3. Assignment
값을 할당하는 연산을 나타내는 노드
4. BinaryExpression
이항 연산을 나타내는 노드
5. Literal
고정된 값을 나타내는 노드
📌 JavaScript 엔진은 이 AST를 통해 코드의 의미를 분석하고, 실행 준비와 최적화를 수행한다.
Babel
소스 코드를 AST로 변환하는 대표적인 도구 중 하나가 Babel이다.
Babel은 최신 JavaScript(ES6+) 코드를 구버전(JavaScript ES5)으로 변환하는 역할을 한다.
❓ 왜 변환이 필요할까?
최신 JavaScript(ES6, ES7, ES8 등)는 점점 더 강력한 기능을 제공하지만, 모든 브라우저가 최신 문법을 지원하지는 않는다.
따라서 Babel을 이용해 구형 브라우저에서도 코드가 동작할 수 있도록 변환해야 한다.
📌 Babel은 트랜스파일러(Transpiler) 또는 컴파일러(Compiler)로 분류된다.
프로그래밍 언어를 기계어로 변환하는 방법은 크게 컴파일러와 인터프리터 방식으로 나뉜다. Babel은 이 중 트랜스파일러로 분류된다.
🔍 각 변환 방식의 특징
컴파일러
- 소스 코드를 전체 스캔하여 기계어로 변환된 실행 파일을 생성한 뒤 실행한다.
- 실행 파일을 사용하므로 실행 속도는 빠르다.
- 하지만 변환 작업이 선행되어야 하므로 초기 실행 시간은 느릴 수 있다.
- 대표 언어: C, C++, Java 등
인터프리터
- 한 줄씩 코드를 읽고 즉시 실행하는 방식이다.
- 컴파일 과정 없이 곧바로 실행되므로 빠르게 수정하고 실행할 수 있다.
- 하지만 한 줄씩 해석하며 실행되기 때문에 속도가 느릴 수 있다.
- 대표 언어: Python, JavaScript
트랜스파일러 (Babel)
- 컴파일러의 한 종류이며, 같은 수준의 언어 간 변환을 수행한다.
- 예: TypeScript → JavaScript, ES6+ → ES5
- 고수준 언어 → 고수준 언어로 변환한다는 점에서 전통적인 컴파일러와 구분된다.
🔧 Babel의 동작 과정
Babel은 파싱 → 변환 → 코드 생성의 3단계를 거쳐 작동한다.
1. 파싱 (Parsing)
- JavaScript 코드를 파싱 하여 AST를 생성한다.
- 이 과정에서 토크나이징 → 렉싱 → 파싱이 순차적으로 이루어진다.
2. 변환 (Transforming)
- 생성된 AST를 순회하며 필요한 노드를 수정하거나 삭제, 추가한다.
- 예를 들어, 아래와 같이 화살표 함수 → 일반 함수 표현으로 변환한다.
// 변환 전 (ES6)
const x = () => 1;
// 변환 후 (ES5)
var x = function () { return 1; };
3. 코드 생성 (Code Generation)
- 변환된 새로운 AST를 기반으로 최종 JavaScript 코드 문자열을 생성한다.
📌 바벨은 트랜스파일러로 분류된다.
바벨도 일종의 컴파일 과정을 거치며, 소스코드를 파싱해 ast를 생성하고 새로운 코드를 생성하기에 컴파일러라고 부르는 경우도 있다.
이제 토크나이징 → 렉싱 → 파싱을 통해 만들어진 ast는 컴파일 과정을 거치게 된다.
JIT
AST는 이후 JIT(Just-In-Time) 컴파일러에 의해 기계어로 변환된다.
JIT 컴파일러는 코드가 실행되는 시점, 즉 "just in time", 런타임에 소스 코드를 기계어로 컴파일한다.
이 과정에서 다양한 최적화 작업이 함께 이루어지며, JavaScript 코드의 실행 속도를 크게 향상할 수 있다.
🆚 기존 인터프리터 방식과의 차이점
기존의 인터프리터 방식은 코드를 한 줄씩 해석하여 즉시 실행하는 구조이므로,
반복문이나 자주 실행되는 코드는 매번 해석되어 실행 속도가 느려질 수 있다.
반면 JIT 컴파일러는 런타임 중 반복되거나 중요도가 높은 코드 영역을 감지하여, 해당 부분을 기계어로 변환하고 캐싱한다.
이를 통해 실행 성능을 최적화할 수 있다.
📌 Chrome의 V8 엔진
Chrome의 V8 JavaScript 엔진에서는 처음에는 인터프리터가 코드를 실행한다.
그러다 특정 코드가 반복 실행되거나 중요하다고 판단되면,
JIT 컴파일러가 해당 부분을 기계어로 변환하여 캐시 하게 된다.
이렇게 하면 이후 반복 실행 시 더 빠르게 실행할 수 있다.
⚙️ JavaScript 엔진의 구성요소

JavaScript 엔진은 크게 메모리 힙(Memory Heap)과 콜 스택(Call Stack)으로 구성된다.
단, 브라우저 환경에서 JavaScript가 실행될 때는 JS 엔진만으로 동작하지 않고,
여러 외부 요소(Web APIs, 이벤트 루프 등)와 함께 작동한다.
🧠 메모리 힙 (Memory Heap)
- 변수나 함수 같은 데이터가 저장되는 공간이다.
- 동적으로 메모리를 할당하는 영역이다.
🧾 콜 스택 (Call Stack)
- 함수의 실행 순서를 관리하는 스택 구조이다.
- 전역 콘텍스트가 먼저 쌓이고, 이후 함수 호출 순서대로 스택에 push 된다.
- 함수가 종료되면 해당 스택 프레임(stack frame)이 pop 된다.
function b() {
console.log('hi');
}
function a() {
b();
}
a();
위 코드 실행 시 스택에는 다음 순서로 함수가 쌓인다
global → a() → b()
이후 b()가 끝나면 b → a 순으로 스택에서 제거된다.
🔁 JavaScript는 싱글 스레드 언어다
JavaScript는 하나의 콜 스택만을 가지는 싱글 스레드(single-threaded) 언어이다.
즉, 한 번에 하나의 작업만 처리할 수 있다.
그렇다면, 클릭 이벤트 등의 작업이 오래 걸리면(10초), 그동안 사용자는 아무것도 못 하게 될까?
→ 그렇지 않다. JavaScript는 비동기 처리를 지원하기 때문이다.
🧩 실행 콘텍스트와 스택 프레임
함수가 호출되면, 해당 함수에 대한 정보가 담긴 실행 콘텍스트(Execution Context)가 만들어져 스택 프레임 형태로 콜 스택에 올라간다.
실행 콘텍스트에 포함되는 정보
- 변수 환경 (Variable Environment)
- 렉시컬 환경 (Lexical Environment)
- this 바인딩
- 스코프 체인
이러한 정보는 해당 함수가 실행되는 동안 JS 엔진이 내부적으로 관리한다. 예를 들어 함수 내부 환경, 전역 환경 등이 포함된다.
JS 엔진 외부 요소
브라우저 환경에서는 JS 엔진 외에도 다음과 같은 런타임 환경 요소들이 함께 동작한다.

🌐 Web APIs
Web APIs는 브라우저가 JavaScript에게 제공하는 내장 인터페이스이다.
비동기 작업, HTTP 요청, 이벤트 리스너 등의 처리를 담당한다.
각 Web API는 자체적으로 스레드가 할당되어 있으며, 이들이 모여 브라우저 환경의 멀티스레드 구조를 이룬다.
JavaScript 엔진은 싱글 스레드이지만, 브라우저는 멀티스레드이기 때문에 비동기 작업의 분산 처리가 가능하다.
이때, 비동기 작업은 JS 엔진 외부에서 실행되며, 작업이 완료되면 콜백 함수가 Callback Queue에 저장된다.
📦 Callback Queue
Callback Queue는 완료된 비동기 작업들의 콜백 함수가 대기하는 공간이다.
콜스택이 비는 순간, 이벤트 루프가 이 큐에 있는 콜백을 실행시킨다.
🔁 Event Loop
Event Loop는 Call Stack과 Callback Queue를 지속적으로 관찰하는 역할을 한다.
Call Stack이 비어 있을 때, Callback Queue에서 콜백을 꺼내 Call Stack에 넣는다.
이를 통해 콜백이 실행되며, 이 과정은 프로그램이 종료될 때까지 반복된다.
실행 흐름
- 비동기 함수 호출
→ JS 엔진이 비동기 함수를 만나면, 이를 Web API로 전달한다. - 콜백 등록
→ 작업이 완료되면, 콜백 함수가 Callback Queue에 등록된다. - 콜백 실행
→ Event Loop가 Call Stack이 비어 있음을 확인하면,
→ Callback Queue에서 콜백을 꺼내 Call Stack에 넣고 실행한다.
🧵 Macro Task vs Micro Task Queue
콜백 큐는 실제로는 하나가 아닌, 두 가지 Task Queue로 나뉜다. 이들은 Call Stack에 들어가는 우선순위가 다르다.
Macro Task Queue
- setTimeout, setInterval, setImmediate, I/O 작업 등
- Event Loop는 한 번의 루프에서 오직 하나의 Macro Task만 실행
- Micro Task가 모두 완료된 뒤에 실행됨
Micro Task Queue
- Promise.then(), catch(), finally(), MutationObserver 등
- Call Stack이 비는 즉시 가장 먼저 실행됨
- Macro Task보다 우선순위가 높음

'1주차' 카테고리의 다른 글
SEO를 위한 HTML 태그 및 속성 활용법 (0) | 2025.04.11 |
---|---|
왜 애니메이션에서 transform이 성능이 좋을까? (0) | 2025.04.11 |
meta 태그 정리 (0) | 2025.04.11 |
Scroll-based CSS 애니메이션 (1) | 2025.04.11 |
SEO를 알고 나의 성공시대 시작됐다 (0) | 2025.04.11 |