이번 사내 스터디 주제는 타입스크립트입니다. 타입스크립트는 키즈노트 FE개발파트의 기술 스택 중 하나인데요, 그래서 모든 파트원들이 타입스크립트를 사용하고 있어요. "타입스크립트를 이미 사용하고 있는데, 굳이 스터디를 할 필요가 있을까?" 싶기도 하지만, 타입스크립트의 작동 방식을 깊게 학습하고 새롭게 릴리즈된 버전을 프로젝트에 적용해보는 것이 이번 스터디의 목표에요.
이번 주는 타입스크립트의 컴파일 과정에 대해 학습하고 내용을 공유하기로 했어요. 이번 주 주제에 대해 생각해 보니, 기술 면접에서 아래와 같은 상황이 꽤 흔하다는 것이 떠올랐습니다.
👩🏻💻: 타입스크립트의 장점은 무엇인가요?
🙋🏻♀️: 컴파일 단계에서 오류를 잡기 때문에 에러를 사전에 방지할 수 있습니다!
그런데 타입스크립트는 어떻게 컴파일 단계에서 오류를 잡을 수 있는 걸까요? 에러를 사전에 방지한다는 건 정확히 어떤 의미인 걸까요? 이 두 가지 궁금증을 해결해 봅시다!
잠시 인터프리터와 컴파일러에 대해 이야기를 해볼게요. 하이 레벨 프로그래밍 언어(High-level programming language)를 둘 중 어떤 방식으로 실행하느냐에 따라 에러를 잡아내고 방지하는 시점이 다르기 때문에, 아주 관련이 없는 이야기는 아닙니다.
하이 레벨 프로그래밍 언어로 작성된 코드를 실행하는 방법은 두 가지가 있어요.
인터프리터(Interpreter) : 소스 코드를 한 번에 한 줄씩 읽어서 번역하고 실행해요. 전통적인 인터프리터는 소스 코드를 다른 언어(보통 로우 레벨 프로그래밍 언어)로 번역하는 컴파일 과정이 없기 때문에, 개발 시 변경된 코드를 빠르게 실행하고 확인할 수 있다는 장점이 있어요. 하지만 번역과 실행이 동시에 이루어지기 때문에 컴파일 언어에 비해 실행 속도가 느리다는 단점이 있어요.
컴파일러(Compiler) : 모든 소스 코드를 한 번에 다른 언어(보통 로우 레벨 프로그래밍 언어)로 번역한 후 실행해요. 코드를 실행하기 전에 이미 모든 코드가 번역되었기 때문에 인터프리터 언어에 비해 실행 시간이 빠르다는 장점이 있어요. 하지만 규모가 큰 프로그램은 번역하는 시간이 오래 걸린다는 단점이 있어요.
자바스크립트는 런타임 시 변수의 타입이 결정되는 동적 언어이기 때문에 인터프리터 방식을 사용해요. 그런데 전통적인 인터프리터라니, 현대의 인터프리터와 어떤 차이가 있는 걸까요? 사실 현대 인터프리터는 자바스크립트를 실행하는 과정에 컴파일 과정이 포함되어 있어요.
전통적인 인터프리터는 코드를 한 줄씩 읽고, 번역하고, 실행하기 때문에 컴파일러에 비해 실행 속도가 느리다는 단점이 있었죠? 과거 자바스크립트는 웹 페이지의 간단한 동적 기능을 구현하는 데 초점을 두어 만들어졌기 때문에, 인터프리터만으로 충분히 빠른 실행이 가능했어요. 하지만 웹 기술이 발전하면서 다양한 요구사항들이 생겼고, 자바스크립트는 더 많은 기능을 갖추어야 했어요. 자바스크립트는 더욱 무거워졌고, 실행 속도가 크게 느려지게 되었습니다.
이러한 자바스크립트의 성능 문제를 해결하고자 나온 것이 크롬의 V8 엔진이에요. V8 엔진의 인터프리터는 소스 코드에서 최적화가 필요한 부분을 파악한 후 바이트 코드로 변환해요. 그리고 변환된 코드를 JIT 컴파일러(Just-in-time compiler)로 보내서 로우 레벨 프로그래밍 언어로 변환하고 실행합니다. JIT 컴파일러는 반복적으로 실행되는 바이트 코드를 식별해서 캐싱하고, 불필요한 연산을 제거하거나 더 효율적인 코드로 바꾸어주는 등 최적화를 진행해요. 코드 실행 횟수가 많지 않거나, 최적화 효과가 미미한 경우 일반적으로 인터프리터를 통해 처리됩니다.
현대의 인터프리터는 바이트 코드와 JIT 컴파일러를 이용하여, 인터프리터와 컴파일러의 장점을 동시에 가져가게 되었어요.
인터프리터와 컴파일러에 대해 이야기를 하다가 잠깐 다른 길로 빠졌군요... 🧐 다시 돌아와서, 위에서 하이 레벨 프로그래밍 언어를 어떤 방식으로 실행하느냐에 따라 에러를 잡아내는 시점이 다르다고 이야기했어요.
컴파일 언어인 타입스크립트는 로컬에서 자바스크립트로 변환하는 컴파일 과정을 거친 후, 브라우저에서 자바스크립트 파일을 실행하기 때문에 컴파일 과정에서 에러를 잡을 수 있어요. 하지만 인터프리터 언어인 자바스크립트는 로컬에서 별도의 컴파일 과정을 거치지 않기 때문에 런타임 과정에서 에러를 잡아야 합니다. 이는 규모가 큰 자바스크립트 프로그램은 브라우저에서 실행하기 전까지 개발자가 에러의 존재를 파악하기 어려울 수 있다는 것을 의미해요.
타입스크립트는 브라우저에서 코드를 실행하기 전에 자바스크립트로 컴파일하는 과정에서 에러를 잡을 수 있기 때문에 에러를 사전에 방지할 수 있게 된 것입니다.
아래는 타입스크립트 코드를 자바스크립트 코드로 컴파일 하는 과정을 표현한 그림이에요.
가장 먼저, 타입스크립트 파일과 컴파일러 옵션(tsconfig.json)으로 구성된 Program
객체를 스캔하여 토큰 스트림을 생성해요. 여기서 토큰이란 예약어, 콜론 등 코드의 가장 작은 단위를 의미해요. 이 과정을 토큰화(Tokenization)라고 합니다.
스캐너는 Parser
에 의해 제어되며, 스캐너를 반복해서 생성하는 비용을 줄이기 위해 parser.ts
파일 내부에 싱글톤 패턴으로 정의되어 있어요.
분해된 토큰들을 이용하여 AST(Abstract Syntax Tree)를 생성해요. AST는 노드로 구성되어 있으며 공백, 주석 등 사소한 부분들은 AST에 저장되지 않습니다.
Symbol
은 각 노드의 타입 정보를 저장하는 추가적인 메타 데이터를 의미해요. Binder
는 AST를 분석해서 타입 정보를 해시맵 형태로 저장합니다.
Program
에서 Emitter
를 호출하면, Emitter
내부에서 Type Checker
를 생성하기 위해 getDiagnostics()
함수를 호출해요. Type Checker
는 checker.ts
에 정의되어 있으며 23,000줄 이상으로 구성되어 있어 컴파일러의 가장 큰 부분을 차지해요.
Emitter
는 AST와 Symbol
정보를 이용하여 타입을 체크하는데, 이 단계에서 에러를 발견하면 TypeError
가 발생하게 됩니다.
타입 검사까지 잘 진행되었다면, 마지막으로 세 종류의 파일을 생성해요.
.js
: 브라우저에서 실행할 자바스크립트 파일입니다..d.ts
: 자바스크립트 코드의 타입이 정의된 파일입니다. .d.ts
파일은 타입스크립트로 개발할 때 타입 정보를 제공하기 위해 사용되며, 자바스크립트 코드 실행에 영향을 주지 않습니다..map
: 원본 소스 코드(타입스크립트)와 변환된 코드(자바스크립트) 간의 매핑 정보를 제공하는 파일입니다. 예를 들어 브라우저의 개발자 도구를 사용하여 디버깅하면 원본 타입스크립트 코드를 디버깅하면서 자바스크립트 코드의 실행 흐름을 따라갈 수 있습니다. 이 파일은 개발자가 디버깅하는 동안에만 사용됩니다.이번 주 스터디를 통해 자바스크립트 인터프리터와 타입스크립트 컴파일러의 동작을 간단하게 알아보았는데요, 사실 매일 사용하는 언어임에도 코드를 실행하기까지 많은 과정을 거쳐간다는 것이 흥미로웠어요. 사용자에게 빠르고 안정적인 서비스를 제공하기 위해 언어는 끊임없이 발전하고 있는 것 같습니다. 😎