이펙티브 타입스크립트 2판 Day 13: 타입은 런타임과.
Item 74–78 범위를 바탕으로 런타임 타입 재구성, DOM과 환경 모델, 단위 테스트 관계, 컴파일러 성능을 코드 리뷰 관점에서 정리합니다.
범위: Effective TypeScript 2판 Item 67–73
오늘의 질문: “타입이 맞으면 라이브러리/컴포넌트 API도 쓰기 쉬운 걸까?”
type InternalButtonState = {
id: string;
mountedAt: number;
analyticsKey?: string;
variant: "primary" | "secondary";
};
export type ButtonProps = InternalButtonState & {
children: React.ReactNode;
onClick?: () => void;
};
동작은 될 수 있습니다. 하지만 이 타입은 내부 구현 상태까지 public props로 새어 나갑니다. 사용하는 쪽은 id, mountedAt, analyticsKey가 정말 필요한 값인지 헷갈립니다. 나중에 내부 구현을 바꾸고 싶어도 이미 공개된 타입이 발목을 잡습니다.
타입 재사용은 좋은 습관처럼 보입니다. 하지만 public API에서는 “중복 제거”보다 “사용자가 붙잡을 계약”이 더 중요할 때가 많습니다.
type ButtonVariant = "primary" | "secondary";
type ButtonProps = {
variant?: ButtonVariant;
children: React.ReactNode;
onClick?: () => void;
};
이렇게 공개 타입을 작게 만들면 내부 state는 자유롭게 바꿀 수 있습니다. 반대로 외부 사용자가 의존해야 하는 값은 타입에 분명히 드러납니다.
리뷰 질문은 이렇습니다.
이 타입은 구현을 설명하는가, 사용자가 의존할 계약을 설명하는가?
타입은 값의 모양을 잘 말하지만, 의도·단위·부작용·주의점까지 모두 말하지는 못합니다.
/**
* 서버에 저장된 초 단위 만료 시간을 받아 남은 시간을 계산한다.
* 클라이언트 시계와 서버 시계가 다를 수 있으므로 표시 용도로만 사용한다.
*/
export function getRemainingSeconds(expiresAtEpochSeconds: number): number {
return expiresAtEpochSeconds - Math.floor(Date.now() / 1000);
}
number라는 타입만 보면 millisecond인지 second인지 모릅니다. 함수 이름으로 어느 정도 추측할 수 있지만, 공개 API라면 문서가 계약 일부가 됩니다.
좋은 TSDoc은 타입을 반복하지 않습니다.
| 약한 문서 | 더 나은 문서 |
|---|---|
id는 string입니다 | GitHub user id이며 URL slug가 아님 |
timeout은 number입니다 | millisecond 단위, 0이면 재시도 없음 |
callback 함수입니다 | 실패해도 throw하지 말고 false를 반환함 |
자바스크립트 라이브러리 API에서는 callback의 this가 계약이 될 수 있습니다. 타입스크립트에서는 가짜 첫 번째 매개변수처럼 this 타입을 적을 수 있습니다.
type VisitCallback = (this: HTMLElement, event: MouseEvent) => void;
function onVisit(element: HTMLElement, callback: VisitCallback) {
element.addEventListener("click", function (event) {
callback.call(element, event);
});
}
React 컴포넌트 코드에서는 보통 callback this를 쓰지 않는 편이 자연스럽습니다. 하지만 DOM 래퍼, 플러그인, 레거시 라이브러리 어댑터를 만들 때는 “this를 제공하는 API인지, this를 쓰면 안 되는 API인지”를 타입으로 드러내야 합니다.
리뷰에서는 다음을 봅니다.
this 바인딩이 필요한 API인가?any로 통과시키고 있지 않은가?기존 라이브러리 타입을 확장해야 할 때 module augmentation을 쓸 수 있습니다.
declare module "express-session" {
interface SessionData {
userId?: string;
}
}
이런 코드는 편합니다. 하지만 전역처럼 적용되므로 위치와 의도가 흐리면 프로젝트 전체 타입 환경을 조용히 바꿉니다. 특히 프론트엔드에서는 라우터, theme, test matcher, global window 확장에서 자주 보입니다.
리뷰 질문은 “확장이 필요한가?”보다 한 단계 더 들어갑니다.
타입스크립트에는 enum, namespace, decorator, parameter property처럼 자바스크립트로 변환될 때 런타임 코드를 만들거나 문법 해석 부담을 주는 기능도 있습니다.
enum Status {
Idle,
Loading,
Done,
}
이 코드가 항상 나쁘다는 뜻은 아닙니다. 다만 프론트엔드 앱에서는 번들 출력, tree-shaking, Babel/SWC/Vite 설정, 문서 예제의 이해 난도를 함께 봐야 합니다. 단순 union으로 충분하다면 런타임 출력이 없는 타입을 고르는 편이 더 예측 가능할 수 있습니다.
type Status = "idle" | "loading" | "done";
리뷰 질문은 “이 기능을 쓸 수 있는가?”가 아니라 “이 팀과 빌드 파이프라인에서 이 기능이 가장 단순한 선택인가?”입니다.
타입스크립트는 브라우저가 그대로 실행하는 코드가 아닙니다. 번들러와 트랜스파일러를 거쳐 실행됩니다. 운영 에러를 원래 TS/TSX 파일 위치로 추적하려면 source map 정책이 필요합니다.
{
"compilerOptions": {
"sourceMap": true
}
}
하지만 source map은 공개 범위도 고려해야 합니다. 외부에 배포되는 앱에서 소스가 그대로 노출되는 형태인지, 에러 수집 도구에만 업로드하는지, 민감한 경로/주석이 들어가지 않는지 확인해야 합니다.
코드 리뷰에서는 배포 설정까지 같이 봅니다.
| 변화 | 확인할 질문 |
|---|---|
export type 추가 | 내부 구현 타입이 public contract로 새고 있지 않은가? |
| TSDoc 추가 | 타입을 반복하지 않고 단위/의도/제약을 설명하는가? |
| callback 타입 | this 계약이 실제 호출 방식과 맞는가? |
| module augmentation | 전역 타입 확장이 찾기 쉽고 런타임 주입과 일치하는가? |
| enum/namespace/decorator 사용 | 빌드 출력과 팀 디버깅 비용까지 감당할 이유가 있는가? |
| source map 설정 | 운영 추적성과 소스 노출 정책이 함께 검토됐는가? |
이번 범위의 핵심은 “타입을 더 많이 적는 것”이 아닙니다. 타입은 사용자가 API를 어떻게 이해하고, 어떤 값에 의존하며, 운영에서 어떻게 디버깅할지를 정하는 인터페이스입니다. 공개 경계에서는 내부 구현 편의보다 사용자 계약을 먼저 봐야 합니다.
Post Q&A
이펙티브 타입스크립트 2판 Day 12: 타입은 공개 API의 사용법이다 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!
Item 74–78 범위를 바탕으로 런타임 타입 재구성, DOM과 환경 모델, 단위 테스트 관계, 컴파일러 성능을 코드 리뷰 관점에서 정리합니다.
Item 79–83 범위를 바탕으로 JS 현대화, @ts-check와 JSDoc, allowJs, 모듈별 마이그레이션, noImplicitAny를 코드 리뷰 관점에서 정리합니다.
Item 67–73 범위를 바탕으로 public API 타입, TSDoc, this callback, module augmentation, TypeScript 기능 선택, source map을 코드 리뷰 관점에서 정리합니다.
Item 60–66 범위를 바탕으로 Object iteration, Record, tuple/rest, XOR, brand, @types 버전 경계를 코드 리뷰에서 다루는 방법을 정리합니다.
Item 55–59 범위를 바탕으로 type test, 타입 표시, 꼬리 재귀 한계, 코드 생성, never exhaustiveness를 코드 리뷰에서 다루는 방법을 정리합니다.
Item 49–54 범위를 바탕으로 type coverage, 제네릭, 조건부 타입, 템플릿 리터럴 타입을 코드 리뷰에서 어떻게 판단할지 정리합니다.
Item 42–48 범위를 바탕으로 any, unknown, 타입 단언, monkey patching, soundness 함정을 외부 입력 경계와 코드 리뷰 관점에서 정리합니다.
Item 35–41 범위를 바탕으로 string 남용, optional 필드, 특수 값, 도메인 이름 설계를 코드 리뷰 관점에서 정리합니다.
Item 28–34 범위를 바탕으로 추론 위치, 유효 상태 모델링, API 입출력, null 경계, union 설계를 프론트엔드 코드 리뷰 관점에서 확장 정리합니다.
Item 22–27 범위를 바탕으로 narrowing, alias, context inference, evolving type, async/type flow를 프론트엔드 코드 리뷰 관점에서 다시 정리합니다.
Effective TypeScript 2판의 Item 16–21을 바탕으로 index signature, 타입 추론 기본, 변수/객체 생성 패턴을 프론트엔드 코드 리뷰 관점에서 정리합니다.
Effective TypeScript 2판의 Item 11–15를 바탕으로 excess property check, 함수식 타입, type vs interface, readonly, 타입 반복 제거를 프론트엔드 코드 리뷰 관점에서 정리합니다.
Effective TypeScript 2판의 Item 6–10을 바탕으로 타입 시스템을 탐색하는 법, 타입을 값의 집합으로 보는 관점, type/value space, 타입 단언의 경계를 코드 리뷰 관점에서 정리합니다.
Effective TypeScript 2판의 Item 1–5를 바탕으로 TypeScript와 JavaScript의 관계, tsconfig, 타입 제거, 구조적 타이핑, any의 위험을 프론트엔드 코드 리뷰 관점에서 정리합니다.