이번 글에서는 erasableSyntaxOnly
라는 실험적인 옵션을 살펴보면서, TypeScript 안에서 “순수 타입”과 “런타임 코드”가 어떻게 뒤섞여 있는지 되짚어보려고 합니다.
타입스크립트에는 꽤 오랫동안 이어져 온 논쟁이 있습니다. 바로 enum
을 써도 되는가, 아니면 써서는 안되는가? 하는 문제죠.
저도 이 논쟁에 대해 생각해보던 중, 최근 토스에서 공개한 frontend-fundamentals
라는 자료에서 enum vs as const 이야기가 오가는 모습을 발견했습니다. 그 토론 속에서 erasableSyntaxOnly
라는 흥미로운 옵션도 보이더군요. “이게 뭘까?” 싶어 살펴보고, 제가 생각한 바를 간단히 정리해보았습니다.
우선, 타입스크립트가 어떤 역할을 하는지 공식 홈페이지 핸드북의 문구를 통해 다시 한 번 떠올려보겠습니다.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
- 타입스크립트 공식 홈페이지 핸드북
결국 타입스크립트는 “JavaScript에 타입 기능을 추가한 상위 언어(슈퍼셋)”이라는 것 입니다. 이런 이유로, 우리가 작성한 타입스크립트 코드는 컴파일만 거치면 완전히 평범한 JavaScript가 됩니다. 즉, 런타임 레벨에서는 보통은 “타입 정보”가 다 제거된 상태로 실행된다는 뜻이죠.
이러한 특징 덕분에 예전 자바스크립트 코드를 차근차근 타입스크립트로 작성해나가거나, 혹은 타입스크립트 코드를 작성하고 “타입만 걷어내서” 순수 자바스크립트 코드로 사용할 수 있습니다.
type
, interface
, generic
처럼 정적 타입 체크에만 쓰이는 요소들은 컴파일 결과에서 모두 사라집니다. 그러나 그와 달리, 일부 TypeScript 전용 문법들은 트랜스파일 후에도 런타임 동작을 하는 코드가 남습니다. 대표적으로 다음과 같은 것들이 있습니다.
enum
enum Colors {
Red = "RED",
Green = "GREEN",
Blue = "BLUE",
}
function printColor(color: Colors) {
console.log(color);
}
printColor(Colors.Red);
위 코드를 컴파일(tsc)하면, 다음과 같이 변환됩니다.
"use strict";
var Colors;
(function (Colors) {
Colors["Red"] = "RED";
Colors["Green"] = "GREEN";
Colors["Blue"] = "BLUE";
})(Colors || (Colors = {}));
function printColor(color) {
console.log(color);
}
printColor(Colors.Red);
결과물에서 Colors라는 실제 객체가 생성되고, Colors.Red는 "RED"라는 값을 갖습니다.
즉, “타입만” 추가하려던 의도와 달리 런타임에 새로운 객체가 남는 형태가 되는 셈이죠.
이 때문에 "enum" 대신 as const를 쓰고, 타입 체커가 알아서 추론하게 만든 뒤, 실제로는 단순 객체나 상수로만 구성하는 방식을 선호하는 분들도 많습니다.
namespace
는 자바스크립트에서 모듈 시스템이 표준화되기 전, 하나의 “네임스페이스”를 만들어 전역 공간 오염을 줄이려는 목적으로 도입된 개념입니다.
namespace Utils {
export function greet(name: string) {
console.log(`Hello, ${name}`);
}
}
Utils.greet("Alice");
위 코드를 컴파일하면, 다음과 같이 변환됩니다.
"use strict";
var Utils;
(function (Utils) {
function greet(name) {
console.log(`Hello, ${name}`);
}
Utils.greet = greet;
})(Utils || (Utils = {}));
Utils.greet("Alice");
보시다시피 즉시 실행 함수(IIFE)와 객체가 만들어지며, 결국 런타임에서도 Utils라는 객체가 살아있게 됩니다. enum과 마찬가지로 런타임에서 객체를 생성하니, "타입만" 사용한다고 보기에는 어렵습니다.
매개변수 속성이란, 생성자(constructor) 매개변수에 public, private, readonly 등을 붙여서 한 번에 필드를 선언하고 초기화할 수 있는 문법입니다.
class Person {
constructor(
public name: string,
private age: number
) {}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
컴파일 결과를 보면, public name: string
같은 타입스크립트 전용 구문이 사라지고, 대신 this.name = name;
이라는 실제 프로퍼티 할당이 남습니다.
"use strict";
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, my name is ${this.name}`);
}
}
그렇다 보니, 런타임에도 동작하는 구문이 생성되는 셈입니다. 따라서 이 역시 “타입만 추가”하는 것이 아니라, 컴파일 후에도 남는 코드를 만들어낸다고 볼 수 있습니다.
앞서 살펴본 문법들은 모두 일정 부분 런타임 동작을 만들어냅니다.
하지만 저를 포함한 일부 사람들은 “정말로 타입 체크만을 원할 뿐, 컴파일된 JS 코드에는 추가 구문이 남지 않길 바란다”는 요구를 갖기도 합니다.
이런 요구를 의식한 듯, TypeScript 5.8 Beta에서 --erasableSyntaxOnly라는 옵션이 공개됐습니다.
이 기능은 2024년 8월경 TypeScript GitHub 이슈 Tsconfig option to disallow features requiring transformations which are not supported by Node.js' --strip-types (#59601)을 통해 구체화되었습니다.
--strip-types
플래그를 도입했습니다. 이는 TypeScript의 타입 주석을 제거할 수 있게 해주는 기능입니다.--strip-types
는 순수한 타입 주석만 제거할 수 있고, JavaScript 코드로 변환이 필요한 TypeScript 기능들은 지원하지 않습니다.--strip-types
와 호환되는 TypeScript 코드를 작성하기 위해서는 여러 설정을 수동으로 관리해야 하는 불편함이 있었습니다.이에 대한 해결책으로 erasableSyntaxOnly
가 등장하게 되었습니다.
erasableSyntaxOnly
옵션은 TypeScript 5.8 Beta에서 도입된 실험적 기능으로 다음과 같은 역할을 합니다.
위에서 살펴 본 예시와 함께 이 옵션이 어떻게 방지해주는지 알아보겠습니다.
먼저 5.8.0-beta 버전 혹은 그 이상의 버전의 Typescript Playground로 이동합니다.
이후 TS Config 설정에서 erasableSyntaxOnly 속성을 활성화 해줍니다.
이전의 캡쳐에서 옵션이 설명된 것처럼 ECMAScript의 일부가 아닌 runtime constructs가 허용되지 않기 때문에 다음과 같이 에러가 나는 모습을 볼 수 있습니다.
정리해보자면,
enum
, namespace
, 클래스 매개변수 속성처럼 런타임 코드가 남는 기능들도 포함되어 있어, “단순히 타입만 추가하는 언어”라고 말하기엔 다소 무리가 있습니다. --strip-types
처럼 “타입만 제거하는 방식”과 충돌이 생길 수 있고, 그 때문에 제안된 옵션이 바로 erasableSyntaxOnly
입니다.추가로, 개인적인 추측을 덧붙이자면, 이 옵션을 활용하면 별도의 변환 과정을 생략해 컴파일 효율을 높이고, 결과물과 소스 코드가 거의 동일해져 디버깅에도 이점이 있을 것으로 보입니다. 다만 이는 어디까지나 제 예상이며, 아직 베타 기능이니 만큼 실제 적용 전에는 충분한 검토와 테스트가 필요합니다.
한편, 저는 이러한 옵션들이 “타입과 JavaScript 코드를 완전히 분리”하고자 하는 타입스크립트의 미래 방향성을 보여주는 것이 아닐까 하는 생각도 듭니다. 만약 향후 ECMAScript 표준에서 타입 문법을 본격적으로 지원하게 된다거나 타입스크립트에서 erasable한 값만 남기게 된다면, 미리 이와 같은 분리 방식을 염두에 두고 코드를 작성해두었을 때 마이그레이션 시점에서 발생할 수 있는 어려움을 덜 수 있겠죠.
물론 정답은 없습니다.
어떤 팀은 enum
이나 클래스 매개변수 속성을 적극 활용해 생산성을 높일 수 있고, 다른 팀은 “정적 타입 체크만을 원한다”며 런타임 구문을 최소화하는 방식을 선호할 수 있습니다. 중요한 건 프로젝트의 목표와 팀의 합의에 맞춰, TypeScript가 제공하는 여러 옵션들을 적절히 선택하는 것입니다.
erasableSyntaxOnly
역시 그런 선택지 중 하나로 보이는 만큼, 앞으로 이 기능이 더 다듬어지는 과정을 관심 있게 지켜봐도 좋겠습니다.
긴 글 읽어주셔서 감사합니다.