본문 바로가기

자바스크립트와 타입스크립트의 차이점

같은 실수를 두 언어가 언제 알려 주는지 비교하고, tsc가 타입을 검사한 뒤 타입을 지운 자바스크립트를 내보내는 과정과 Node.js의 타입 제거 실행이 하지 않는 일을 확인합니다.

타입스크립트를 처음 보면 문법이 거의 같아서 큰 차이가 없다고 느끼기 쉽습니다. 실제로 .js 파일의 확장자만 .ts로 바꾸어도 그대로 통과하는 코드가 많습니다. 두 언어의 차이는 문법보다, 오류를 알게 되는 시점과 코드가 실행되기까지 거치는 과정에서 드러납니다.

이 글은 같은 실수를 두 언어가 각각 언제 알려 주는지 비교하고, 타입스크립트 컴파일러가 타입을 검사한 뒤 타입을 지운 자바스크립트를 내보내는 과정을 봅니다. 마지막으로 이미 있는 자바스크립트 코드에 타입스크립트를 조금씩 들이는 순서를 정리합니다.

동적 타입과 정적 타입

자바스크립트에서 변수의 타입은 그 변수에 지금 들어 있는 값의 타입입니다. 같은 변수에 숫자를 넣었다가 문자열과 불리언을 차례로 넣어도 실행에는 문제가 없고, 값의 타입은 실행하는 동안 바뀝니다.

let value = 10;
value = "hello";
value = true;
console.log(typeof value);
boolean

타입스크립트는 여기에 정적 타입(static typing) 검사를 더합니다. 변수에 타입을 표기하면 그 변수에 들어갈 수 있는 값의 종류가 선언할 때 정해지고, 컴파일러는 실행하기 전에 모든 대입이 그 타입에 맞는지 검사합니다.

let value: number = 10;
value = "hello";
error TS2322: Type 'string' is not assignable to type 'number'.

TS2322는 대입하는 값의 타입이 변수의 타입에 할당될 수 없다는 진단입니다. 앞 글의 TS2345가 함수에 넘기는 인수에 대한 진단이었다면, TS2322는 변수에 값을 넣는 것처럼 값을 옮기는 곳에서 나옵니다.

타입을 직접 쓰지 않아도 검사는 이루어집니다. 타입스크립트는 초기값에서 변수의 타입을 추론(type inference)하고, 이후의 대입을 그 타입으로 검사합니다.

let attempts = 1;
attempts = 2;
// attempts = "두 번";
console.log(attempts);
2

attempts의 초기값이 숫자 1이므로 타입스크립트는 attempts의 타입을 number로 추론합니다. 주석을 그대로 두면 2가 출력되고, 마지막 대입의 주석을 풀면 문자열을 number 변수에 넣는 것이므로 같은 TS2322가 나옵니다. 타입을 적지 않은 것과 타입이 없는 것은 다르며, 편집기에서 추론된 타입을 확인하는 방법은 첫 번째 타입스크립트 프로그램 작성에서 직접 해 봅니다.

코드가 실행되기까지

자바스크립트 파일은 브라우저나 Node.js 같은 런타임(runtime)이 바로 읽어 실행합니다. 런타임 안에서 구문 분석과 최적화가 일어나지만, 개발자가 따로 변환하는 단계는 없습니다.

타입 표기는 자바스크립트의 문법이 아니므로, 브라우저는 타입이 적힌 .ts 파일을 그대로 실행하지 못합니다. 그래서 보통은 타입스크립트 컴파일러인 tsc가 먼저 타입을 검사하고, 타입 표기를 지운 자바스크립트 파일을 내보냅니다(emit). 아래 그림처럼 타입스크립트의 경로에는 실행 전에 검사하는 단계가 하나 더 있습니다.

일반 JavaScript는 소스가 런타임으로 전달된다. TypeScript 경로는 TS소스, 정적 검사와 emit 정책, JavaScript 산출물, 런타임으로 이어진다. 검사 없이 타입 표기만 제거하는 실행 도구는 정적 검사와 별개이다.

TYPE / RUNTIME / CONTRACT

TypeScript 검사는 JavaScript 실행을 앞서 확인한다

TypeScript 소스에는 정적 검사와 JavaScript 출력 단계가 더해집니다. 일반 JavaScript 실행과 두 경로를 비교하되, 런타임의 외부 입력 검증은 양쪽 모두 필요합니다.

JavaScript 실행과 TypeScript 검사·출력 경로서로 독립인 두3노드2간선 경로다. JavaScript 소스→런타임→실제값동작과 TypeScript 소스→정적검사및emit정책→JavaScript실행을 비교한다. 출력 허용 여부는 설정에 따르며 type stripping은 정적검사와 별개이다.일반 JavaScriptJavaScript 소스타입 표기 없음JavaScript 런타임실제 값으로 실행동작 관찰출력 · 오류 확인TypeScriptTypeScript 소스명시 · 추론한 타입정적 검사 · emit출력 정책 적용JavaScript 실행외부 입력은 별도 검증
JavaScript 실행과 TypeScript 검사·출력 경로 · 모바일서로 독립인 두3노드2간선 경로다. JavaScript 소스→런타임→실제값동작과 TypeScript 소스→정적검사및emit정책→JavaScript실행을 비교한다. 출력 허용 여부는 설정에 따르며 type stripping은 정적검사와 별개이다. 모바일에서도 노드와 연결 방향·독립 경로가 같다.일반 JavaScriptJavaScript 소스타입 표기 없음JavaScript 런타임실제 값으로 실행동작 관찰출력 · 오류 확인TypeScriptTypeScript 소스명시 · 추론한 타입정적 검사 · emit출력 정책 적용JavaScript 실행외부 입력은 별도 검증

JAVASCRIPT

같은 변수에 서로 다른 값 할당

본문의 JavaScript value는10, "hello", true로 바뀝니다. 런타임은 실제 값의 동작을 따릅니다. JavaScript에도 별도 도구로 정적 검사를 적용할 수 있습니다.

TYPESCRIPT

number 선언 뒤 문자열 할당을 진단

TypeScript value:number=10에는 문자열을 할당할 수 없다고 진단합니다. 명시한 타입뿐 아니라 초기값과 문맥에서 추론한 타입도 검사에 사용합니다.

OUTPUT POLICY

타입 오류만으로 출력이 항상 막히지는 않는다

tsc의 기본 정책에서는 오류가 있어도 JavaScript가 출력될 수 있습니다. noEmitOnError를 켜면 오류가 있는 출력물을 막고, noEmit은 산출물 없이 검사합니다.

RUNTIME / TOOLING

입력 검증과 type stripping을 구분

타입 표기가 제거되어도 외부 입력의 정확성이 자동으로 보장되지 않습니다. Node의 type stripping은 제한된 문법의 타입 표기 제거 기능이며 tsc의 타입 검사나 tsconfig 변환을 대신하지 않습니다.

일반 JavaScript는 소스가 런타임으로 전달된다. TypeScript 경로는 TS소스, 정적 검사와 emit 정책, JavaScript 산출물, 런타임으로 이어진다. 검사 없이 타입 표기만 제거하는 실행 도구는 정적 검사와 별개이다.

greet.ts
function greet(name: string): string {
  return "안녕하세요, " + name;
}

console.log(greet("민지"));
greet.js
"use strict";
function greet(name) {
    return "안녕하세요, " + name;
}
console.log(greet("민지"));

tsc가 TypeScript 6.0의 기본 설정으로 내보낸 greet.js에서는 매개변수와 반환값의 : string이 사라졌고, 나머지 코드는 그대로입니다. 맨 위의 "use strict";는 이 파일을 엄격 모드(strict mode)로 실행하라는 지시문으로, 6.0에서 기본으로 켜져 있는 alwaysStrict 설정이 붙입니다. 들여쓰기가 네 칸으로 바뀌고 빈 줄이 빠지는 것처럼 모양은 조금 달라지지만, 실행하면 두 파일 모두 안녕하세요, 민지를 출력합니다.

tsc는 기본 설정에서 타입 오류를 보고하면서도 자바스크립트 파일을 내보냅니다. 오류가 있을 때 출력을 막으려면 noEmitOnError를, 파일은 만들지 않고 검사만 하려면 noEmit을 켭니다. 두 설정 모두 이미 만들어 둔 출력 파일을 지우지는 않으므로, 오류를 고친 뒤에는 출력을 다시 만들어야 합니다. 설정의 뜻은 공식 noEmitOnError 설명에 있습니다.

Node.js는 22.6부터 .ts 파일의 타입 표기를 공백으로 바꾸고 바로 실행하는 타입 제거(type stripping) 기능을 넣었고, 23.6부터는 이 기능이 기본으로 켜져 있습니다. 이 기능은 타입을 검사하지 않고 tsconfig.json도 읽지 않으며, enum처럼 새 자바스크립트 코드를 만들어야 하는 문법은 --experimental-transform-types 플래그 없이는 오류로 처리합니다. 그래서 node로 .ts 파일을 바로 실행하더라도 타입 검사는 tsc로 따로 해야 하고, 자세한 조건은 Node.js의 TypeScript 문서에 있습니다.

더 깊이: 검사와 출력이 따로 움직이는 이유

tsc가 타입 오류를 보고하면서도 파일을 내보내는 것은 의도한 동작입니다. 공식 Handbook은 그 이유로 자바스크립트 코드를 타입스크립트로 옮기는 상황을 듭니다. 이미 잘 실행되던 코드에 타입 검사의 오류가 생겼다고 실행까지 막을 까닭은 없고, 많은 경우 코드를 쓴 사람이 타입스크립트보다 사정을 더 잘 안다는 것입니다. 그래서 출력을 막는 쪽을 기본값이 아니라 noEmitOnError라는 선택으로 두었습니다.

이렇게 나눌 수 있는 것은 출력이 타입 검사의 결과에 기대지 않기 때문입니다. 타입스크립트의 설계 목표는 타입 검사의 결과에 따라 다른 코드를 내보내지 않는 것을 원칙으로 삼으므로, 대부분의 파일은 다른 파일의 타입을 몰라도 표기만 지우면 출력이 완성됩니다. TSConfig 레퍼런스의 isolatedModules 설명도 Babel 같은 변환 도구가 파일을 하나씩만 보고 변환한다는 점을 전제로 합니다. 다만 const enum처럼 다른 파일의 정보가 있어야 바르게 바꿀 수 있는 기능이 있어서, isolatedModules는 파일 하나만 보고 변환하면 틀릴 수 있는 코드를 진단해 줍니다.

Node.js가 타입 표기를 지울 때 빈 문자열이 아니라 공백으로 바꾸는 데도 이유가 있습니다. 표기가 있던 자리를 공백으로 채우면 나머지 코드가 원래 파일과 같은 줄에 남아서, 스택 트레이스의 줄 번호를 맞추는 소스맵(source map)이 필요 없습니다. Node.js 문서는 코드를 새로 만들어야 하는 문법을 기본으로 지원하지 않는 것도 이 성질을 지키려는 선택이라고 설명하며, --experimental-transform-types를 켜서 그런 문법을 변환할 때는 소스맵이 기본으로 켜집니다. 타입 제거 기능은 Node.js 24.3.0에서 실험 기능 경고가 사라졌고 24.12.0에서 안정(stable) 기능이 되었습니다.

개발 경험과 도입 비용

두 언어로 같은 프로그램을 만들 때 달라지는 점을 정리하면 다음과 같습니다.

비교 항목자바스크립트타입스크립트
타입 오류를 알게 되는 시점그 코드가 실행될 때편집기에서 코드를 쓸 때와 tsc로 검사할 때
편집기의 자동 완성매개변수처럼 추론할 단서가 없는 곳은 타입을 알 수 없다선언된 타입으로 속성과 매개변수를 보여 준다
이름을 바꾸는 리팩터링같은 이름의 다른 변수와 구분하기 어렵다같은 선언을 쓰는 곳만 찾아 함께 바꾼다
실행되는 코드작성한 코드타입을 지운 자바스크립트
추가 비용없다타입 설계와 설정, 빌드 단계, 팀의 학습

마지막 줄의 비용은 실제로 존재합니다. 타입을 설계하고 설정을 맞추는 데 시간이 들고, 팀이 타입 시스템에 익숙해질 때까지 학습의 부담이 생깁니다. 그래서 이미 자바스크립트로 만든 프로젝트라면 한 번에 모두 바꾸기보다 조금씩 들이는 편이 안전합니다.

기존 실행과 테스트 기준선이 없으면 먼저 기준선을 마련한다. 기준선이 준비되면 외부 입력의 unknown과 검증, 공용 API 타입, strict 검사 유지 순서로 전환한다. 이 절은 실제 마이그레이션 명령이나 완료 테스트를 제공하지 않는 전략 설명이다.

TYPE / RUNTIME / CONTRACT

기존 실행 기준을 유지하며 경계부터 타입을 강화한다

이 흐름은 기존 JavaScript 프로젝트의 점진적 전환 전략입니다. 기준선이 없으면 먼저 실행과 테스트를 고정하고, 외부 입력과 공용 계약을 작은 범위부터 옮긴 뒤 검사 강도를 유지합니다.

작은 범위의 타입 전환과 결과 대조기준선→외부입력→공용API→검사강도→변경검증→다음작은범위의6노드5간선이다. 기준선이없으면 먼저준비한다. 실제마이그레이션실행이나strict도입을반드시마지막으로미룬다는규칙이아닌전략설명이다.
1 · 기준선실행 · 테스트 확인
연결 관계1 · 기준선 · 실행 · 테스트 확인 → 2 · 외부 입력 · unknown · 값 검증
2 · 외부 입력unknown · 값 검증
연결 관계2 · 외부 입력 · unknown · 값 검증 → 3 · 공용 API · 타입 계약 표현
3 · 공용 API타입 계약 표현
연결 관계3 · 공용 API · 타입 계약 표현 → 4 · 검사 강도 · strict 적용 범위
4 · 검사 강도strict 적용 범위
연결 관계4 · 검사 강도 · strict 적용 범위 → 변경 검증 · 기존 결과와 비교
변경 검증기존 결과와 비교
연결 관계변경 검증 · 기존 결과와 비교 → 다음 작은 범위 · 전환을 반복
다음 작은 범위전환을 반복
작은 범위의 타입 전환과 결과 대조 · 모바일기준선→외부입력→공용API→검사강도→변경검증→다음작은범위의6노드5간선이다. 기준선이없으면 먼저준비한다. 실제마이그레이션실행이나strict도입을반드시마지막으로미룬다는규칙이아닌전략설명이다. 모바일에서도 노드와 연결 방향·독립 경로가 같다.
1 · 기준선실행 · 테스트 확인
연결 관계1 · 기준선 · 실행 · 테스트 확인 → 2 · 외부 입력 · unknown · 값 검증
2 · 외부 입력unknown · 값 검증
연결 관계2 · 외부 입력 · unknown · 값 검증 → 3 · 공용 API · 타입 계약 표현
3 · 공용 API타입 계약 표현
연결 관계3 · 공용 API · 타입 계약 표현 → 4 · 검사 강도 · strict 적용 범위
4 · 검사 강도strict 적용 범위
연결 관계4 · 검사 강도 · strict 적용 범위 → 변경 검증 · 기존 결과와 비교
변경 검증기존 결과와 비교
연결 관계변경 검증 · 기존 결과와 비교 → 다음 작은 범위 · 전환을 반복
다음 작은 범위전환을 반복

PRECONDITION

기준선이 없다면 먼저 고정한다

현재 빌드와 실행 결과, 테스트가 비교 기준입니다. 경계나 테스트가 준비되지 않았다면 이 준비부터 하고 다음 단계로 진행합니다.

INPUT / CONTRACT

모르는 외부 값부터 감싼다

입력 경계에는 unknown과 런타임 검증을 두고, 공용 함수와 데이터 모델에는 타입 계약을 표현합니다. 단언으로 외부 값의 정확성을 보장하지 않습니다.

STRATEGY / CONFIG

검사 강도는 전환 범위와 함께 정한다

any를 줄이고 strict 옵션을 적용할 범위를 정합니다. 이 순서는 도입 전략의 예이며 모든 프로젝트가 마지막까지 strict를 미뤄야 한다는 규칙은 아닙니다.

LESSON SCOPE

작은 범위의 결과를 비교하며 반복한다

본문은 도입 비용과 협업상 장점을 설명합니다. 실제 저장소 전환이나 CI 실행 예제는 제공하지 않습니다. 적용할 때는 옮긴 범위의 진단과 기존 실행 결과를 함께 확인합니다.

기존 실행과 테스트 기준선이 없으면 먼저 기준선을 마련한다. 기준선이 준비되면 외부 입력의 unknown과 검증, 공용 API 타입, strict 검사 유지 순서로 전환한다. 이 절은 실제 마이그레이션 명령이나 완료 테스트를 제공하지 않는 전략 설명이다.

그림의 순서처럼, 먼저 지금의 동작을 확인할 실행 방법과 테스트를 마련합니다. 그다음 외부에서 들어오는 입력을 unknown으로 받아 검증하고, 여러 모듈이 함께 쓰는 공용 API에 타입을 붙인 뒤, 엄격한 검사를 켠 상태를 유지합니다. 파일을 아직 .ts로 바꾸기 어렵다면, 자바스크립트 파일에 JSDoc 주석으로 타입을 적고 검사를 받는 방법도 있습니다.

sum.js
// @ts-check

/**
 * @param {number} left
 * @param {number} right
 */
function sumNumbers(left, right) {
  return left + right;
}

console.log(sumNumbers(10, "20"));
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

맨 위의 // @ts-check는 이 자바스크립트 파일을 JSDoc의 타입으로 검사하라는 표시이고, VS Code 같은 편집기는 이 줄만으로 진단을 보여 줍니다. tsc로 검사하려면 설정에서 자바스크립트 파일을 포함하는 allowJs를 켜고, 모든 자바스크립트 파일을 검사하려면 checkJs도 켭니다. 이 방법은 공식 JS 프로젝트 안내에 정리되어 있습니다.

정리

  • 자바스크립트는 변수에 들어 있는 값이 실행 중에 타입을 정하고, 타입스크립트는 선언에서 정한 타입으로 모든 대입을 실행 전에 검사합니다.
  • 타입 표기가 없어도 타입스크립트는 초기값에서 타입을 추론하고, 맞지 않는 대입을 TS2322로 진단합니다.
  • tsc는 타입을 검사하고 타입 표기를 지운 자바스크립트를 내보내며, 기본 설정에서는 타입 오류가 있어도 파일을 내보냅니다.
  • Node.js의 타입 제거 실행은 타입을 검사하지 않고 tsconfig.json도 읽지 않으므로, 검사는 tsc로 따로 합니다.
  • 기존 자바스크립트 프로젝트는 테스트와 외부 입력, 공용 API 순서로 타입을 들이고, // @ts-check와 JSDoc으로 파일을 바꾸기 전부터 검사를 받을 수 있습니다.

연습 문제

1. 검사하지 않고 실행하기

다음 count.ts를 tsc로 검사하면 어떤 진단이 나오나요? 같은 파일을 Node.js 24에서 node count.ts로 바로 실행하면 무엇이 출력되나요?

count.ts
const count: number = "3";
console.log(count + 1);
해설 보기

tsc는 문자열을 number 변수에 넣으므로 error TS2322: Type 'string' is not assignable to type 'number'.를 보고합니다. Node.js의 타입 제거는 : number를 공백으로 바꿀 뿐 검사하지 않으므로, const count = "3"; console.log(count + 1);를 실행하는 것과 같습니다. 문자열에 1을 더하면 이어 붙이므로 출력은 31입니다.

2. 오류가 있는데 생긴 파일

타입 오류가 있는 상태로 tsc를 실행했는데 .js 파일이 만들어졌습니다. 왜 그런가요? 오류가 있을 때 출력을 막도록 설정을 바꾼 뒤에도, 폴더에 남아 있는 예전 .js 파일은 어떻게 되나요?

해설 보기

noEmitOnError의 기본값이 false라서, tsc는 오류를 보고하면서도 파일을 내보냅니다. noEmitOnError를 켜면 오류가 있을 때 새 파일을 만들지 않지만, 이전에 만들어 둔 파일은 지우지 않으므로 그대로 남습니다. 그 파일을 실행해 나온 결과는 고친 코드의 결과가 아니므로, 오류를 고치고 다시 출력한 파일을 실행해야 합니다.

3. 진단이 있는 자바스크립트 파일

편집기가 위의 sum.js에 TS2345를 표시하는데도 node sum.js로 실행했습니다. 무엇이 출력되나요?

해설 보기

1020이 출력됩니다. // @ts-check와 JSDoc은 주석이어서 Node.js는 이 파일을 평범한 자바스크립트로 실행하고, 10 + "20"은 문자열을 이어 붙입니다. 진단은 실행을 막지 않으므로, 검사를 통과한 뒤에 실행하는 순서를 지켜야 합니다.

다음 글 개발 환경 설정에서는 Node.js와 타입스크립트 컴파일러, 편집기를 준비하고 tsconfig.json이 있는 첫 프로젝트를 만듭니다.