TypeScript는 프로덕션에 도달하기 전에 어처구니없는 버그들을 잡아냅니다. 오타가 난 속성, 누락된 인자, 그리고 잘못된 형태를 반환하는 분기 등에 대해 경고를 보냅니다. 수정하고 빌드가 통과되면 배포하면 됩니다. 하지만 정적 타입에는 명확한 한계가 있습니다. 컴파일이 끝나면 모든 어노테이션은 제거됩니다. 코드를 실행하는 JavaScript 엔진은 여러분의 인터페이스, 브랜디드 타입(branded types), 또는 정교하게 제약된 문자열 리터럴에 대해 들어본 적이 없습니다. 엔진은 오직 값과 언어의 실제 규칙만을 알 뿐입니다.
빌드가 통과되었다고 해서 런타임이 안전하다는 뜻은 아닙니다. 테스트가 통과(green)되었다고 해서 사용자가 안정적인 애플리케이션을 본다는 보장도 없습니다. 만약 여러분의 사고 모델이 TypeScript 경계에서 멈춘다면, 실제 충돌이 발생하는 바로 그 지점에서 눈을 가리고 비행하는 것과 같습니다.
컴파일 타임의 신기루
TypeScript의 타입 시스템 전체는 컴파일 중에 삭제됩니다. 어떤 프로젝트든 컴파일된 JavaScript 출력물을 열어보면 interface, type, 또는 제네릭 제약 조건(generic constraints)의 흔적을 전혀 찾을 수 없을 것입니다. 이것들은 설계 시점의 발판(scaffolding)일 뿐입니다. 브라우저나 Node.js 프로세스는 순수 JavaScript를 실행하며, 함수를 통해 흐르는 값들이 종이 위에 선언한 타입과 일치한다는 보장은 없습니다.
이 간극은 시스템의 경계에서 가장 중요하게 작용합니다. 네트워크 응답, 사용자 입력, 그리고 서드파티 라이브러리는 타입을 위반하는 값을 주입할 수 있습니다. strictEmail: string으로 선언한 변수라도 신뢰할 수 없는 API를 통해 잘못된 데이터가 들어오면 런타임에 숫자를 가질 수 있습니다. TypeScript는 코드를 프로덕션까지 따라가며 무언가를 강제할 수 없습니다. 런타임은 완전히 별개의 평면에서 작동하며, 이 두 평면을 혼동하면 정적 분석으로는 절대 잡아낼 수 없는 장애가 발생합니다.
숫자가 당신을 배신할 때
TypeScript는 number를 봅니다. JavaScript 엔진은 IEEE 754 배정밀도 부동 소수점(double-precision float)을 봅니다. 이 차이는 재앙이 닥치기 전까지는 무해해 보입니다.
JavaScript는 모든 숫자에 64비트를 할당하지만, 가수부(mantissa)에는 53비트만 저장합니다. 이로 인해 안전한 정수 상한선은 9,007,199,254,740,991이 됩니다. 이보다 큰 값은 가장 가까운 표현 가능한 값으로 반올림됩니다. 실제로, 서로 완전히 다른 두 식별자가 애플리케이션 내부에서 동일한 값으로 수렴될 수 있습니다.
Snowflake ID 및 기타 분산 64비트 정수 식별자는 이 한계를 빈번하게 초과합니다. 고액의 금액을 작은 통화 단위로 추적하는 금융 시스템 또한 이 한계에 부딪힐 수 있습니다. 위험은 종종 비즈니스 로직이 실행되기도 전에 나타납니다. JSON.parse는 페이로드의 숫자 리터럴을 JavaScript 숫자로 성급하게 변환하며, 도착과 동시에 정밀도를 조용히 잘라버립니다. 타입 정의에는 id: number라고 약속되어 있을지 모르지만, 런타임 값은 첫 번째 함수 호출이 이루어지기도 전에 이미 손상되어 있을 수 있습니다.
해결책은 간단하지만 스택 전반에 걸친 규율이 필요합니다. 네트워크 전송 중에는 큰 식별자를 문자열로 유지하십시오. JSON 스키마와 API 계약에서 이러한 필드를 숫자가 아닌 문자열로 정의하십시오. 안전 범위를 벗어난 값에 대해 산술 연산을 수행해야 한다면 BigInt를 사용하십시오. 하지만 주의할 점이 있습니다. BigInt는 표준 JavaScript 숫자와 암시적으로 혼합되지 않으며, JSON.stringify는 명시적으로 문자열로 다시 변환하지 않으면 오류를 발생시키며 BigInt를 직렬화할 수 없습니다. 기본적으로 ID를 불투명한 토큰(opaque tokens)으로 취급하십시오. 진정으로 계산이 필요한 격리된 계산 모듈 내부에서만 숫자 형태로 파싱하십시오.
