자바스크립트의 유연함이 때로는 장점이 되기도 하지만, 종종 예상치 못한 런타임 오류의 원인이 되기도 합니다. 타입스크립트는 이러한 자바스크립트의 한계를 극복하기 위해 만들어졌습니다.
정적 타입 시스템을 통해 코드 실행 전 많은 오류를 미리 잡고, 더 안전하고 유지보수하기 쉬운 코드를 작성할 수 있게 해줍니다.
하지만 브라우저와 Node.js는 여전히 자바스크립트만 이해할 수 있는데, 타입스크립트 코드는 어떻게 동작하는 걸까요?
타입스크립트 컴파일러의 내부 동작 원리를 살펴보고, 소스 코드가 어떻게 검사되고 변환되는지, 컴파일 과정의 각 단계를 살펴보며, 타입스크립트의 안전성과 한계도 정리해보았습니다.
1. 자바스크립트의 런타임과 타입스크립트의 컴파일
// 🎯 타입스크립트에서는 컴파일 시점에 타입 검사가 이루어집니다
function add(a: number, b: number) {
return a + b;
}
// ✅ 올바른 타입 사용
add(10, 20); // 결과: 30
// ❌ 컴파일 오류 발생
// add(10, "20"); // 에러: Argument of type 'string' is not assignable to parameter of type 'number'
// 🧩 런타임 오류 예방 사례 1: undefined 속성 접근
function accessProperty(obj: { name?: string }) {
// 타입스크립트가 없다면 런타임에 오류 발생 가능
// 타입스크립트는 컴파일 시점에 이런 오류를 잡아냅니다
return obj.name?.toUpperCase(); // 옵셔널 체이닝으로 안전하게 접근
}
// 🧩 런타임 오류 예방 사례 2: 배열 접근
function getFirstItem(arr: string[] | null) {
// 타입스크립트가 강제하는 null 체크
if (arr === null) {
return "배열이 없습니다";
}
return arr.length > 0 ? arr[0] : "빈 배열입니다";
}
// 🧩 런타임 오류 예방 사례 3: 스코프 문제
function testScope() {
const localVar = "이 변수는 함수 내부에서만 접근 가능";
// 블록 스코프를 벗어난 변수 사용 시 컴파일 오류
// return innerVar; // 에러: Cannot find name 'innerVar'
if (true) {
const innerVar = "블록 스코프 변수";
return innerVar; // 이건 정상 작동
}
return localVar;
}
// 💯 타입스크립트는 오류를 미리 발견하여 안정적인 코드 작성을 도와줍니다
1) 런타임과 컴파일타임
(1) 고수준 언어와 저수준 언어
프로그래밍 언어는 크게 두 가지로 구분할 수 있습니다:
- 고수준 언어 👨💻: 사람이 이해하기 쉬운 형식 (예: JavaScript, TypeScript, Python)
- 저수준 언어 🤖: 컴퓨터가 이해하기 쉬운 형식 (예: 어셈블리어, 기계어)
고수준 언어로 작성된 코드는 컴퓨터가 실행하기 위해 저수준 언어로 변환되는 과정이 필요합니다.
(2) 컴파일타임 vs 런타임
컴파일타임(Compile Time) ⚙️
- 소스코드가 기계어 또는 중간 코드로 변환되는 시점
- 코드의 문법, 타입 검사 등이 이루어짐
- 오류가 발견되면 프로그램이 실행되기 전에 알려줌
런타임(Runtime) 🏃♂️
- 프로그램이 실제로 실행되는 시점
- 사용자 입력 처리, 메모리 할당, 연산 등이 발생
- 예상치 못한 상황(null 참조, 잘못된 입력 등)으로 오류 발생 가능
2) 자바스크립트 런타임
자바스크립트 런타임은 자바스크립트 코드가 실행되는 환경입니다.
대표적인 예 )
- 웹 브라우저 (Chrome, Firefox, Safari 등)
- Node.js
주요 구성 요소
- 자바스크립트 엔진: 코드를 해석하고 실행 (예: V8, SpiderMonkey)
- 웹 API: DOM, AJAX, setTimeout 등의 기능 제공
- 이벤트 루프, 콜백 큐: 비동기 작업을 관리
런타임 오류의 예시
// 예시 1: undefined 속성 접근
let foo;
foo.bar; // TypeError: Cannot read properties of undefined (reading 'bar')
// 예시 2: null 객체 속성 접근
const testArr = null;
if (testArr.length === 0) {
console.log("zero length");
} // TypeError: Cannot read properties of null (reading 'length')
// 예시 3: 스코프 문제
function testFn() {
const foo = "bar";
}
console.log(foo); // ReferenceError: foo is not defined
💡 참고: 자바스크립트는 흔히 인터프리터 언어로 알려져 있지만, 실제로는 V8 같은 현대 엔진이 내부적으로 JIT(Just-In-Time) 컴파일하여 성능을 최적화합니다.
3) 타입스크립트의 컴파일
타입스크립트는 소스코드를 자바스크립트로 변환합니다. 이 과정은 기존의 컴파일과 약간 다릅니다:
- 📝 고수준 언어(TypeScript) → 또 다른 고수준 언어(JavaScript)로 변환
- 🔄 이러한 변환을 트랜스파일(Transpile) 또는 소스 대 소스 컴파일이라고도 함
- 🔧 tsc라는 컴파일러를 사용
타입스크립트의 가장 큰 장점: 컴파일 타임 타입 체크
function add(a: number, b: number) {
return a + b;
}
add(10, 20); // ✅ 정상 동작
add(10, "20"); // ❌ 컴파일 오류 발생: 'string' 타입은 'number' 타입에 할당할 수 없습니다.
런타임 오류를 방지하는 타입스크립트
타입스크립트는 앞서 본 자바스크립트 런타임 오류를 컴파일 시점에 미리 잡아낼 수 있습니다:
// undefined 속성 접근 방지
let foo: { bar?: string };
foo.bar; // ❌ 오류: 'foo'가 초기화되기 전에 사용되었습니다.
// null 체크 강제
const testArr: string[] | null = null;
if (testArr?.length === 0) { // 옵셔널 체이닝으로 안전하게 접근
console.log("zero length");
}
// 스코프 문제 감지
function testFn() {
const foo = "bar";
}
console.log(foo); // ❌ 오류: 'foo'가 선언되기 전에 사용되었습니다.
🌟 타입스크립트를 사용해야 하는 이유
- 안전성 🛡️: 컴파일 타임에 많은 오류를 잡아내어 런타임 오류 감소
- 가독성 📖: 코드의 의도를 명확히 표현할 수 있는 타입 시스템
- 생산성 ⚡: 자동 완성, 리팩토링 지원 등 개발 도구의 향상된 지원
- 유지보수성 🔧: 타입 정보를 통해 코드 변경 시 영향 범위 파악 용이
2. 타입스크립트 컴파일러
타입스크립트는 자바스크립트의 슈퍼셋(확장)으로, 개발 단계에서 강력한 타입 체크를 제공하지만 결국 브라우저나 Node.js에서 실행되기 위해서는 자바스크립트로 변환되어야 합니다. 이 과정을 담당하는 것이 바로 타입스크립트 컴파일러(tsc) 입니다.
타입스크립트 컴파일러가 소스코드를 컴파일하고, 프로그램 실행되기까지의 과정은 아래와 같습니다.
1 tsc 명령어를 실행하여 프로그램 객체가 컴파일 과정을 시작한다.
2 스캐너는 소스 파일을 토큰 단위로 분리한다.
3 파서는 토큰을 이용하여 AST를 생성한다.
4 바인더는 AST의 각 노드에 대응하는 심볼을 생성한다. 심볼은 선언된 타입의 노드 정보를 담고 있다.
5 체커는 AST를 탐색하면서 심볼 정보를 활용하여 타입 검사를 수행한다.
6 타입 검사 결과 에러가 없다면 이미터를 사용해서 자바스크립트 소스 파일로 변환한다
1) 코드 검사기로서의 타입스크립트 컴파일러
타입스크립트 컴파일러의 첫 번째 중요한 역할은 코드 검사입니다. 이는 코드를 실행하기 전에 타입 오류를 찾아내는 것을 의미합니다.
예시: 런타임 에러를 컴파일 타임에 잡아내기
자바스크립트에서는
const developer = {
work() {
console.log("working...");
},
};
developer.work(); // 정상 작동: "working..." 출력
developer.sleep(); // 🔴 런타임 에러: TypeError: developer.sleep is not a function
타입스크립트에서는
const developer = {
work() {
console.log("working...");
},
};
developer.work(); // ✅ 정상 작동
developer.sleep(); // ❌ 컴파일 에러: Property 'sleep' does not exist on type '{ work(): void; }'
💡 핵심 이점: 타입스크립트는 코드를 실행하기 전에 오류를 발견하여 런타임 에러를 미리 방지합니다.
타입스크립트 컴파일러는 tsc binder를 사용하여 타입 검사를 수행하며, 이 과정에서 타입스크립트 AST(Abstract Syntax Tree)를 분석합니다. 타입 검사 후에는 이 AST를 자바스크립트 코드로 변환합니다.
2) 코드 변환기로서의 타입스크립트 컴파일러
타입스크립트 컴파일러의 두 번째 주요 역할은 코드 변환입니다. 타입스크립트 코드는 브라우저나 Node.js와 같은 자바스크립트 런타임에서 직접 실행될 수 없습니다. 따라서 컴파일러는 타입스크립트 코드를 표준 자바스크립트로 변환(트랜스파일)합니다.
예시: 타입스크립트에서 자바스크립트로의 변환
타입스크립트
type Fruit = "banana" | "watermelon" | "orange" | "apple" | "kiwi" | "mango";
const fruitBox: Fruit[] = ["banana", "apple", "mango"];
const welcome = (name: string) => {
console.log(`hi! ${name} :)`);
};
컴파일 후 자바스크립트 코드(ES5 기준)
"use strict";
var fruitBox = ["banana", "apple", "mango"];
var welcome = function (name) {
console.log("hi! ".concat(name, " :)"));
};
🔎 주목할 점: 컴파일 후에는 모든 타입 정보(type Fruit, : Fruit[], : string)가 제거되었습니다.
컴파일러의 target 옵션을 통해 어떤 버전의 자바스크립트로 변환할지 지정할 수 있습니다(예: ES5, ES6, ES2020 등).
중요한 특성: 타입 에러가 있어도 컴파일은 진행됨
타입스크립트 컴파일러는 타입 오류가 있더라도 코드 변환을 계속 진행합니다.
이는 타입 검사와 코드 변환이 독립적으로 동작하기 때문입니다.
const name: string = "zig";
const age: number = "zig"; // ❌ 타입 에러: Type 'string' is not assignable to type 'number'
위 코드는 타입 에러가 있지만, 다음과 같이 자바스크립트로 컴파일됩니다:
const name = "zig";
const age = "zig"; // 타입 정보가 제거되어 에러가 없어짐
⚠️ 주의: 타입 에러가 있는 코드도 컴파일이 가능하지만, 실제 런타임에서 예상치 못한 동작을 할 수 있습니다.
3) 런타임에서의 제약: 타입 정보 손실
타입스크립트의 타입 시스템은 컴파일 타임에만 존재합니다.
코드가 자바스크립트로 컴파일되면 모든 타입 정보가 사라집니다. 이는 런타임에서 타입 정보를 사용할 수 없다는 것을 의미합니다.
예시: 런타임에서 타입 사용 시 문제
interface Square {
width: number;
}
interface Rectangle extends Square {
height: number;
}
type Shape = Square | Rectangle;
function calculateArea(shape: Shape) {
// ❌ 오류: 'Rectangle'은 타입이므로 런타임에서 값으로 사용할 수 없음
if (shape instanceof Rectangle) {
return shape.width * shape.height;
} else {
return shape.width * shape.width;
}
}
🛠️ 해결책: 대신 '속성 체크'나 '타입 가드'를 사용해야 합니다. 타입스크립트의 인터페이스, 타입 별칭, 제네릭 등은 모두 컴파일 타임에만 존재하는 개념이므로, 런타임에서는 이를 직접 참조할 수 없습니다. 대신 자바스크립트의 기본 연산자와 패턴을 활용해 비슷한 기능을 구현합니다.
function calculateArea(shape: Shape) {
// ✅ 'height' 속성의 존재 여부로 타입 구분
if ('height' in shape) {
// 타입스크립트는 이 블록 내에서 shape를 Rectangle로 인식
return shape.width * shape.height;
} else {
return shape.width * shape.width;
}
}
TSC vs Babel 차이
타입스크립트 컴파일러(tsc)와 Babel은 모두 최신 코드를 이전 버전의 자바스크립트로 변환한다는 점에서 유사하지만, 아래 표와 같이 다른 점이 있습니다.
특징 | 타입스크립트 컴파일러(tsc) | Babel |
타입 검사 | ✅ 수행 | ❌ 수행하지 않음 |
코드 변환 | ✅ 수행 | ✅ 수행 |
주요 목적 | 타입 검사 + 코드 변환 | 최신 자바스크립트를 구 버전으로 변환 |
💡 참고: 실제 프로젝트에서는 Babel은 코드 변환을, TypeScript는 타입 검사만을 담당하도록 설정하는 경우도 있습니다.
🌟 타입스크립트 컴파일러의 두 가지 핵심 역할
아래 두 역할을 통해 타입스크립트는 개발 단계에서의 안전성을 제공하면서도, 모든 자바스크립트 런타임 환경과의 호환성을 유지할 수 있습니다.
- 타입 검사: 코드의 타입 오류를 컴파일 타임에 발견
- 코드 변환: 타입스크립트 코드를 자바스크립트로 트랜스파일
💡 개발 Tip
- tsconfig.json의 noEmitOnError 옵션을 true로 설정하면, 타입 에러가 있을 때 자바스크립트 파일을 생성하지 않도록 할 수 있습니다.
- 개발 단계에서는 --watch 플래그를 사용하여 파일 변경 시 자동으로 컴파일되도록 설정하면 효율적입니다.
- 런타임에 타입 검사가 필요한 경우, 별도의 런타임 타입 검사 라이브러리(예: io-ts, zod)를 고려해볼 수 있습니다.
3. 타입스크립트 컴파일러의 구조와 동작 원리
1) 타입스크립트 컴파일러의 구조
타입스크립트는 개발자가 작성한 타입스크립트 코드를 브라우저나 Node.js가 이해할 수 있는 자바스크립트로 변환해야 합니다. 이 변환 과정을 담당하는 것이 타입스크립트 컴파일러입니다. 컴파일러는 단순히 코드를 변환하는 것뿐만 아니라 타입 검사, 문법 오류 확인 등 다양한 작업을 수행합니다.
2) 컴파일러의 다섯 단계
타입스크립트 컴파일러는 소스코드를 자바스크립트로 변환하기 위해 다음 다섯 가지 주요 단계를 거칩니다:
단
1. 스캐너(Scanner) | 소스코드를 토큰으로 분리 | 토큰(Token) |
2. 파서(Parser) | 토큰을 기반으로 구문 분석 | AST(추상 구문 트리) |
3. 바인더(Binder) | 타입 정보를 AST에 연결 | 심볼(Symbol) |
4. 체커(Checker) | 타입 검사 수행 | 진단 정보(오류) |
5. 이미터(Emitter) | 최종 JavaScript 코드 생성 | .js 파일 |
각 단계를 자세히 살펴보겠습니다.
[ 1단계 ] 프로그램(Program)
컴파일 과정은 tsc 명령어가 실행되면서 시작됩니다. 이 시점에서 프로그램 객체가 생성되며, 다음 작업을 수행합니다:
- tsconfig.json 파일에서 컴파일 옵션 로드
- 컴파일할 소스 파일 및 관련 파일 식별
- 전체 컴파일 과정 조정
# 🖥️ 터미널에서 컴파일 실행
tsc index.ts # 특정 파일 컴파일
# 또는
tsc # tsconfig.json 설정에 따라 컴파일
[ 2 단계 ] 스캐너(Scanner)
스캐너는 타입스크립트 소스코드를 토큰(Token)이라는 최소 의미 단위로 분해합니다. 이 과정을 어휘 분석(Lexical Analysis)이라고 합니다.
예를 들어 다음 코드를
const woowa = "bros";
스캐너는 다음과 같은 토큰으로 분해합니다:
- ConstKeyword (const)
- WhitespaceTrivia (공백)
- Identifier (woowa)
- WhitespaceTrivia (공백)
- EqualsToken (=)
- WhitespaceTrivia (공백)
- StringLiteral ("bros")
- SemicolonToken (;)
💡 참고: 타입스크립트의 모든 토큰 유형은 SyntaxKind 변수에서 확인할 수 있습니다.
[ 3단계 ] 파서(Parser)
파서는 스캐너가 생성한 토큰을 받아 추상 구문 트리(AST: Abstract Syntax Tree)를 생성합니다.
AST는 소스코드의 구조를 트리 형태로 표현한 자료구조입니다.
- 스캐너가 '무엇이 있는지'를 파악했다면, 파서는 '어떤 의미인지'를 파악합니다
- 예를 들어, 괄호(())가 함수 호출인지, 그룹화 연산자인지 결정합니다
// 📌 예제 코드
function normalFunction() {
console.log("normalFunction");
}
normalFunction();
이 코드의 AST는 대략 다음과 같은 구조를 가집니다:
FunctionDeclaration
├── Identifier (normalFunction)
├── Empty Parameters
└── Block
└── ExpressionStatement
└── CallExpression (console.log)
├── PropertyAccessExpression
│ ├── Identifier (console)
│ └── Identifier (log)
└── StringLiteral ("normalFunction")
ExpressionStatement
└── CallExpression (normalFunction)
🔍 도구 팁: TypeScript AST Viewer를 사용하면 코드의 AST 구조를 시각적으로 확인할 수 있습니다.
[ 4단계 ] 바인더(Binder)
바인더의 주요 역할은 파서가 생성한 AST 노드들을 의미적으로 연결하고, 타입 검사를 위한 심볼(Symbol)을 생성하는 것입니다.
- 심볼은 코드에서 선언된 식별자(변수, 함수, 클래스 등)에 대한 정보를 담고 있습니다
- 각 심볼은 다음과 같은 정보를 포함합니다:
- flags: 심볼의 종류(변수, 함수, 클래스 등)
- escapedName: 심볼의 이름
- declarations: 해당 심볼과 관련된 선언 노드 목록
// 📌 심볼 인터페이스 일부 (타입스크립트 소스 코드에서)
export interface Symbol {
flags: SymbolFlags; // 심볼 종류 식별자
escapedName: string; // 심볼 이름
declarations?: Declaration[]; // 관련 선언 노드
// 이하 생략...
}
심볼 플래그는 AST에서 선언된 타입의 노드 정보를 저장하는 식별자입니다.
// 📌 심볼 플래그 예시 (일부)
export const enum SymbolFlags {
None = 0,
FunctionScopedVariable = 1 << 0, // var 변수 또는 매개변수
BlockScopedVariable = 1 << 1, // let이나 const 변수
Property = 1 << 2, // 속성 또는 enum 멤버
Function = 1 << 4, // 함수
Class = 1 << 5, // 클래스
Interface = 1 << 6, // 인터페이스
// 이하 생략...
}
[ 5단계 ] 체커(Checker)와 이미터(Emitter)
체커(Checker)
체커는 앞서 생성된 AST와 심볼을 바탕으로 타입 검사를 수행합니다.
- 타입스크립트 컴파일러에서 가장 큰 부분을 차지합니다(약 2.7MB)
- AST 노드를 순회하면서 각 노드에 대한 타입을 분석하고 검증합니다
- 타입 오류가 발견되면 진단 정보(에러 메시지)를 생성합니다
// 📌 타입 오류 예시
let name: string = "Woowa";
name = 42; // ❌ 오류: Type 'number' is not assignable to type 'string'
이미터(Emitter)
이미터는 최종적으로 타입스크립트 코드를 자바스크립트 코드로 변환합니다.
- 자바스크립트 파일(.js)과 타입 선언 파일(.d.ts)을 생성합니다
- 이미터는 체커의 타입 검사 결과를 활용합니다
- 타입 정보는 제거되고 실행 가능한 자바스크립트 코드만 남습니다
// 📌 타입스크립트 원본 코드
function greet(name: string): string {
return `Hello, ${name}!`;
}
// 📌 이미터가 생성한 자바스크립트 코드
function greet(name) {
return "Hello, " + name + "!";
}
컴파일 과정 요약
타입스크립트 컴파일러의 전체 과정을 요약하면 다음과 같습니다:
- 프로그램 객체 생성: tsc 명령어 실행으로 컴파일러 시작
- 스캐너: 소스코드를 토큰으로 분리 (어휘 분석)
- 파서: 토큰을 기반으로 AST 생성 (구문 분석)
- 바인더: AST 노드와 심볼 연결, 타입 정보 준비
- 체커: AST와 심볼을 기반으로 타입 검사 수행
- 이미터: 타입 정보가 제거된 자바스크립트 코드 생성
💡개발 Tip
- 타입 오류 이해하기: 컴파일러 에러 메시지의 원인은 주로 체커 단계에서 발생합니다. 에러 메시지를 이해하면 문제 해결이 쉬워집니다.
- 컴파일 성능 최적화: 대규모 프로젝트에서는 증분 컴파일을 활용하여 성능을 향상시킬 수 있습니다.
- AST 탐색 도구 활용: TypeScript AST Viewer를 사용하면 코드의 구조를 더 잘 이해할 수 있습니다.
- 타입스크립트 컴파일러 API: 타입스크립트는 컴파일러 API를 제공하여 자체 도구를 개발할 수 있습니다.