TypeScript를 왜 쓰는가
핵심 내용
초기 웹은 문서였습니다. 서버가 HTML을 내려주면 브라우저는 그걸 보여주는 게 전부였고, 스크립트는 버튼에 이벤트 하나 다는 수준이었죠. 이 시절엔 타입이 없어도 아무 문제가 없었습니다.
지금은 다릅니다. 컴포넌트, 상태, API, 권한, 배포, 협업이 전부 얽힌 하나의 시스템이 됐습니다. 함수 하나의 반환값이 바뀌면 그걸 쓰는 열 군데가 조용히 깨지고, API 응답 구조가 바뀌면 런타임에 가서야 터집니다. 타입이 갑자기 중요해진 게 아니라, 코드 사이의 계약을 실행 전에 검증할 필요가 규모가 커지면서 같이 커진 겁니다.
JavaScript에도 값 자체엔 타입이 있습니다(typeof "hello" -> "string"). 진짜 문제는 변수, 인자, 반환값에 대한 타입 계약을 실행 전에 강제하지 않는다는 것입니다.
TypeScript 이전에도 이 문제를 막으려는 시도는 많았습니다.
| 방식 | 할 수 있는 일 | 부족한 점 |
|---|---|---|
typeof 검사 | 실행 중 값의 타입을 직접 확인할 수 있음 | 그 코드가 실제로 실행돼야만 문제를 잡음 |
| JSDoc | 주석으로 타입 힌트를 남기고 IDE 도움을 받을 수 있음 | 순수 JS 입장에선 결국 주석이라 잘못된 호출을 막지 못함 |
| PropTypes | React props를 런타임에 검사할 수 있음 | React props 범위에 가깝고 일반 함수, API 응답, 도메인 모델 전체는 커버하기 어려움 |
| 테스트 | 작성한 시나리오를 실제로 실행해서 검증할 수 있음 | 작성한 케이스만 검증함 |
| 다른 언어로 전환 | 언어 차원에서 타입 시스템을 가져갈 수 있음 | 기존 코드, 라이브러리, 팀 경험까지 통째로 바꿔야 해서 비용이 큼 |
다섯 가지 공통점은 전부 사람이 뭔가를 추가로 해야만 작동한다는 것입니다. TypeScript는 정확히 이 지점을 노렸습니다. 사람이 추가로 뭘 안 해도 언어 차원에서 자동으로 확인해주는 것.
예시
function double(n) {
return n * 2;
}
double(10); // 20
double("hello"); // NaN, 에러 없이 조용히 틀린 값이 나옴
function greet(name: string): string {
return name.toUpperCase();
}
greet("kim"); // OK
greet(123); // 실행하기도 전에 컴파일 에러
흐름으로 보면 대략 이런 느낌입니다.
헷갈리기 쉬운 점
TypeScript는 런타임 검증 도구가 아닙니다.
const data = JSON.parse('{"age":"20"}') as { age: number };
function doubleAge(user: { age: number }) {
return user.age * 2;
}
doubleAge(data); // 컴파일 통과. 근데 실제 age는 string
JSON.parse 결과는 런타임에 정해지는데, as로 "number라고 쳐줘"라고 컴파일러한테 우겼을 뿐이기 때문입니다. 컴파일러는 그 말을 그대로 믿습니다.
설계도에 비유하면 이해가 쉽습니다. 설계도대로 짓고 있는지는 시공 전에 철저히 검토하지만, 건물이 다 올라간 뒤에 반입되는 자재까지 설계도가 확인해주진 않습니다. TypeScript의 타입도 똑같습니다. 컴파일 시점엔 철저히 검사하지만, 그 타입 정보 자체가 런타임엔 남아있지 않습니다.
내 코드 안의 타입 계약은 TypeScript가 강하게 잡아주지만, 밖에서 들어오는 값(API 응답, 사용자 입력, JSON.parse)은 별도의 런타임 검증으로 메워야 합니다.
실무에서 볼 점
- API 응답, URL query,
localStorage처럼 밖에서 들어오는 값은 타입 선언만 믿지 말고 런타임 검증을 둡니다. as로 타입 에러를 덮기 전에, 실제 값이 그 타입을 만족하는지 먼저 확인합니다.
예상 질문
TypeScript를 쓰면 JavaScript의 타입 문제가 완전히 해결되나요?
아닙니다. TypeScript는 내 코드 안의 타입 계약을 실행 전에 검증하는 도구에 가깝습니다.
함수 인자, 반환값, 객체 구조처럼 코드 안에서 정의한 관계는 컴파일 단계에서 꽤 강하게 잡아줍니다. 하지만 API 응답, 사용자 입력, JSON.parse, localStorage처럼 밖에서 들어오는 값의 실제 모양까지 자동으로 보장해주진 않습니다.
그래서 TypeScript를 써도 외부 데이터 경계에서는 type guard나 Zod 같은 런타임 검증이 필요합니다. TypeScript는 런타임 버그를 전부 없애는 도구가 아니라, 실행 전에 잡을 수 있는 오류를 최대한 앞으로 당겨오는 도구라고 보는 게 맞습니다.