Ray Book
JavaScript 엔진의 내부

소스 코드에서 토큰으로

JavaScript 엔진이 코드 문자열을 의미 있는 조각으로 쪼개는 첫 번째 단계, 토크나이징을 시각화합니다

javascriptv8tokenizerlexer

코드는 그냥 문자열이다

브라우저에게 JavaScript 코드란 처음에는 그냥 긴 문자열 입니다. <script> 태그 안의 텍스트든, .js 파일의 내용이든, 엔진이 보는 것은 UTF-16으로 인코딩된 문자의 나열입니다.

let message = "hello";

사람의 눈에는 변수 선언문이 보이지만, 엔진은 아직 이것을 이해하지 못합니다. l, e, t, , m, e, s... 이런 낱개 문자만 있을 뿐입니다.

이 문자열에서 의미를 추출하는 첫 번째 단계 가 바로 토크나이징 (tokenizing) 입니다.

토크나이저가 하는 일

토크나이저 (tokenizer, 또는 렉서/lexer) 는 문자열을 왼쪽에서 오른쪽으로 훑으며 토큰 (token) 이라는 의미 있는 조각으로 분류합니다.

아래 시각화에서 한 단계씩 넘기며 토크나이저가 어떻게 코드를 토큰으로 쪼개는지 확인해보세요.

소스 코드
let message = "hello";
토큰 스트림
키워드let
변수 선언 키워드. 예약어(reserved word)로, 식별자로 사용할 수 없습니다.

각 토큰에는 타입 이 있습니다. 엔진은 let이라는 문자 세 개를 만나면 이것이 키워드인지, 변수 이름인지 판단해야 합니다. 이 판단은 예약어 목록과 대조해서 이루어집니다.

조금 더 복잡한 코드

함수 선언은 토큰 수가 훨씬 많습니다. 괄호, 쉼표, 중괄호 같은 구분자 (punctuation) 도 각각 독립된 토큰입니다.

소스 코드
function add(a, b) { return a + b; }
토큰 스트림
키워드function
함수 선언 키워드. 뒤에 식별자가 와야 한다는 것을 파서에게 알립니다.

토크나이저의 핵심 동작

토크나이저는 간단해 보이지만 몇 가지 까다로운 판단을 합니다.

Lookahead — 미리 보기

=를 만났을 때, 이것이 할당 (=) 인지 동등 비교 (==) 인지 일치 비교 (===) 인지 알려면 다음 문자를 미리 봐야 합니다. 마찬가지로 +++일 수도, +=일 수도 있습니다.

키워드 vs 식별자

let, const, function 같은 문자열은 키워드지만, letterfunctional은 식별자입니다. 토크나이저는 단어의 경계를 찾은 뒤 예약어 목록과 비교합니다.

문자열과 템플릿 리터럴

따옴표를 만나면 닫는 따옴표까지의 모든 문자를 하나의 토큰으로 묶습니다. 백틱의 경우 ${를 만나면 다시 일반 토크나이징 모드로 전환해야 하므로 더 복잡합니다.

V8에서의 실제 구현

V8 엔진의 토크나이저는 src/parsing/scanner.cc에 구현되어 있습니다. 몇 가지 특징이 있습니다:

  • Lazy parsing — 모든 코드를 즉시 파싱하지 않습니다. 당장 실행되지 않는 함수는 건너뛰고 (pre-parsing), 실제로 호출될 때 파싱합니다
  • UTF-16 스트림 — 소스 코드를 UTF-16 코드 유닛 단위로 읽습니다
  • Multi-token lookahead — V8의 스캐너는 현재 토큰, 다음 토큰, 그 다음 토큰까지 최대 세 개를 유지합니다 (화살표 함수 등의 파싱에 필요)

다음 단계

토크나이저가 만든 토큰 스트림은 그 자체로는 코드의 구조 를 표현하지 못합니다. let message = "hello"가 변수 선언문이라는 것을 이해하려면, 토큰들 사이의 관계를 파악해야 합니다.

이것이 다음 글에서 다룰 파서 (Parser) 의 역할입니다 — 토큰 스트림을 AST (추상 구문 트리) 로 변환하는 과정을 살펴보겠습니다.