LogoSEO Jing
  • All Posts
  • SEO Jing
  • okayJing
  • KD Team
  • CLAB Coreteam
  • Study

Contact Me

© 2026 SEOJing. All rights reserved.

이펙티브 타입스크립트 2판 Day 13: 타입은 런타임과 테스트를 대신하지 않는다

2026년 7월 8일·8분 읽기
범위: Effective TypeScript 2판 Item 74–78
오늘의 질문: “타입스크립트가 있으니 런타임 검증과 테스트는 조금 덜 봐도 될까?”

이 글의 원천 앵커

이번 범위는 타입스크립트의 바깥을 봅니다. 타입을 잘 설계해도 브라우저가 실행하는 것은 타입이 지워진 자바스크립트입니다. DOM은 환경마다 다르게 구현되고, 테스트는 타입이 놓치는 런타임 분기를 확인합니다. 타입이 너무 복잡하면 컴파일러도 느려집니다.

이번 글에서 잃으면 안 되는 포인트는 이겁니다.

타입은 런타임에 사라지므로 외부 값은 다시 확인해야 한다. 런타임 타입 정보를 얻으려면 값, 태그, 스키마, 판별자 같은 단서가 필요하다. DOM 타입 선언은 실제 브라우저/테스트 환경의 API 존재를 보장하지 않는다. 단위 테스트와 타입 검사는 서로 대체 관계가 아니라 서로 다른 실패를 잡는다. 타입 수준 프로그래밍은 표현력이 있지만, 컴파일러 성능과 팀 디버깅 비용을 만든다.

런타임과 테스트 경계

먼저 보는 코드

ts
type User = {
  id: string;
  name: string;
};

async function loadUser(): Promise<User> {
  const response = await fetch("/api/me");
  const data = await response.json();
  return data as User;
}

이 코드는 타입스크립트 입장에서는 깔끔합니다. loadUser()는 Promise를 반환합니다. 하지만 런타임에서는 response.json()이 어떤 값을 줄지 모릅니다. 서버가 name: null을 보내도, 인증 실패 HTML이 섞여도, 오래된 API가 user_name을 보내도 as User는 아무것도 확인하지 않습니다.

이 범위의 핵심은 “타입을 믿지 말자”가 아닙니다. 타입이 책임지는 구역과 런타임 코드가 책임지는 구역을 나누자는 말입니다.

런타임 타입은 값에서 다시 만들어야 한다

unknown을 내부 타입으로 바꾸려면 검증 코드가 필요합니다.

ts
type User = {
  id: string;
  name: string;
};

function isUser(value: unknown): value is User {
  if (!value || typeof value !== "object") return false;
  const record = value as Record<string, unknown>;
  return typeof record.id === "string" && typeof record.name === "string";
}

async function loadUser(): Promise<User | null> {
  const response = await fetch("/api/me");
  const data: unknown = await response.json();
  return isUser(data) ? data : null;
}

여기서 isUser는 타입과 런타임을 이어주는 다리입니다. value is User라는 반환 타입만 중요한 게 아니라, 함수 본문이 실제 값을 확인한다는 점이 중요합니다.

리뷰할 때는 다음을 봅니다.

unknown이 경계에서 바로 좁혀지는가? as SomeType가 검증 없이 외부 데이터를 믿게 만들지 않는가? 실패했을 때 null, Result, throw, error state 중 무엇으로 표현하는가? 검증 코드와 타입 정의가 서로 어긋날 가능성은 없는가?

작은 앱에서는 직접 type guard를 써도 됩니다. 데이터가 복잡하거나 API가 많아지면 Zod 같은 스키마 검증 도구를 쓰는 편이 더 일관적일 수 있습니다. 중요한 건 도구 이름이 아니라, 런타임 값이 타입으로 들어오는 관문이 있다는 사실입니다.

타입만으로는 DOM 환경을 보장하지 못한다

DOM 코드는 타입 선언이 있어도 실제 환경에서 깨질 수 있습니다.

ts
const dialog = document.querySelector("dialog") as HTMLDialogElement;
dialog.showModal();

브라우저에서는 맞을 수 있습니다. 하지만 테스트 환경이 jsdom이면 showModal이 구현되어 있지 않을 수 있습니다. 오래된 브라우저나 WebView도 마찬가지입니다. 타입 선언은 “이 API가 표준 DOM 모델에 있다”는 정보를 줄 뿐, 지금 실행 환경에 반드시 구현되어 있다는 보장은 아닙니다.

더 안전한 코드는 환경 차이를 드러냅니다.

ts
const dialog = document.querySelector("dialog");

if (!(dialog instanceof HTMLDialogElement)) {
  throw new Error("dialog element is missing");
}

if (typeof dialog.showModal !== "function") {
  // 테스트 환경이나 구형 환경에서는 fallback 처리
  dialog.setAttribute("open", "");
} else {
  dialog.showModal();
}

프론트엔드 코드 리뷰에서는 DOM 타입을 볼 때 다음 질문이 필요합니다.

selector 결과가 null일 수 있는가? as HTMLElement가 실제 element 종류를 확인하지 않고 있지 않은가? 브라우저, jsdom, Storybook, SSR 환경에서 같은 API를 기대하는가? 접근성/기본 동작을 타입만 보고 생략하지 않았는가?

Day 12의 browser event 글과도 이어집니다. target은 타입상 EventTarget이고, 실제로는 텍스트 노드나 내부 span일 수 있습니다. 타입을 좁히는 코드가 이벤트 흐름을 제대로 반영해야 합니다.

테스트는 타입이 모르는 실패를 본다

타입은 많은 실수를 컴파일 전에 막습니다. 하지만 테스트가 봐야 하는 영역은 여전히 남습니다.

ts
type Price = {
  amount: number;
  currency: "KRW" | "USD";
};

function formatPrice(price: Price) {
  if (price.currency === "KRW") return `${price.amount.toLocaleString()}원`;
  return `$${price.amount.toFixed(2)}`;
}

타입은 currency가 두 값 중 하나라는 사실을 보장합니다. 하지만 다음은 타입만으로 충분하지 않습니다.

KRW가 소수점 없이 표시되는가? USD가 소수점 두 자리로 표시되는가? NaN, Infinity, 음수, 0은 어떻게 보이는가? locale이 바뀌면 결과가 달라지는가?

테스트는 타입이 아니라 동작을 확인합니다.

ts
it("formats KRW without decimal places", () => {
  expect(formatPrice({ amount: 12000, currency: "KRW" })).toBe("12,000원");
});

it("formats USD with two decimals", () => {
  expect(formatPrice({ amount: 12, currency: "USD" })).toBe("$12.00");
});

반대로 테스트가 타입을 대체하지도 않습니다. 타입이 없으면 테스트가 커버하지 않은 호출 조합이 계속 생깁니다. 좋은 방향은 둘을 나누는 겁니다.

도구잘 잡는 것못 잡는 것
TypeScript잘못된 호출 모양, 빠진 필드, 불가능한 union branch실제 렌더링 결과, 외부 API 응답, 브라우저 구현 차이
Unit test구체적인 입력/출력, 실패 branch, 환경 fallback모든 호출 조합, 타입 설계 의도
Runtime validation외부 값의 실제 shapeUI 동작, 성능, 접근성

코드 리뷰에서는 “타입이 있으니 테스트 생략”과 “테스트가 있으니 타입 대충”을 둘 다 경계해야 합니다.

컴파일러 성능도 설계 비용이다

타입스크립트 타입은 꽤 강력합니다. conditional type, mapped type, template literal type, recursive type을 조합하면 많은 규칙을 타입으로 표현할 수 있습니다.

ts
type DotPath<T> = T extends object
  ? {
      [K in keyof T & string]: T[K] extends object
        ? K | `${K}.${DotPath<T[K]>}`
        : K;
    }[keyof T & string]
  : never;

이런 타입은 폼 경로, i18n key, route key를 표현할 때 유용할 수 있습니다. 하지만 객체가 커지고 union이 섞이면 컴파일러가 계산해야 할 조합이 급격히 늘어납니다. 에디터 자동완성이 느려지고, tsc --noEmit 시간이 늘고, 팀원이 타입 에러를 읽기 어려워질 수 있습니다.

타입 수준 설계도 리뷰 대상입니다.

이 복잡한 타입이 실제 버그를 줄이는가? 런타임 코드나 codegen으로 푸는 편이 더 단순하지 않은가? public API에 노출되어 사용자 에러 메시지를 어렵게 만들지 않는가? 큰 union/recursive type이 에디터 성능을 망치지 않는가? 타입 테스트가 있어 변경 시 깨지는 지점을 알 수 있는가?

Day 10에서 타입 테스트를 다룬 이유가 여기서 다시 중요해집니다. 복잡한 타입을 쓰려면 “잘 동작하겠지”가 아니라 타입 테스트와 성능 감각이 같이 있어야 합니다.

프론트엔드 코드 리뷰 체크리스트

변화확인할 질문
as SomeType 추가외부 값을 검증하지 않고 믿게 만드는가?
type guard 추가실제 런타임 값의 필드를 확인하는가, 이름만 guard인가?
DOM API 사용브라우저/jsdom/SSR 환경 차이를 고려했는가?
테스트 삭제타입이 정말 그 동작을 보장하는가?
테스트 추가타입으로 막을 수 있는 호출 조합까지 테스트로 떠안고 있지 않은가?
복잡한 타입 추가버그 감소 효과가 컴파일/디버깅 비용보다 큰가?

이번 범위의 결론은 분명합니다. 타입스크립트는 리뷰 범위를 줄여주지만, 런타임을 없애지는 않습니다. 타입이 사라진 뒤에도 데이터, DOM, 테스트, 컴파일러 성능이라는 경계는 남습니다. 좋은 TS 코드는 이 경계를 숨기지 않습니다.

Day 13 타입 리뷰 퀴즈

Quiz1 / 4
Q.외부 API 응답에 `as User`를 바로 붙이는 코드의 가장 큰 문제는 무엇일까요?

Post Q&A

오케이징에게 물어보기

이펙티브 타입스크립트 2판 Day 13: 타입은 런타임과 테스트를 대신하지 않는다 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!

0/500

포스트 목록

/study/effective-typescript
파일 14개, 폴더 0개
이펙티브 타입스크립트 2판 Day 1: 타입스크립트를 믿기 전에 알아야 할 경계이펙티브 타입스크립트 2판 Day 2: 타입을 값의 집합으로 보기이펙티브 타입스크립트 2판 Day 3: 타입 반복을 줄이는 리뷰 감각이펙티브 타입스크립트 2판 Day 4: 추론을 살리는 값 생성 패턴이펙티브 타입스크립트 2판 Day 5: 좁혀진 타입은 언제 다시 넓어지는가이펙티브 타입스크립트 2판 Day 6: 유효한 상태만 표현하는 타입 설계이펙티브 타입스크립트 2판 Day 7: 문자열보다 도메인 의미를 좁히기이펙티브 타입스크립트 2판 Day 8: any를 밖에 세우고 unknown으로 검증하기이펙티브 타입스크립트 2판 Day 9: 제네릭은 관계를 보존할 때만 강하다이펙티브 타입스크립트 2판 Day 10: 타입도 테스트하고 빠지는 분기는 막는다이펙티브 타입스크립트 2판 Day 11: 객체 타입은 열려 있고, 계약은 닫아야 한다이펙티브 타입스크립트 2판 Day 12: 타입은 공개 API의 사용법이다이펙티브 타입스크립트 2판 Day 13: 타입은 런타임과 테스트를 대신하지 않는다이펙티브 타입스크립트 2판 Day 14: JS에서 TS로 옮길 때 먼저 좁혀야 할 경계

같은 섹션의 대표 이미지

14 posts · latest first
런타임 값 검증, DOM 환경, 테스트, 컴파일러 성능으로 이어지는 타입스크립트 경계 다이어그램
Study26. 07. 09.

이펙티브 타입스크립트 2판 Day 13: 타입은 런타임과.

Item 74–78 범위를 바탕으로 런타임 타입 재구성, DOM과 환경 모델, 단위 테스트 관계, 컴파일러 성능을 코드 리뷰 관점에서 정리합니다.

26. 07. 09.SEOJing
JavaScript 현대화에서 @ts-check, allowJs, 모듈별 전환, noImplicitAny로 이어지는 마이그레이션 단계 다이어그램
Study26. 07. 09.

이펙티브 타입스크립트 2판 Day 14: JS에서 TS로 옮길.

Item 79–83 범위를 바탕으로 JS 현대화, @ts-check와 JSDoc, allowJs, 모듈별 마이그레이션, noImplicitAny를 코드 리뷰 관점에서 정리합니다.

26. 07. 09.SEOJing
내부 구현 타입에서 문서화된 public API 타입으로 넘어가는 경계 다이어그램
Study26. 07. 08.

이펙티브 타입스크립트 2판 Day 12: 타입은 공개 API의.

Item 67–73 범위를 바탕으로 public API 타입, TSDoc, this callback, module augmentation, TypeScript 기능 선택, source map을 코드 리뷰 관점에서 정리합니다.

26. 07. 08.SEOJing
Object iteration, Record, tuple/rest, XOR, brand, @types 버전 관리를 연결한 타입 리뷰 다이어그램
Study26. 07. 07.

이펙티브 타입스크립트 2판 Day 11: 객체 타입은 열려.

Item 60–66 범위를 바탕으로 Object iteration, Record, tuple/rest, XOR, brand, @types 버전 경계를 코드 리뷰에서 다루는 방법을 정리합니다.

26. 07. 07.SEOJing
타입 계약, 타입 테스트, never exhaustiveness, 코드 생성 경계를 보여주는 다이어그램
Study26. 07. 06.

이펙티브 타입스크립트 2판 Day 10: 타입도 테스트하고.

Item 55–59 범위를 바탕으로 type test, 타입 표시, 꼬리 재귀 한계, 코드 생성, never exhaustiveness를 코드 리뷰에서 다루는 방법을 정리합니다.

26. 07. 06.SEOJing
외부 경계, 제네릭, 조건부 타입, 템플릿 리터럴 타입의 역할과 과한 타입 계산의 위험을 보여주는 다이어그램
Study26. 07. 05.

이펙티브 타입스크립트 2판 Day 9: 제네릭은 관계를 보존할.

Item 49–54 범위를 바탕으로 type coverage, 제네릭, 조건부 타입, 템플릿 리터럴 타입을 코드 리뷰에서 어떻게 판단할지 정리합니다.

26. 07. 05.SEOJing
외부 JSON을 unknown으로 받고 검증 뒤 도메인 타입으로 넘기는 경계와 any가 내부로 번지는 위험을 보여주는 다이어그램
Study26. 07. 04.

이펙티브 타입스크립트 2판 Day 8: any를 밖에 세우고.

Item 42–48 범위를 바탕으로 any, unknown, 타입 단언, monkey patching, soundness 함정을 외부 입력 경계와 코드 리뷰 관점에서 정리합니다.

26. 07. 04.SEOJing
넓은 문자열과 선택적 필드를 도메인 이름과 유효 상태 유니온으로 좁히는 다이어그램
Study26. 06. 29.

이펙티브 타입스크립트 2판 Day 7: 문자열보다 도메인.

Item 35–41 범위를 바탕으로 string 남용, optional 필드, 특수 값, 도메인 이름 설계를 코드 리뷰 관점에서 정리합니다.

26. 06. 29.SEOJing
API 응답을 유효한 상태 유니온으로 변환하는 흐름 다이어그램
Study26. 06. 28.

이펙티브 타입스크립트 2판 Day 6: 유효한 상태만 표현하는.

Item 28–34 범위를 바탕으로 추론 위치, 유효 상태 모델링, API 입출력, null 경계, union 설계를 프론트엔드 코드 리뷰 관점에서 확장 정리합니다.

26. 06. 28.SEOJing
타입스크립트 추론이 값에서 API 경계로 흐르는 과정을 보여주는 다이어그램
Study26. 06. 27.

이펙티브 타입스크립트 2판 Day 5: 좁혀진 타입은 언제.

Item 22–27 범위를 바탕으로 narrowing, alias, context inference, evolving type, async/type flow를 프론트엔드 코드 리뷰 관점에서 다시 정리합니다.

26. 06. 27.SEOJing
TypeScript에서 넓은 타입으로 먼저 담으면 key와 literal 정보가 사라지고 리뷰 체크가 약해지는 흐름 다이어그램
Study26. 06. 26.

이펙티브 타입스크립트 2판 Day 4: 추론을 살리는 값 생성.

Effective TypeScript 2판의 Item 16–21을 바탕으로 index signature, 타입 추론 기본, 변수/객체 생성 패턴을 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 26.SEOJing
반복된 타입 선언을 원본 타입에서 파생한 타입으로 바꿔 리뷰 체크를 강하게 만드는 흐름 다이어그램
Study26. 06. 25.

이펙티브 타입스크립트 2판 Day 3: 타입 반복을 줄이는 리뷰.

Effective TypeScript 2판의 Item 11–15를 바탕으로 excess property check, 함수식 타입, type vs interface, readonly, 타입 반복 제거를 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 25.SEOJing
TypeScript 타입을 값의 집합, type space, value space, assertion 경계로 정리한 다이어그램
Study26. 06. 24.

이펙티브 타입스크립트 2판 Day 2: 타입을 값의 집합으로.

Effective TypeScript 2판의 Item 6–10을 바탕으로 타입 시스템을 탐색하는 법, 타입을 값의 집합으로 보는 관점, type/value space, 타입 단언의 경계를 코드 리뷰 관점에서 정리합니다.

26. 06. 24.SEOJing
TypeScript를 JavaScript 위의 정적 타입 계층, 타입 제거, 구조적 타이핑, any의 구멍으로 정리한 다이어그램
Study26. 06. 23.

이펙티브 타입스크립트 2판 Day 1: 타입스크립트를 믿기.

Effective TypeScript 2판의 Item 1–5를 바탕으로 TypeScript와 JavaScript의 관계, tsconfig, 타입 제거, 구조적 타이핑, any의 위험을 프론트엔드 코드 리뷰 관점에서 정리합니다.

26. 06. 23.SEOJing