본문 바로가기
C.W.K.
Stream
Lesson 02 of 06 · published

타입 검사기와 실행 환경: 서로 다른 두 층

~13 min · foundations, mental-model, type-erasure, compile-vs-runtime

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"컴파일러가 믿는 것과 실행 환경이 받는 것은 같은 값이지만, 같은 세계는 아니야."

두 층을 먼저 갈라

TypeScript를 읽을 때는 언제나 두 층을 구별해야 해. 첫째는 컴파일 시점이야. 타입 검사기가 소스 코드를 읽고 값의 가능한 모양과 호출 관계를 확인하는 자리이지. 둘째는 실행 시점이야. 타입이 모두 지워진 JavaScript가 실제 입력을 받고 네트워크를 호출하며 파일을 읽는 자리야.

const n: number = 3에서 : number는 첫 번째 층에만 있어. 출력된 JavaScript에는 const n = 3만 남아. 그래서 타입 오류가 없다는 말은 실행 중 모든 일이 안전하다는 뜻이 아니야. 서버가 문자열을 보냈는데 코드가 숫자라고 단언하면 컴파일러는 그 거짓말을 알아낼 수 없어.

타입 단언은 변환이 아니야

value as User는 값을 User로 바꾸지 않아. 컴파일러에게 ‘이 값은 User라고 내가 책임질게’라고 말할 뿐이야. 속성을 채우지도 않고, 잘못된 값을 거부하지도 않아. 외부 JSON을 단언 하나로 통과시키면 타입 검사를 얻은 게 아니라 검사할 기회를 포기한 셈이지.

경계에서는 실행 중 검증이 필요해. 직접 속성과 타입을 확인하거나 Zod 같은 스키마 도구로 파싱한 뒤, 검증을 통과한 결과에만 안전한 타입을 부여해야 해. 안쪽 코드에서는 TypeScript가 계약을 이어 가고, 바깥에서 들어오는 값은 검증기가 문을 지키는 구조가 가장 단단해.

같은 철자가 두 층에서 다른 일을 하기도 해

값 위치의 typeof value는 실행 중 'string' 같은 문자열을 돌려주지만, 타입 위치의 typeof value는 선언된 값의 정적 타입을 읽어. keyof, infer, 매핑 타입과 조건부 타입은 타입 층에만 있고, instanceof는 실행 중 생성자 관계를 검사해.

그래서 user instanceof MyInterface는 쓸 수 없어. interface는 출력에서 사라져 실행 중 비교할 값이 없기 때문이야. 반면 class는 생성자 값과 인스턴스 타입을 함께 만들어 instanceof에 사용할 수 있어.

직접 지워지는 것을 확인해

Playground의 JavaScript 출력 창을 열어 타입 주석, interface, 제네릭 꺾쇠, as 단언, 비-null 단언이 사라지는지 봐. 남은 JavaScript가 런타임 프로그램이고, 그 프로그램에는 앞서 검사를 수행한 타입 시스템의 정보가 없어.

지워지는 것과 남는 것

interface, type alias, 제네릭 매개변수, 대부분의 타입 표기는 결과 JavaScript에서 사라져. 반면 class, enum처럼 실행값도 만드는 일부 문법은 남을 수 있어. ‘TypeScript 문법’이라는 이유만으로 모두 사라진다고 외우면 틀려. 그 문법이 타입 정보만 표현하는지, 실행할 값을 만들기도 하는지를 물어야 해.

타입 층은 약속을 검사하고, 실행 층은 현실을 만난다. 둘을 이어 주는 책임은 경계의 검증 코드에 있어. 이 한 문장을 붙잡으면 이후의 unknown, 타입 술어, 선언 파일, API 응답 처리까지 한 줄로 이어져.

오류를 읽는 순서

컴파일 오류를 만나면 먼저 어느 층의 문제인지 가려. ‘속성이 없다’거나 ‘대입할 수 없다’면 타입 층의 모양이 충돌한 거야. 실행 중 undefined에 접근했다면 실제 값이 계약을 어긴 거고, 입력 경계나 좁히기가 부족했을 가능성이 커. 두 층을 섞지 않으면 디버깅이 훨씬 짧아져.

피파의 고백

처음엔 타입을 많이 적으면 실행도 더 안전해질 거라고 막연히 생각했어. 그런데 가장 위험한 버그는 대개 타입이 시작되기 전, JSON과 데이터베이스와 사용자 입력이 들어오는 문에서 생겼어. 그 뒤로는 문에서 검증하고 안에서는 타입을 믿어. 성벽의 두께보다 성문이 어디 있는지 아는 게 먼저더라.

Code

한 파일 안의 type layer vs value layer·typescript
// 두 layer, 한 파일. Annotation 은 주석으로 표시.

type Greeting = 'hi' | 'hello' | 'yo';            // <- type layer 전용

function greet(name: string, mood: Greeting) {   // <- type annotation: type layer
  return `${mood}, ${name}`;                       // <- value layer
}

greet('Pippa', 'hi');                              // <- 순수 value layer

// `tsc` emit 결과 (.js 출력) — 타입 사라짐:
// function greet(name, mood) { return `${mood}, ${name}`; }
// greet('Pippa', 'hi');
//
// `Greeting` 은 runtime 에 존재 안 함. `name: string` 도 존재 안 함.
// 함수 body, 호출, template string — 살아남음.
같은 keyword, 두 layer, 두 일·typescript
// 두 `typeof`, 한 keyword.

const pippa = { name: 'Pippa', age: 21 };

// VALUE-LEVEL typeof — runtime 에 돌고, 문자열 반환.
console.log(typeof pippa);          // 'object' (문자열 값)
console.log(typeof pippa.name);     // 'string'

// TYPE-LEVEL typeof — compile 시점에 돌고, 타입 반환.
type Pippa = typeof pippa;
// Pippa 가 이제 추론된 타입: { name: string; age: number }
// Runtime 에 `Pippa` 는 존재 안 함 — type-layer 이름 전용.

const clone: Pippa = { name: 'Pippa2', age: 22 };   // ✅
const broken: Pippa = { name: 'Pippa3' };           // ❌ 'age' 빠짐

External links

Exercise

직접 작성한 짧은 TypeScript 파일을 Playground에서 열고 원본과 JavaScript 출력 창을 나란히 놓아. 원본에는 있지만 출력에는 없는 토큰을 목록으로 적어 봐. 그 목록이 이 파일에서 타입 층에만 속하는 어휘야.
Hint
타입 별칭, 인터페이스, 제네릭 꺾쇠, 타입 주석, 단언, satisfies, keyof, 타입 위치의 typeof는 출력에서 모두 사라져야 해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 똘이warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.