erasableSyntaxOnly를 알아보며 되짚어본 TypeScript

BO·2025년 1월 31일
2
post-thumbnail

글을 시작하며

이번 글에서는 erasableSyntaxOnly라는 실험적인 옵션을 살펴보면서, TypeScript 안에서 “순수 타입”“런타임 코드”가 어떻게 뒤섞여 있는지 되짚어보려고 합니다.

타입스크립트에는 꽤 오랫동안 이어져 온 논쟁이 있습니다. 바로 enum을 써도 되는가, 아니면 써서는 안되는가? 하는 문제죠.

저도 이 논쟁에 대해 생각해보던 중, 최근 토스에서 공개한 frontend-fundamentals라는 자료에서 enum vs as const 이야기가 오가는 모습을 발견했습니다. 그 토론 속에서 erasableSyntaxOnly라는 흥미로운 옵션도 보이더군요. “이게 뭘까?” 싶어 살펴보고, 제가 생각한 바를 간단히 정리해보았습니다.

타입스크립트의 역할

우선, 타입스크립트가 어떤 역할을 하는지 공식 홈페이지 핸드북의 문구를 통해 다시 한 번 떠올려보겠습니다.

TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
- 타입스크립트 공식 홈페이지 핸드북

결국 타입스크립트는 “JavaScript에 타입 기능을 추가한 상위 언어(슈퍼셋)”이라는 것 입니다. 이런 이유로, 우리가 작성한 타입스크립트 코드는 컴파일만 거치면 완전히 평범한 JavaScript가 됩니다. 즉, 런타임 레벨에서는 보통은 “타입 정보”가 다 제거된 상태로 실행된다는 뜻이죠.

이러한 특징 덕분에 예전 자바스크립트 코드를 차근차근 타입스크립트로 작성해나가거나, 혹은 타입스크립트 코드를 작성하고 “타입만 걷어내서” 순수 자바스크립트 코드로 사용할 수 있습니다.

JS로 트랜스파일될 때도 남아있는 구문들

type, interface, generic처럼 정적 타입 체크에만 쓰이는 요소들은 컴파일 결과에서 모두 사라집니다. 그러나 그와 달리, 일부 TypeScript 전용 문법들은 트랜스파일 후에도 런타임 동작을 하는 코드가 남습니다. 대표적으로 다음과 같은 것들이 있습니다.

1. 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를 쓰고, 타입 체커가 알아서 추론하게 만든 뒤, 실제로는 단순 객체나 상수로만 구성하는 방식을 선호하는 분들도 많습니다.

2. namespace

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과 마찬가지로 런타임에서 객체를 생성하니, "타입만" 사용한다고 보기에는 어렵습니다.

3. 클래스의 매개변수 속성(Parameter Properties)

매개변수 속성이란, 생성자(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}`);
    }
}

그렇다 보니, 런타임에도 동작하는 구문이 생성되는 셈입니다. 따라서 이 역시 “타입만 추가”하는 것이 아니라, 컴파일 후에도 남는 코드를 만들어낸다고 볼 수 있습니다.

erasableSyntaxOnly

앞서 살펴본 문법들은 모두 일정 부분 런타임 동작을 만들어냅니다.

하지만 저를 포함한 일부 사람들은 “정말로 타입 체크만을 원할 뿐, 컴파일된 JS 코드에는 추가 구문이 남지 않길 바란다”는 요구를 갖기도 합니다.

이런 요구를 의식한 듯, TypeScript 5.8 Beta에서 --erasableSyntaxOnly라는 옵션이 공개됐습니다.

erasableSyntaxOnly 등장 배경

이 기능은 2024년 8월경 TypeScript GitHub 이슈 Tsconfig option to disallow features requiring transformations which are not supported by Node.js' --strip-types (#59601)을 통해 구체화되었습니다.

  • Node.js는 실험적 기능으로 --strip-types 플래그를 도입했습니다. 이는 TypeScript의 타입 주석을 제거할 수 있게 해주는 기능입니다.
  • 하지만 Node.js의 --strip-types는 순수한 타입 주석만 제거할 수 있고, JavaScript 코드로 변환이 필요한 TypeScript 기능들은 지원하지 않습니다.
    • enum
    • experimentalDecorators
    • namespaces
    • parameter properties 등
  • 이로 인해 개발자들이 Node.js의 --strip-types와 호환되는 TypeScript 코드를 작성하기 위해서는 여러 설정을 수동으로 관리해야 하는 불편함이 있었습니다.

이에 대한 해결책으로 erasableSyntaxOnly가 등장하게 되었습니다.

erasableSyntaxOnly 옵션은 TypeScript 5.8 Beta에서 도입된 실험적 기능으로 다음과 같은 역할을 합니다.

  1. 순수하게 타입 제거만으로 실행 가능한 코드만 허용합니다.
  2. JavaScript로의 변환이 필요한 TypeScript 기능들을 사용하지 못하게 합니다.
  3. Node.js의 --strip-types와 완벽하게 호환되는 코드를 작성할 수 있게 해줍니다.

erasableSyntaxOnly 살펴보기

위에서 살펴 본 예시와 함께 이 옵션이 어떻게 방지해주는지 알아보겠습니다.

먼저 5.8.0-beta 버전 혹은 그 이상의 버전의 Typescript Playground로 이동합니다.

이후 TS Config 설정에서 erasableSyntaxOnly 속성을 활성화 해줍니다.

이전의 캡쳐에서 옵션이 설명된 것처럼 ECMAScript의 일부가 아닌 runtime constructs가 허용되지 않기 때문에 다음과 같이 에러가 나는 모습을 볼 수 있습니다.

  • erasableSyntaxOnly 설정을 true로 설정하면, 앞서 예로 든 enum, namespace, 클래서 매개변수 속성 등 런타임 변환이 필요한 TS 구문을 전부 막아주게 됩니다.
  • 해당 구문들이 등장하면 컴파일 에러가 발생하므로, 순수한 타입 주석만 사용하는 코드만이 허용됩니다.

글을 마치며

정리해보자면,

  1. TypeScript에는 enum, namespace, 클래스 매개변수 속성처럼 런타임 코드가 남는 기능들도 포함되어 있어, “단순히 타입만 추가하는 언어”라고 말하기엔 다소 무리가 있습니다.
  2. 이러한 문법들은 Node.js의 --strip-types처럼 “타입만 제거하는 방식”과 충돌이 생길 수 있고, 그 때문에 제안된 옵션이 바로 erasableSyntaxOnly입니다.

추가로, 개인적인 추측을 덧붙이자면, 이 옵션을 활용하면 별도의 변환 과정을 생략해 컴파일 효율을 높이고, 결과물과 소스 코드가 거의 동일해져 디버깅에도 이점이 있을 것으로 보입니다. 다만 이는 어디까지나 제 예상이며, 아직 베타 기능이니 만큼 실제 적용 전에는 충분한 검토와 테스트가 필요합니다.

한편, 저는 이러한 옵션들이 “타입과 JavaScript 코드를 완전히 분리”하고자 하는 타입스크립트의 미래 방향성을 보여주는 것이 아닐까 하는 생각도 듭니다. 만약 향후 ECMAScript 표준에서 타입 문법을 본격적으로 지원하게 된다거나 타입스크립트에서 erasable한 값만 남기게 된다면, 미리 이와 같은 분리 방식을 염두에 두고 코드를 작성해두었을 때 마이그레이션 시점에서 발생할 수 있는 어려움을 덜 수 있겠죠.

물론 정답은 없습니다.

어떤 팀은 enum이나 클래스 매개변수 속성을 적극 활용해 생산성을 높일 수 있고, 다른 팀은 “정적 타입 체크만을 원한다”며 런타임 구문을 최소화하는 방식을 선호할 수 있습니다. 중요한 건 프로젝트의 목표와 팀의 합의에 맞춰, TypeScript가 제공하는 여러 옵션들을 적절히 선택하는 것입니다.

erasableSyntaxOnly 역시 그런 선택지 중 하나로 보이는 만큼, 앞으로 이 기능이 더 다듬어지는 과정을 관심 있게 지켜봐도 좋겠습니다.

긴 글 읽어주셔서 감사합니다.

profile
Time waits for no one

0개의 댓글

관련 채용 정보