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

Contact Me

© 2026 SEOJing. All rights reserved.

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

2026년 6월 28일·10분 읽기

오늘의 범위

Day 2는 Effective TypeScript 2판의 Item 6–10을 묶어서 읽습니다. 세부 item 이름을 외우기보다 하나의 질문으로 정리하면 이렇습니다.

text
타입스크립트가 보는 타입 세계를 내가 직접 탐색하고 설명할 수 있는가?

Day 1에서 TypeScript가 JavaScript 위에 얹히는 정적 검사 계층이라는 경계를 봤습니다. 오늘은 그 검사기가 실제로 타입을 어떻게 생각하는지 쪽으로 들어갑니다.

오늘 잡을 포인트는 네 가지입니다.

에디터와 컴파일러를 타입 탐색 도구로 쓰기 타입을 “값의 집합”으로 이해하기 type space와 value space를 구분하기 타입 단언 as를 안전 경계가 아니라 우회 경계로 보기

---

먼저 지도를 그려보자

TypeScript 타입을 값의 집합, type space, value space, assertion 경계로 정리한 다이어그램

타입을 “변수 옆에 붙은 이름표”로만 보면 TypeScript의 오류 메시지가 답답해집니다. string, number, union, literal type, unknown, never가 서로 어떤 관계인지 감이 잘 안 옵니다.

더 나은 관점은 타입을 가능한 값의 집합으로 보는 것입니다.

ts
type A = string;
type B = "ready" | "error";
type C = never;
type D = unknown;

이 타입들을 집합처럼 보면 다음과 같습니다.

타입가능한 값의 범위
"ready"문자열 값 하나
"ready" 또는 "error"두 개의 문자열 값
string모든 문자열 값
unknownTypeScript가 표현할 수 있는 거의 모든 값
never가능한 값이 없음

이 관점 하나만 있어도 union, narrowing, assignability, exhaustiveness를 훨씬 덜 신비롭게 볼 수 있습니다.

---

1. 타입 시스템을 탐색하는 습관

TypeScript를 공부할 때 책이나 문서만 읽으면 금방 추상적이 됩니다. 실제 코드를 놓고 “지금 컴파일러가 이 값을 뭐라고 추론하는지”를 확인해야 합니다.

예를 들어 이런 코드를 봅니다.

ts
const status = "ready";
let phase = "ready";

두 값은 같은 문자열로 초기화됐지만 타입은 다르게 추론됩니다.

ts
// const status: "ready"
// let phase: string

const는 재할당되지 않으므로 literal type으로 좁게 잡을 수 있습니다. let은 나중에 다른 문자열이 들어올 수 있으므로 string으로 넓게 잡힙니다.

이런 차이는 에디터 hover, “Go to Definition”, “Quick Info”, tsc --noEmit 같은 도구로 바로 확인할 수 있습니다. 실무에서는 이 탐색 습관이 꽤 중요합니다. 타입 에러를 억지로 없애기 전에 먼저 물어야 합니다.

text
TypeScript는 지금 이 값을 어떤 타입으로 보고 있지?
내 의도보다 넓게 추론됐나, 좁게 추론됐나?

특히 객체와 배열에서는 이 차이가 자주 나옵니다.

ts
const route = { kind: "post", slug: "ts-day-2" };

// route.kind는 "post"가 아니라 string으로 넓어질 수 있다.

객체의 프로퍼티는 나중에 바뀔 수 있으므로 literal이 바로 보존되지 않는 경우가 있습니다. 이때 as const를 쓰면 좁아지지만, 무작정 붙이면 수정 가능성까지 막아버립니다. 먼저 추론 결과를 확인하고 의도를 결정하는 것이 순서입니다.

---

2. 타입은 값의 집합이다

타입을 집합으로 보면 extends나 할당 가능성도 더 자연스럽습니다.

ts
type Small = "ready" | "error";
type Big = string;

const a: Small = "ready";
const b: Big = a;

Small의 값은 모두 string 안에 들어갑니다. 그래서 Small 값을 string 변수에 넣는 것은 안전합니다.

반대는 아닙니다.

ts
const anyString: string = "pending";
const phase: Small = anyString;
// Type 'string' is not assignable to type 'Small'.

string에는 "ready", "error" 말고도 너무 많은 값이 들어올 수 있습니다. 그러니 좁은 집합에 바로 넣을 수 없습니다.

이 감각은 API 상태 모델링에서 중요합니다.

ts
type LoadState = "idle" | "loading" | "success" | "error";

이 타입은 “문자열 네 개 중 하나”입니다. 단순히 문자열에 이름을 붙인 게 아닙니다. 가능한 상태를 네 개로 제한한 것입니다. 그래서 리뷰에서는 이렇게 물을 수 있습니다.

text
이 상태 타입은 실제 UI 상태 집합을 충분히 표현하는가?
반대로 너무 넓게 string으로 열어둔 곳은 없는가?

---

3. `unknown`과 `never`를 집합으로 이해하기

unknown과 never는 처음 보면 낯설지만, 집합 관점에서는 단순합니다.

unknown은 가장 넓은 쪽입니다. 어떤 값이든 들어올 수 있지만, 바로 사용할 수는 없습니다.

ts
function renderValue(value: unknown) {
  value.toUpperCase();
  // Property 'toUpperCase' does not exist on type 'unknown'.
}

unknown은 불편하지만 정직합니다. 외부에서 들어온 값이 무엇인지 모르면 먼저 좁히라고 요구합니다.

ts
function renderValue(value: unknown) {
  if (typeof value === "string") {
    return value.toUpperCase();
  }

  return String(value);
}

반대로 never는 가능한 값이 없는 타입입니다. 보통 모든 경우를 처리했는지 확인할 때 쓸모가 있습니다.

ts
type Result =
  | { type: "success"; data: string }
  | { type: "error"; message: string };

function render(result: Result) {
  switch (result.type) {
    case "success":
      return result.data;
    case "error":
      return result.message;
    default: {
      const _exhaustive: never = result;
      return _exhaustive;
    }
  }
}

나중에 Result에 `이 추가되면 default의 never` 체크가 깨집니다. 이건 좋은 실패입니다. UI 상태가 늘어났는데 렌더링 분기가 따라오지 않았다는 신호니까요.

---

4. type space와 value space를 섞지 않기

TypeScript에는 타입으로만 존재하는 이름과 런타임 값으로 존재하는 이름이 있습니다. 둘은 문맥에 따라 다릅니다.

ts
interface User {
  name: string;
}

const User = {
  label: "사용자",
};

interface User는 type space에 있습니다. 런타임 JavaScript로 남지 않습니다. const User는 value space에 있습니다. 실제 실행 코드에 남습니다.

헷갈리는 대표 사례는 typeof입니다.

ts
const config = {
  mode: "dark",
  retry: 3,
};

type Config = typeof config;

여기서 typeof config는 런타임의 typeof 연산이 아니라 type space에서 값의 타입을 가져오는 문법입니다. 같은 단어라도 위치에 따라 의미가 달라집니다.

클래스는 더 헷갈립니다. 클래스 이름은 타입으로도 쓰이고 값으로도 쓰입니다.

ts
class Dialog {
  open() {}
}

let instance: Dialog;
const Constructor = Dialog;

instance: Dialog의 Dialog는 인스턴스 타입입니다. const Constructor = Dialog의 Dialog는 런타임 생성자 값입니다. 이 차이를 모르면 typeof Dialog, InstanceType, React 컴포넌트 props 타입을 읽을 때 자주 막힙니다.

리뷰에서는 import도 확인해야 합니다.

ts
import type { User } from "./types";
import { createUser } from "./factory";

타입으로만 쓰는 것은 import type으로 분리하면 번들에 남을 값 import를 줄이고, type/value 경계를 더 분명히 할 수 있습니다.

---

5. 타입 단언은 변환이 아니다

as는 TypeScript에게 “내가 더 잘 아니까 이렇게 봐줘”라고 말하는 문법입니다. 값을 바꾸지 않습니다.

ts
const input = document.querySelector("input") as HTMLInputElement;

input.value;

이 코드는 편하지만, 실제 DOM에 input이 없으면 런타임에서 깨질 수 있습니다. as HTMLInputElement는 null을 없애는 마법이 아닙니다. 타입 검사기를 설득했을 뿐입니다.

더 안전한 코드는 런타임 체크를 같이 둡니다.

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

if (!(input instanceof HTMLInputElement)) {
  throw new Error("input element not found");
}

input.value;

물론 모든 as가 나쁜 것은 아닙니다. TypeScript가 추론하지 못하지만 개발자가 런타임 조건을 이미 확인한 경우도 있습니다. 문제는 as가 빠른 해결책처럼 보인다는 점입니다.

ts
const user = JSON.parse(raw) as User;

이 코드는 컴파일러를 조용하게 만들지만, raw가 정말 User인지 검증하지 않습니다. 외부 입력, API 응답, localStorage, URL query처럼 경계 밖에서 들어온 값에는 단언보다 검증이 먼저입니다.

코드 리뷰 질문은 이렇게 바꿀 수 있습니다.

text
이 as는 TypeScript의 한계를 보정하는가?
아니면 실제로 필요한 런타임 검증을 숨기고 있는가?

---

프론트엔드 코드 리뷰에서 보는 법

Day 2의 내용은 타입 이론처럼 보이지만, 실제 리뷰에서는 꽤 구체적인 체크리스트가 됩니다.

1. 추론 결과를 확인했는가

상태, props, API 응답 타입이 이상할 때 바로 타입을 덧씌우지 말고 hover로 현재 추론 결과를 확인합니다. 타입이 너무 넓으면 생성 지점을 고치고, 너무 좁으면 확장될 가능성이 있는지 봅니다.

ts
const tabs = ["home", "settings", "profile"];

이 배열이 단순 문자열 배열인지, 세 개의 탭 값으로 제한되어야 하는지에 따라 타입 모델이 달라집니다.

2. union은 실제 상태 집합인가

ts
type ModalState = "open" | "closed";

이 모델이 충분한지 봐야 합니다. 로딩 중, 제출 중, 에러 상태가 실제 UI에 있다면 두 값만으로는 부족할 수 있습니다. 타입을 값의 집합으로 보면 누락된 상태를 질문하기 쉬워집니다.

3. `unknown`을 `any`로 도망치지 않았는가

외부 입력은 처음에는 unknown으로 받는 편이 정직합니다. 바로 any로 바꾸면 타입 시스템의 도움을 잃습니다.

ts
function handleMessage(message: unknown) {
  // 좁힌 뒤 사용한다.
}

4. `as`가 검증을 대체하고 있지 않은가

DOM query, JSON parse, URL query, 서버 응답 뒤에 붙은 as를 특히 봅니다. 그 값이 실제로 그 타입인지 확인하는 코드가 없다면 타입 안정성이 아니라 타입 침묵일 수 있습니다.

---

오늘의 정리

TypeScript를 잘 쓰려면 타입 문법을 많이 외우는 것보다, 컴파일러가 보는 세계를 탐색하는 습관이 먼저입니다.

에디터 hover와 tsc는 타입 시스템을 관찰하는 도구다. 타입은 가능한 값의 집합이다. 좁은 타입은 넓은 타입에 들어갈 수 있지만, 넓은 타입은 좁은 타입에 바로 들어갈 수 없다. unknown은 넓지만 안전하게 좁히라고 요구한다. never는 가능한 값이 없어서 exhaustiveness 체크에 유용하다. type space와 value space는 문맥에 따라 다르다. as는 값을 바꾸지 않고 타입 검사기를 설득할 뿐이다.

Day 2의 핵심은 타입을 “붙이는 것”이 아니라 “가능한 값의 범위를 설계하고 검증하는 것”입니다.

---

퀴즈

Quiz1 / 4
Q.타입을 값의 집합으로 볼 때 ready 또는 error 두 값의 union과 string의 관계로 가장 맞는 것은 무엇일까요?

Post Q&A

오케이징에게 물어보기

이펙티브 타입스크립트 2판 Day 2: 타입을 값의 집합으로 보기 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!

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