TypeScript 7.1 베타, 뭐가 달라지나 — API가 돌아왔다
TypeScript 7.0이 나왔을 때 속도는 10배쯤 빨라졌는데, 정작 도구 만드는 분들한테는 빈손이었어요. 프로그램으로 컴파일러를 부르는 API가 없었거든요. 그래서 Volar, Svelte 언어 서비스, typescript-eslint, Angular 템플릿 체커 같은 도구들은 6.0에 묶여 있었습니다. 10월 20일에 나온 7.1 베타는 그 구멍을 메우는 릴리스예요. 새 API가 돌아왔고, Vue·Svelte·Astro 같은 싱글 파일 형식을 위한 content mapper라는 새 플러그인 지점도 생겼습니다. 언어 기능도 몇 가지 들어왔고, 속도·메모리도 7.0보다 더 좋아졌어요. 베타라서 당장 갈아탈 건 아니지만, 6.0에 묶여 있던 팀이라면 지금 브랜치에서 한 번 만져볼 만합니다.
설치는 한 줄이면 됩니다.
```bash
npm install -D typescript@beta
```
**1. 새 API — 7.0에서 빠져 있던, 도구가 컴파일러를 부르는 방법**
문제는 이거예요. 7.0은 Go로 다시 쓴 네이티브 컴파일러라 빨랐지만, 공개 API가 없어서 에디터 플러그인이나 린터가 컴파일러를 직접 부를 수가 없었습니다. 7.1은 API를 통째로 하나 내놓는 대신, 용도별로 나눈 여러 진입점으로 내놨어요.
| 진입점 | 용도 |
| --- | --- |
| `typescript/sync` | 동기 컴파일러 클라이언트 (옵션, 타입 체커, 프로그램 API) |
| `typescript/async` | 같은 것의 Promise 버전 |
| `typescript/fs` | 요청 단위 파일시스템 (전체·레이어드) |
| `typescript/ast` | AST 노드 타입과 `SyntaxKind`, `ScriptTarget` 같은 enum |
| `typescript/ast/is` | 노드 타입 가드 |
| `typescript/ast/factory` | `create*`, 불변 `update*` 함수 |
간단한 컴파일은 이렇게 합니다. 공식 발표의 예제를 그대로 가져왔어요.
```ts
import { API, formatDiagnosticsWithColorAndContext, type Diagnostic } from "typescript/sync";
using api = new API();
const reportErrors = (diagnostics: readonly Diagnostic[]) => {
console.log(formatDiagnosticsWithColorAndContext(diagnostics, api));
};
const parsedCommandLine = api.parseCommandLine(process.argv.slice(2));
if (parsedCommandLine.errors.length > 0) {
reportErrors(parsedCommandLine.errors);
process.exit();
}
using program = api.createProgram(parsedCommandLine.fileNames, parsedCommandLine.options);
const diagnostics = [
...program.getGlobalDiagnostics(),
...program.getProgramDiagnostics(),
...program.getSyntacticDiagnostics(),
...program.getSemanticDiagnostics(),
];
if (diagnostics.length > 0) {
reportErrors(diagnostics);
}
const emitResult = program.emit();
if (emitResult.diagnostics.length > 0) {
reportErrors(emitResult.diagnostics);
}
```
여기서 `using`이 핵심입니다. `API`와 `Program`은 `Disposable`이라서, 다 쓰고 나면 직접 닫아줘야 해요. `api.close()`, `program.dispose()`로 직접 닫거나, 위처럼 `using`으로 스코프를 벗어날 때 자동으로 닫히게 하세요. 그냥 가비지 컬렉터에 맡기면 Go 쪽에서 누수가 납니다. 그리고 이 예제는 `tsconfig.json`을 자동으로 읽지 않고, `--project`도 지원하지 않습니다. 파일명과 옵션을 명시적으로 넘기는 방식이에요.
파일이 바뀌는 워크플로에는 스냅샷이 있습니다. `api.createSnapshot()`으로 지금 상태를 통째로 찍고, `snapshot.update()`로 바뀐 파일만 레이어로 얹어서 새 상태를 만듭니다. 프로그램마다 id가 유지돼서, 바뀐 뒤에도 이전 스냅샷의 프로그램으로 진단을 물어볼 수 있어요. 단, 스냅샷끼리 객체를 섞어 쓰면 안 되고, 스냅샷도 `Disposable`이라 다 쓰면 닫아야 합니다.
**2. Content Mapper — .vue, .svelte 같은 파일도 타입체크 안으로**
두 번째로 큰 변화예요. Vue 파일 하나 안에는 HTML, 자바스크립트, CSS가 같이 들어 있고, Astro·Svelte·Ember도 비슷합니다. Volar가 이런 파일에 에디터 지원을 해줘 왔지만, 명령줄 타입체커는 Volar와는 전혀 다른 API로 붙어야 해서 제대로 쓸 수가 없었어요. 7.1부터는 Volar의 핵심 모델이 공식 기능이 됐습니다. content mapper라는 플러그인 지점이에요.
mapper는 특정 확장자에 등록돼서, 그 파일을 TypeScript가 볼 수 있는 가상 표현으로 바꿔줍니다. 원래 파일의 어느 위치에 해당하는 코드인지 매핑도 같이 넘겨서, 에러 위치와 자동완성, 이동이 원래 파일 기준으로 정확히 나옵니다. `tsconfig.json`에 이렇게 등록해요.
```json
{
"compilerOptions": {
// ...
},
"contentMappers": [
{
"package": "@vue/content-mapper",
"extensions": [".vue"],
"options": {
"target": 3.5
}
}
]
}
```
주의할 게 두 가지예요. 첫째, 위의 `@vue/content-mapper`는 공식 발표의 예시 이름이고, 지금 npm에 있는 패키지가 아닙니다. 실제 mapper는 Astro, CSS Modules, Ember, MDX, Svelte, Vue용으로 프로토타입이나 구현이 나와 있는 상태라, 쓰는 프레임워크 쪽에서 제공하는 걸 확인하고 써야 합니다. 둘째, mapper는 사실상 임의의 실행 코드라서, 명령줄에서는 꼭 `--runExternalCode` 플래그를 붙여야 돌아갑니다.
```bash
tsc --runExternalCode
```
에디터(VS Code)에서는 workspace trust와 content mapper 권한을 허용하면 그냥 동작하고, `js/ts.contentMappers.enabled` 설정으로 끌 수도 있습니다.
**3. 언어 기능 — import attributes, source phase imports, es2026**
나머지는 개발자가 직접 만지는 부분이에요.
먼저 ambient module 선언에 import attributes를 쓸 수 있게 됐습니다. 지금까지는 `declare module "*.css"`처럼 확장자 패턴으로만 선언했는데, 런타임이 신경 쓰는 건 확장자가 아니라 `with { type: "css" }` 같은 속성이에요. 속성을 빼먹으면 에러도 안 나고, `.txt`랑 `.md`를 둘 다 `type: "text"`로 쓰는데도 따로 선언해야 했죠. 7.1부터는 이렇게 씁니다.
```ts
declare module "*" with { type: "css" } {
const content: CSSStyleSheet;
export default content;
}
declare module "*" with { type: "text" } {
const content: string;
export default content;
}
declare module "*" with { type: "bytes" } {
const content: Uint8Array;
export default content;
}
```
이렇게 해두면 속성을 빼먹고 import하면 에러가 나고, 속성에 맞는 타입이 나옵니다. 다만 이 선언들은 내장 라이브러리에 들어 있지 않아서, 라이브러리가 배포 패키지에 넣으면 안 되고, 필요하면 각자 프로젝트에서 직접 선언해야 합니다.
다음은 source phase imports예요. `import source`로 모듈을 실행하지 않고, 파싱·분석된 상태의 모듈 소스만 가져옵니다. WebAssembly에 쓰면 이렇게 됩니다.
```ts
import source SuperFastThing from "./super-fast-thing.wasm";
import { WASI } from "node:wasi";
const wasi = new WASI({ /*...*/ });
const fastThingInstance = await WebAssembly.instantiate(SuperFastThing, {
wasi_snapshot_preview1: wasi.wasiImport,
});
wasi.start(fastThingInstance);
```
동적 버전은 `await import.source("./super-fast-thing.wasm")`으로 씁니다. 자바스크립트 모듈의 source import는 아직 할 수 있는 게 거의 없고, 많은 도구도 지원을 안 하니, 당장은 wasm 쪽이 실전입니다. `--module`은 `esnext`, `nodenext`, `preserve` 중 하나, `--lib`이나 `--target`에는 `esnext`가 필요합니다.
마지막으로 `--target es2026`과 `--lib es2026`이 정식으로 들어왔습니다. `Math.sumPrecise()`, `Error.isError()`, `Map.prototype.getOrInsert()`, `Uint8Array`의 `toBase64()`·`fromBase64()`·`toHex()`·`fromHex()`, `Array.fromAsync()` 같은 것들이고, `--lib esnext`에는 `Promise.allKeyed`, `Iterator.zip`, `Iterator.prototype.chunks` 같은 제안 단계 메서드들이 추가됐습니다.
**속도는 얼마나 빨라졌나**
공식 발표의 측정값입니다 (4개 타입체커 스레드, `--noEmit` 기준, 7.0 대비).
| 코드베이스 | 7.0 | 7.1 베타 | 빨라진 정도 |
| --- | --- | --- |
| mui-docs | 22.2초 | 11.9초 | 1.9배 (46.6% 단축) |
| vscode | 21.6초 | 17.7초 | 1.2배 (17.7% 단축) |
메모리는 mui-docs가 8,093 MiB에서 4,079 MiB로 49.6% 줄었고, vscode는 8,387 MiB에서 6,943 MiB로 17.2% 줄었습니다. 7.0 자체가 6.0보다 10배쯤 빨랐던 데서, 한 번 더 다듬은 수준이에요.
**흔한 함정 3가지**
1. **API·Program·스냅샷을 닫지 않는 것.** `using` 없이 `new API()`만 하고 끝내면 Go 쪽 리소스가 남습니다. `api.close()`, `program.dispose()` 중 하나로 꼭 닫으세요. 스냅샷도 마찬가지입니다.
2. **content mapper를 그냥 켜는 것.** mapper는 임의 코드라 `--runExternalCode` 없이는 명령줄에서 안 돌고, 믿을 수 없는 패키지의 mapper는 켜지 마세요. 그리고 `@vue/content-mapper` 같은 예시 이름을 그대로 설치하려 들지 마세요. 지금 npm에 없습니다.
3. **7.1 API가 나왔다고 도구가 바로 따라오는 줄 아는 것.** API는 컴파일러 쪽 준비가 끝난 거고, Volar·typescript-eslint 같은 각 도구가 7.1 API에 맞게 업데이트를 해야 실제로 갈아탈 수 있습니다. 자기 도구가 7.1을 지원하는지 먼저 확인하세요. 또 베타라서, 주력 프로젝트의 컴파일러를 바로 바꾸기보다 브랜치나 CI에서 먼저 돌려보는 게 맞습니다.
그리고 업그레이드할 때 하나 더: 분배 조건부 타입 검사가 엄격해졌습니다. 분배된 분기 안에서는 타입 파라미터를 원래 제약과 무관한 것처럼 쓸 수 없어서, 예전에는 통과하던 코드에서 새 에러가 날 수 있습니다. 공식 발표의 예시대로라면 이런 코드가 이제 에러예요.
```ts
type Show<A extends number, B extends A> = `${B} extends ${A}`;
type Issue<A extends number, B extends A> =
A extends B ?
never :
Show<A, B>; // error! Type 'B' does not satisfy the constraint 'A'.
```
이럴 때는 원래 제약이 필요 없으면 빼거나, 조건부 타입 검사를 하나 더 넣어서 고쳐야 합니다.
**체크리스트**
- [ ] `npm install -D typescript@beta`로 브랜치에서만 설치했나 (주력 컴파일러 교체는 RC 이후에)
- [ ] 내 도구가 7.1 API를 지원하는지 확인했나 (Volar, typescript-eslint, Svelte, Angular 등)
- [ ] API를 쓸 때 `using`이나 `dispose()`로 닫는 코드가 있나
- [ ] content mapper는 믿을 수 있는 패키지 것만 쓰고, `--runExternalCode`를 붙였나
- [ ] `declare module "*" with { type: ... }` 선언이 필요하면 직접 프로젝트에 넣었나
- [ ] 업그레이드 뒤 분배 조건부 타입 관련 새 에러가 뜨는지 확인했나
여러분은 7.0에서 6.0으로 발목 잡혀 있던 도구가 있으셨나요? 저는 content mapper가 Vue·Svelte 쪽 타입체크를 얼마나 편하게 해줄지 궁금하네요. RC는 공식 발표 기준으로 한 달쯤 뒤에 나올 예정이라, 그때 다시 한 번 정리해보겠습니다.
0 0 1
댓글 0
로그인하고 댓글 남기기 →