타입스크립트의 역사와 필요성
커지는 자바스크립트 코드에서 동적 타입이 늦게 드러내는 오류를 재현하고, 타입스크립트가 왜 만들어졌으며 타입 표기가 무엇을 지키고 무엇은 지키지 못하는지 확인합니다.
타입스크립트를 처음 배우려 할 때 자주 듣는 말이 "자바스크립트를 잘하면 굳이 필요 없다"입니다. 짧은 예제에서는 그 말이 맞아 보이지만, 코드와 팀이 커질수록 사정이 달라집니다. 함수에 엉뚱한 종류의 값을 넘긴 실수가 실행 중에 늦게 드러나면, 원인을 찾는 데 많은 시간이 들기 때문입니다.
타입스크립트(TypeScript)는 그런 늦게 드러나는 오류를 코드를 실행하기 전에 찾으려고 만든 언어입니다. 이 글은 자바스크립트의 동적 타입이 어떤 결과를 내는지 먼저 재현하고, 타입스크립트가 등장한 배경과, 타입 표기가 지켜 주는 것과 지켜 주지 못하는 것을 차례로 봅니다.
TYPE / RUNTIME / CONTRACT
실행 전 피드백과 실행 중 입력 검증을 구분한다
JavaScript의 add(10, "20")는 "1020"을 만들 수 있습니다. number 매개변수로 의도를 표현하면 같은 호출을 작성 단계에서 진단할 수 있지만, 타입 표기가 외부 입력을 검증해 주지는 않습니다.
LESSON EXAMPLE
덧셈 의도와 문자열 연결 결과
본문 JavaScript 예제는 숫자끼리30, 문자열끼리 연결, 숫자10과 문자열"20"을 함께 넘기면 "1020"을 반환합니다. 마지막 호출도 JavaScript에서는 예외 없이 실행됩니다.
STATIC CONTRACT
매개변수의 number가 호출을 제한
타입은 작성 단계에서 의도를 비교하는 도구입니다. 에디터와 컴파일러의 진단을 통해 계약에 맞지 않는 인자를 찾을 수 있습니다.
RUNTIME BOUNDARY
외부 값에는 실행 중 검증이 필요
타입 표기는 JavaScript 산출물에서 제거됩니다. 서버 응답과 사용자 입력은 unknown으로 받아 모양과 값의 조건을 확인한 뒤 사용합니다. 이 절의 add 예제에는 validator가 구현되어 있지 않습니다.
LIMIT / BUILD POLICY
검사 통과와 실행 성공은 별개
any와 부정확한 단언은 검사를 약하게 만듭니다. noEmitOnError는 오류가 있는 산출물 생성을 제어하는 별도 옵션입니다. 타입 도입 자체를 모든 버그의 제거로 해석하지 않습니다.
동일한 잘못된 호출을 JavaScript에서는 실행 결과1020으로 관찰하고 TypeScript number 계약에서는 실행 전 진단한다. 외부 입력은 별도의 런타임 검증을 거쳐 사용한다. 타입 단언이나 any는 이 보장을 약하게 만들 수 있다.
자바스크립트의 성장과 동적 타입
자바스크립트(JavaScript)는 웹 페이지에 동작을 더하는 언어로 시작해, 이제는 Node.js 서버와 React Native 모바일 앱, Electron 데스크톱 앱까지 만드는 언어가 되었습니다. 이 교재는 웹 기초 교재의 함수와 스코프에서 다룬 함수의 인자와 반환값을 알고 있다고 전제하고, 그 위에 타입의 표기를 더합니다.
자바스크립트는 동적 타입(dynamic typing) 언어입니다. 변수에 어떤 종류의 값이든 넣을 수 있고, 값의 타입은 실행하는 중에 정해집니다. 이 유연함은 빠르게 만들어 보기에는 좋지만, 코드가 커지면 예상하지 못한 값의 조합을 만들어 냅니다.
function add(a, b) {
return a + b;
}
console.log(add(10, 20));
console.log(add("Hello, ", "TypeScript"));
console.log(add(10, "20"));30
Hello, TypeScript
1020add는 숫자의 덧셈을 의도했지만, 세 호출 모두 오류 없이 실행됩니다. 마지막 호출은 숫자 10과 문자열 "20"을 받았고, 자바스크립트의 +는 한쪽이 문자열이면 다른 쪽도 문자열로 바꾸어 이어 붙이므로 "1020"을 돌려줍니다. 실행해 결과를 보기 전까지는 이 실수를 알 수 없고, 이런 값이 몇 단계를 거쳐 화면이나 데이터베이스에 닿은 뒤에야 문제가 드러나기도 합니다.
타입스크립트의 등장
타입스크립트는 마이크로소프트가 만들어 오픈 소스로 공개한 언어로, 2012년 10월에 처음 공개되었고 C#을 설계한 앤더스 헤일스버그(Anders Hejlsberg)가 설계를 이끌었습니다. 타입스크립트는 자바스크립트의 상위 집합(superset)이라고 불립니다. 자바스크립트의 문법을 그대로 받아들이고, 그 위에 타입의 표기와 정적인 검사를 더했다는 뜻입니다.
다만 문법을 받아들이는 것과 타입 검사를 통과하는 것은 다릅니다. 자바스크립트에서 잘 실행되던 코드라도, 값을 쓰는 방식이나 검사의 설정에 따라 타입스크립트가 진단을 낼 수 있습니다. 컴파일러는 이후 버전을 거듭하며 기능을 넓혔고, 2025년에는 컴파일러를 Go 언어로 다시 구현하는 계획이 발표되었고, 6.0이 그 전환을 준비한 뒤 2026년 7월에 Go로 만든 7.0이 나왔습니다. 7.0은 6.0과 기본값이 같지만, 6.0에서는 "ignoreDeprecations": "6.0"으로 끌 수 있던 폐기 예정 옵션의 오류를 끌 수 없게 했고 아직 컴파일러 API를 제공하지 않아, 그 API를 쓰는 일부 도구는 6.0을 함께 써야 합니다. 이 교재는 컴파일러의 기본값과 진단을 TypeScript 6.0을 기준으로 설명하고, 버전을 고정해 따라 하는 16장의 실전 프로젝트는 그 장에서 고정한 버전을 따로 밝힙니다.
타입을 표기하면 다음과 같은 것을 얻습니다.
| 얻는 것 | 내용 |
|---|---|
| 실행 전의 오류 발견 | 매개변수를 number로 선언하면, 문자열을 넘기는 호출을 코드를 쓰는 단계에서 진단한다 |
| 읽기 쉬운 코드 | 변수와 함수가 어떤 데이터를 다루는지 타입이 드러내므로, 다른 사람이 코드를 이해하고 고치기 쉽다 |
| 도구의 도움 | 편집기가 타입 정보로 자동 완성과 이름 바꾸기, 정의로 이동을 정확하게 제공한다 |
| 협업 | 여러 사람이 함께 고치는 큰 프로젝트에서, 함수와 데이터의 약속을 코드에 남긴다 |
그렇다고 타입스크립트를 도입하는 것만으로 품질이 저절로 보장되지는 않습니다. any를 남용하거나, 확인하지 않은 값에 타입 단언을 쓰거나, 검사의 설정을 느슨하게 두면 자바스크립트와 크게 다르지 않게 됩니다. 중요한 것은 타입을 붙이는 행위 자체가 아니라, 타입으로 코드의 의도를 분명하게 나타내는 것입니다.
타입 표기가 지키는 것
앞의 덧셈의 의도를 number라는 타입으로 옮겨 봅시다. 아직 실행 환경을 만들지 않았다면 지금은 코드를 읽기만 하고, 개발 환경 설정을 마친 뒤 별도의 sum.ts 파일에서 확인합니다.
function sumNumbers(left: number, right: number): number {
return left + right;
}
console.log(sumNumbers(10, 20));
// console.log(sumNumbers(10, "20"));30검사를 통과한 코드를 실행하면 첫 호출은 30을 출력합니다. 마지막 줄의 주석을 풀면, 두 번째 인수가 문자열이어서 number 매개변수와 맞지 않는다는 진단이 나옵니다.
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.TS2345는 인수의 타입이 매개변수의 타입에 할당될 수 없다는 진단입니다. 이 진단은 실행하기 전에 나오므로, "1020" 같은 결과가 만들어질 기회 자체가 없습니다. 다만 타입의 표기가 문자열을 숫자로 바꾸어 주는 것은 아니므로, 인수를 처음부터 숫자로 넘길지, 입력을 받는 곳에서 변환하고 검증할지는 요구에 맞추어 정해야 합니다.
타입 검사가 지켜 주지 못하는 것도 있습니다. Number("스무")처럼 숫자로 바꿀 수 없는 문자열을 바꾸면 NaN이 되는데, NaN도 자바스크립트의 숫자 값이므로 타입스크립트의 number에 들어갑니다. 유한한 숫자여야 한다거나 양수여야 한다는 업무의 조건은, 타입이 아니라 실행 중의 검증이 맡습니다. 타입 표기가 실행 전에 지워지고 진단만 남는다는 설명은 공식 Handbook의 기초에 이어집니다.
더 깊이: 설계 목표에 적힌 것과 적히지 않은 것
타입스크립트 팀은 언어의 설계 목표(design goals)를 공식 저장소의 TypeScript Design Goals 문서에 목표와 목표가 아닌 것(non-goals)으로 나누어 적어 두었습니다. 목표에는 출력한 프로그램에 실행 비용을 더하지 않는 것, 모든 자바스크립트 코드의 실행 동작을 그대로 두는 것, 모두 지울 수 있는(fully erasable) 일관된 구조적 타입 시스템을 쓰는 것이 들어 있습니다. 반대로 실행 중의 타입 정보를 프로그램에 더하거나 그 정보에 기대는 것, 그리고 타입 검사의 결과에 따라 다른 코드를 내보내는 것은 목표가 아니라고 밝힙니다. 이 글에서 본 '타입 표기는 지워지고 값은 바꾸지 않는다'는 성질은 우연이 아니라 이 목표에서 나온 것입니다.
같은 문서는 건전한(sound) 타입 시스템, 즉 검사를 통과한 프로그램에는 실행 중 타입 오류가 없다고 증명할 수 있는 체계도 목표가 아니라고 적습니다. 대신 정확성과 생산성 사이의 균형을 고른다고 합니다. 그래서 타입스크립트는 컴파일 시점에는 안전하다고 알 수 없는 연산을 몇 군데에서 일부러 허용하며, Handbook의 타입 호환성(Type Compatibility) 글은 그런 자리마다 허용한 이유를 설명합니다. any와 타입 단언이 검사를 약하게 만든다는 앞의 이야기도 이 균형의 일부로 볼 수 있습니다. 타입 검사를 통과했다는 것은 '컴파일러가 아는 정보 안에서 모순이 없다'는 뜻이지, 실행 중의 모든 값이 맞다는 증명은 아닙니다.
정리
- 자바스크립트는 값의 타입이 실행 중에 정해지는 동적 타입 언어여서,
add(10, "20")같은 실수가"1020"처럼 오류 없이 실행됩니다. - 타입스크립트는 자바스크립트에 타입의 표기와 정적인 검사를 더한 상위 집합이며, 2012년에 마이크로소프트가 공개했습니다.
- 매개변수를
number로 선언하면 문자열을 넘기는 호출을 실행 전에 TS2345로 진단합니다. - 타입의 표기는 값을 바꾸지 않으며,
NaN처럼 타입은 맞지만 업무에 맞지 않는 값은 실행 중의 검증이 필요합니다. any와 확인하지 않은 값의 단언, 느슨한 설정은 타입 검사의 이점을 약하게 만듭니다.
연습 문제
1. 숫자로 바꾼 인수
sumNumbers(10, Number("20"))는 타입 검사와 실행에서 각각 어떤 결과를 보이나요? 이어서 Number("스무")를 넘기면 어떻게 되나요?
해설 보기
Number("20")의 타입은 number이므로 검사를 통과하고, 값은 숫자 20이어서 실행 결과는 30입니다. Number("스무")도 타입은 number라서 검사를 통과하지만, 값은 NaN이고 10 + NaN도 NaN이므로 결과는 NaN입니다. 타입 검사는 "숫자인가"까지만 보장하므로, 변환에 실패했는지는 Number.isNaN이나 Number.isFinite로 따로 확인해야 합니다.
2. 어느 단계에서 발견되는가
다음 세 가지 실수는 각각 타입 검사와 실행 중의 검증 가운데 어디에서 발견되나요? 매개변수는 모두 number라고 합시다. (가) sumNumbers(10, "20")의 호출, (나) 나눗셈 함수에 10과 0을 넘김, (다) 사용자가 나이 입력란에 "abc"를 적음.
해설 보기
(가)는 문자열을 number 매개변수에 넘기므로 타입 검사에서 TS2345로 발견됩니다. (나)는 두 인수가 모두 number라 검사를 통과하고, 자바스크립트의 10 / 0은 오류가 아니라 Infinity를 돌려주므로, 0으로 나누지 않는다는 규칙은 실행 중의 검증이 맡습니다. (다)의 입력값은 실행 중에 문자열로 들어오므로 코드를 쓰는 시점의 타입 검사는 그 값을 알 수 없고, 입력을 받는 곳에서 숫자로 바꾸고 검증해야 합니다.
다음 글 자바스크립트와 타입스크립트의 차이점에서는 두 언어가 오류를 발견하는 시점과 코드가 실행되기까지의 과정이 어떻게 다른지 비교합니다.