TypeScript 6.0이 기본값을 바꾼 이유 — strict 기본값, using 선언, Temporal API, 그리고 JS 컴파일러의 마지막
2026년 3월, TypeScript 6.0이 출시됐습니다. 단순한 버전 숫자 증가가 아니라 "이제부터 TypeScript는 엄격함을 기본으로 한다"는 선언이었어요. strict 모드가 기본값이 되고, 컴파일 타깃이 ES2025로 올라가고, 수년간 쌓였던 레거시 옵션들이 한꺼번에 정리됐습니다. 거기다 이 버전은 JavaScript로 작성된 TypeScript 컴파일러의 마지막 릴리즈이기도 합니다. 다음 버전인 TypeScript 7.0은 Go 언어로 전면 재작성(Project Corsa) 중이에요. 이 글에서는 strict 기본값 변경, using 선언, Temporal API, 서브패스 임포트, 그리고 Go 기반 7.0 전환까지 순서대로 살펴봅니다.
이 글은 TypeScript를 이미 사용 중인 팀을 기준으로 씁니다. TypeScript를 아직 도입 전이라면, strict가 기본값이 된 6.0 이후가 오히려 시작하기 좋은 시점입니다 — 처음부터 엄격한 환경에서 시작하면 나중에 빚을 갚을 필요가 없으니까요.
저도 처음 릴리즈 노트를 봤을 땐 "뭐, 이 정도야"했는데, 막상 사내 프로젝트를 올려보니 타입 오류가 30개 넘게 쏟아지더라고요. 기존 코드에 strict: false 의존도가 얼마나 깊게 박혀 있는지 그때 실감했습니다. 그 경험을 바탕으로, 마이그레이션 과정에서 신경 써야 할 것들도 함께 다뤄볼게요.
핵심 개념
strict 모드가 드디어 기본값이 됐습니다
솔직히 이게 가장 파급력이 큰 변화입니다. 지금까지 TypeScript를 새로 시작할 때 tsconfig.json에 "strict": true를 직접 넣지 않으면, 아래의 체크들이 조용히 꺼진 상태였어요.
| 체크 항목 | 설명 |
|---|---|
strictNullChecks |
null/undefined를 명시적으로 처리해야 함 |
noImplicitAny |
암묵적 any 타입 금지 |
strictFunctionTypes |
함수 매개변수 타입의 안전성 강제 |
useUnknownInCatchVariables |
catch 블록 변수의 타입이 unknown |
strictPropertyInitialization |
클래스 프로퍼티 반드시 초기화 |
strictFunctionTypes가 생소할 수 있어서 짧게 설명하면, 이런 상황을 잡아줍니다:
// strictFunctionTypes 켜진 상태에서
type Handler = (value: string | number) => void;
function handleString(value: string) {
console.log(value.toUpperCase()); // string 메서드만 사용
}
// Handler는 number도 받을 수 있는데, handleString은 string만 처리함
// → 런타임에 number가 들어오면 터질 수 있으니 타입 오류!
const handler: Handler = handleString; // Error!이제 tsconfig.json에 아무것도 안 써도 이 모든 체크가 켜진 상태에서 시작됩니다. 새 프로젝트라면 반갑겠지만, 기존 프로젝트를 6.0으로 올리면 한번에 타입 오류가 쏟아질 수 있어요.
기본 컴파일 타깃이 ES2025로 올라갔습니다
TypeScript의 기본 target이 ES3이었다는 게 솔직히 좀 황당하기도 했죠. 2026년에 Internet Explorer를 위한 코드를 기본으로 뽑고 있었던 셈이니까요.
// 6.0 이전의 기본값 (체감상)
{
"compilerOptions": {
"target": "ES3", // 거의 2000년대 초반 수준
"module": "commonjs",
"strict": false
}
}// 6.0부터의 기본값
{
"compilerOptions": {
"target": "ES2025", // 현대 런타임 기준
"module": "esnext",
"moduleResolution": "nodenext",
"strict": true
}
}ESM(ECMAScript Module):
import/export문법을 사용하는 JavaScript 표준 모듈 시스템입니다. 기존의 CommonJS(require())와 달리 정적 분석이 가능하고, 브라우저 네이티브 지원이 됩니다.
JavaScript 기반 마지막 릴리즈라는 의미
TypeScript 7.0은 "Project Corsa"라는 이름으로 Go 언어를 사용해 컴파일러를 완전히 다시 작성하고 있습니다. Microsoft가 Rust 대신 Go를 선택한 이유는 TypeScript 컴파일러의 공유 가변 상태와 재귀적 타입 추론 구조가 Go와 더 잘 맞기 때문이라고 해요.
Microsoft가 공개한 내부 벤치마크 기준 성능 향상 수치가 꽤 인상적입니다.
| 프로젝트 | 기존 (JS) | TypeScript 7.0 (Go) | 속도 향상 |
|---|---|---|---|
| VS Code (1.5M LOC) | 89초 | 8.74초 | 10.2배 |
| Sentry | 133초 | 16초 | 8.2배 |
| Playwright | 11.1초 | 1.1초 | 10.1배 |
| TypeORM | 17.5초 | 1.3초 | 13.5배 |
6.0에서 레거시 옵션들을 대거 정리하는 이유가 바로 여기 있어요. 7.0 Go 컴파일러로 넘어갈 때 기술 부채를 최소화하기 위한 사전 정리 작업인 셈입니다. 참고로 6.x 마이너 버전 출시 계획은 없고, 6.0이 유일한 6.x 릴리즈입니다.
실전 적용
예시 1: using 선언으로 리소스 누수 없애기
using 선언은 TypeScript 5.2(2023년 8월)에서 처음 도입됐고, 6.0에서 권장 패턴으로 더욱 자리를 잡은 기능입니다. ECMAScript Explicit Resource Management 제안과 연동되어, try-finally 없이 자원을 안전하게 정리할 수 있어요.
실무에서 자주 맞닥뜨리는 상황인데, 파일이나 DB 커넥션을 열고 예외가 발생하면 cleanup이 안 되는 경우가 생각보다 많습니다.
// 이전 방식 — finally 잊으면 리소스 누수
function processFile() {
const file = openFile('data.txt');
try {
doWork(file);
} finally {
file.close(); // 깜빡하면 끝
}
}
// using 선언 — 스코프 이탈 시 [Symbol.dispose]() 자동 호출
function processFile() {
using file = openFile('data.txt'); // 예외가 나도, 정상 종료돼도 반드시 닫힘
doWork(file);
}using 키워드가 동작하려면 해당 객체가 [Symbol.dispose]() 메서드를 구현하고 있어야 합니다. Node.js의 대부분 내장 API는 아직 이를 직접 지원하지 않기 때문에, 직접 래퍼를 만들거나 라이브러리를 활용해야 해요. 아래처럼 구현할 수 있습니다.
import * as fs from 'fs';
// [Symbol.dispose]()를 구현한 파일 핸들 래퍼
function openFile(path: string) {
const handle = fs.openSync(path, 'r');
return {
read(buffer: Buffer, offset: number, length: number, position: number) {
return fs.readSync(handle, buffer, offset, length, position);
},
[Symbol.dispose]() {
fs.closeSync(handle);
}
};
}
// DB 커넥션도 같은 방식으로
class DatabaseConnection {
constructor(private conn: Connection) {}
query(sql: string) {
return this.conn.execute(sql);
}
[Symbol.dispose]() {
this.conn.close();
}
}
async function queryData() {
using db = new DatabaseConnection(await connect());
return await db.query('SELECT * FROM users');
// 블록을 벗어나는 순간 db.conn.close() 자동 호출
}비동기 cleanup이 필요하다면 await using과 [Symbol.asyncDispose]()를 쓸 수 있습니다.
[Symbol.dispose]: ECMAScript 표준에 추가된 well-known Symbol입니다.using선언은 스코프를 벗어날 때 이 메서드를 자동으로 호출해 리소스를 정리합니다. C#의IDisposable, Python의__exit__와 유사한 개념입니다.
예시 2: #/ 서브패스 임포트로 경로 지옥 탈출
../../utils/formatDate 같은 상대 경로 임포트, 파일을 이리저리 옮기다 보면 관리가 정말 힘들죠. Node.js의 subpath imports 기능은 12.7 버전부터 지원됐고, TypeScript에서는 4.7부터 타입 지원이 가능했는데, TypeScript 6.0에서 IDE 자동완성이 크게 개선됐습니다. #/으로 시작하는 경로에 대한 자동완성과 정의로 이동이 훨씬 자연스럽게 동작해요.
핵심은 package.json의 imports 필드와 tsconfig.json의 moduleResolution 설정이 같이 맞아야 한다는 점입니다. 두 파일을 함께 보는 게 중요해요.
// package.json — subpath imports 정의
{
"name": "my-app",
"imports": {
"#/*": "./dist/*",
"#utils/*": "./src/utils/*",
"#lib/*": "./src/lib/*"
}
}// tsconfig.json — moduleResolution이 nodenext 또는 bundler여야 함
{
"compilerOptions": {
"moduleResolution": "nodenext",
"module": "esnext"
}
}// 이전 방식 — 파일 위치 바뀌면 일일이 수정
import { formatDate } from '../../../../utils/formatDate';
import { apiClient } from '../../../lib/api/client';
// subpath imports
import { formatDate } from '#utils/formatDate';
import { apiClient } from '#lib/api/client';참고로, 6.0에서 --baseUrl 옵션이 제거됐습니다. baseUrl: "." 과 paths를 함께 쓰던 패턴을 사용 중이라면, 서브패스 임포트로 전환하거나 paths를 tsconfig.json 파일 기준 상대 경로로 재설정하는 방향을 고려해볼 수 있습니다. 정확한 마이그레이션 방법은 아래 참고 자료의 공식 릴리즈 노트에서 확인할 수 있어요.
예시 3: Temporal API로 날짜 계산 안전하게 하기
JavaScript의 Date 객체가 얼마나 불편한지는 다들 아실 텐데요. 저도 타임존이 섞인 날짜 계산을 할 때마다 moment.js나 date-fns를 꺼내 쓰는 게 당연하다고 생각했는데, Temporal API가 나오면서 그 필요성이 크게 줄어들 것 같더라고요.
TypeScript 6.0에서는 esnext.temporal lib 옵션을 통해 Temporal의 타입 정의를 사용할 수 있습니다.
// tsconfig.json
{
"compilerOptions": {
"lib": ["esnext", "esnext.temporal"]
}
}// 날짜 계산 — 완전 타입 지원
const now = Temporal.Now.plainDateISO();
const deadline = Temporal.PlainDate.from('2026-12-31');
const duration = now.until(deadline);
console.log(`마감까지 ${duration.days}일 남았습니다`);
// 타임존 처리도 직관적
const seoulTime = Temporal.Now.zonedDateTimeISO('Asia/Seoul');
const nyTime = seoulTime.withTimeZone('America/New_York');
console.log(nyTime.toString());⚠️ 런타임 주의: Temporal은 현재 TC39 Stage 4에 막 진입한 단계로, 주요 브라우저와 Node.js에서 아직 플래그 없이 완전히 지원되지 않을 수 있습니다. TypeScript에서 타입 오류 없이 코드를 작성할 수 있어도, 런타임에서
Temporal is not defined오류가 발생할 수 있어요. 실제 사용 시에는@js-temporal/polyfill을 함께 설치하는 것을 권장합니다.
예시 4: CSS 임포트 오류 대응하기
noUncheckedSideEffectImports가 기본으로 켜지면서 Vite나 webpack 프로젝트에서 CSS를 그냥 임포트하면 에러가 납니다. 저도 처음엔 "왜 갑자기 CSS 임포트가 안 되지?" 했는데, 앰비언트 모듈 선언을 추가하면 해결됩니다.
// 6.0 업그레이드 후 TS2882 에러 발생
import './style.css'; // Error: 파일을 찾을 수 없음
import 'font-awesome/css/font-awesome.css'; // 동일 에러// 해결 방법: global.d.ts 또는 vite-env.d.ts에 추가
declare module '*.css';
declare module '*.scss';
declare module '*.sass';
declare module '*.less';
declare module '*.svg';
declare module '*.png';Vite 프로젝트라면 기본적으로 생성되는 vite-env.d.ts에 추가하는 게 깔끔합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 타입 안전성 향상 | strict 기본값으로 새 프로젝트부터 누락 없는 타입 체크 적용 |
| 현대 생태계 정렬 | ES2025 타깃, esnext 모듈, nodenext 해석이 2026년 현실과 일치 |
| 리소스 누수 방지 | using 선언으로 cleanup 로직을 구조적으로 보장 가능 |
| 7.0 마이그레이션 준비 | 6.0 전환 완료 시 Go 기반 10배 빠른 7.0으로의 전환이 수월 |
| 번들 사이즈 감소 | AMD/UMD/SystemJS 레거시 모듈 포맷 제거로 코드 정리 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 기존 프로젝트 빌드 중단 | strict 기본값 변경으로 한꺼번에 타입 오류 발생 | tsconfig에 "strict": false 명시 후 점진적 수정 |
| 레거시 옵션 제거 | moduleResolution: "classic", --outFile, --baseUrl 삭제 |
ts5to6 도구로 tsconfig 자동 마이그레이션 |
| CSS 임포트 오류 | noUncheckedSideEffectImports 기본 활성화 |
global.d.ts에 앰비언트 모듈 선언 추가 |
| ES5 타깃 지원 종료 | IE 지원 레거시 프로젝트 대응 어려움 | Babel 등 별도 트랜스파일러 구성 필요 |
esModuleInterop 강제 |
false로 설정 불가, CJS/ESM 혼용 프로젝트 영향 | import 방식 점검 및 수정 필요 |
| Temporal 런타임 미지원 | 타입은 되지만 런타임에서 오류 가능 | @js-temporal/polyfill 함께 설치 |
esModuleInterop: CommonJS 모듈을
import문법으로 불러올 때 호환성을 보장하는 옵션입니다.import React from 'react'처럼 default import를 가능하게 해주는데, 6.0부터는 항상 켜진 상태로 고정됩니다.
실무에서 가장 흔한 실수
-
strict 오류를 한번에 다 고치려다 지치는 경우 — 기존 프로젝트를 올릴 때는 일단 tsconfig에
"strict": false를 명시해 빌드를 살린 뒤, 파일 단위로 개별 strict 옵션을 점진적으로 활성화하는 방식이 훨씬 현실적입니다. 한번에 수십 개 오류와 씨름하다 번아웃이 오는 경우를 자주 봤어요. -
#/서브패스 임포트를 moduleResolution 설정 없이 쓰려는 경우 —moduleResolution: "node"구버전 설정에서는 동작하지 않습니다."nodenext"또는"bundler"로 변경한 뒤 사용해야 합니다. -
Temporal API를 lib 설정 없이 쓰려는 경우 —
"target": "esnext"만 설정한다고 자동으로 Temporal 타입이 로드되지 않는 프로젝트도 있습니다."lib": ["esnext", "esnext.temporal"]을 명시적으로 추가하는 게 안전하고, 런타임 폴리필도 잊지 않는 것이 좋습니다.
마치며
TypeScript 6.0은 "TypeScript를 쓴다는 것이 이제부터 무엇을 의미하는지"를 다시 정의한 릴리즈입니다. 단기적으로 마이그레이션 비용이 있는 건 사실이지만, 이 과정을 거치고 나면 strict 환경이 당연하게 느껴지고, Go로 재작성된 TypeScript 7.0이 등장했을 때도 준비된 상태로 맞이할 수 있습니다.
지금 바로 시작해볼 수 있는 방향을 상황별로 나눠봤습니다.
기존 프로젝트라면:
npx ts5to6실행 — TypeScript 팀의 Andrew Branch가 만든 공식 도구로, 현재 tsconfig에서 변경이 필요한 항목을 자동으로 분석하고 마이그레이션해줍니다. 모노레포와 project references 환경도 잘 지원해요.- 빌드 살리기 우선 — strict 오류가 많다면 tsconfig에
"strict": false를 일단 명시한 뒤, 팀 일정에 맞게 점진적으로 수정해나가는 방식을 권장합니다.
새 프로젝트라면: 3. 6.0 기본값 그대로 시작 — 별도 tsconfig 설정 없이 strict, ES2025, nodenext가 모두 켜진 환경에서 시작해볼 수 있습니다. 처음부터 엄격한 환경에서 개발하면 나중에 레거시 부채를 갚을 일이 없어요.
이 글을 다 읽고 나면 TypeScript 6.0의 변화를 "귀찮은 마이그레이션 이슈"가 아닌 "7.0 시대를 준비하는 필연적인 정리"로 바라볼 수 있을 겁니다.
참고 자료
- Announcing TypeScript 6.0 | Microsoft DevBlogs
- TypeScript 6.0 Release Notes | typescriptlang.org
- Announcing TypeScript 6.0 RC | Microsoft DevBlogs
- Announcing TypeScript 6.0 Beta | Microsoft DevBlogs
- TypeScript 5.x to 6.0 Migration Guide | GitHub Gist (privatenumber)
- TypeScript 6.0 Is Here, And Microsoft Is Rebuilding the Entire Compiler in Go | Nandann
- TypeScript 7.0 and Project Corsa: The Go Rewrite Guide | CodeWithSeb
- What's New in TypeScript 6.0 | OpenReplay
- TypeScript 6.0 and CSS Side-Effect Imports | Schalk Neethling
- TypeScript 6.0 vs 5.x: Complete Migration Guide | DevBolt