요약과 서평 | 우아한 타입스크립트
이 책은 배민에서 사용하는 실제 코드를 기반으로 타입스크립트의 기본 개념과 리액트에서의 타입스크립트 활용법에 대해 알려준다. 또한 배민의 개발 사례와 인터뷰를 통해 다양한 견해와 기술 활용 팁을 알려준다.
타입스크립트 책은 읽어본 적이 없어서 고민하던 중에 이 책을 알게 되었고 사내 스터디를 통해 빠르게 한 권을 완독할 수 있었다.
간단하게 각 장을 요약하고 간단한 서평을 해보려고한다.
요약
1장 들어가며
자바스크립트의 역사와 한계를 간단히 알아보면서 타입스크립트가 등장하게 된 배경을 살펴본다.
2장 타입
정적 타이핑을 하기 위해 타입스크립트가 제공하는 타입과 관련된 내용을 살펴본다.
컴퓨터의 메모리 공간은 한정적이다. 따라서 특정 메모리에 값을 효율적으로 저장하기 위해서는 먼저 해당 메모리 공간을 차지할 값의 크기를 알아야 한다. 값의 크기를 명시한다면 컴퓨터가 값을 참조할 때 한 번에 읽을 메모리 크기를 알 수 있어 값을 훼손하지 않고 가져올 수 있다.
자바스크립트의 7가지 데이터 타입
- undefined
- null
- Boolean
- String
- Symbol
- Numeric(Number와 BigInt)
- Object
정적 타입과 동적 타입
정적 타입 시스템에서는 모든 변수의 타입이 컴파일 타임에 결정된다. 동적 타입 시스템에서는 변수 타입이 런타임에서 결정된다.
- 컴파일 타임 : 기계(컴퓨터, 엔진)가 소스코드를 이해할 수 있도록 기계어로 변환되는 시점
- 런타임 : 변환된 파일이 메모리에 적재되어 실행되는 시점
타입스크립트는 강타입 언어다.
- 강타입 : 서로 다른 타입을 갖는 값끼리 연산을 시도하면 컴파일러 또는 인터프리터에서 에러가 발생
타입 시스템의 두 가지 종류
- 어떤 타입을 사용하는지를 컴파일러에 명시적으로 알려줘야 하는 시스템
- 자동으로 타입을 추론하는 시스템
타입스크립트는 두 가지 타입 시스템에 영향을 받았다.
타입스크립트의 타입 시스템
타입스크립트는 구조로 타입을 구분한다. 이것을 구조적 타이핑이라고 한다.
구조적 서브타이핑이란 객체가 가지고 있는 속성을 바탕으로 타입을 구분하는 것이다. 이름이 다른 객체라도 가진 속성이 동일하다면 타입스크립트는 서로 호환이 가능한 동일한 타입으로 여긴다.
자바스크립트는 본질적으로 덕 타이핑을 기반으로 한다.
타입스크립트는 자바스크립트의 특징을 그대로 받아들여 명시적인 이름을 가지고 타입을 구분하는 대신 객체나 함수가 가진 구조적 특징을 기반으로 타이핑하는 방식을 택했다.
덕 타이핑은 런타임에 타입 검사, 구조적 타이핑은 컴파일 타임에 검사
값 vs 타입
타입스크립트 문법인 type으로 선언한 내용은 자바스크립트 런타임에서 제거되기 때문에 값 공간과 타입 공간은 서로 충돌하지 않는다.
값과 타입 공간에 동시에 존재하는 클래스와 enum도 있다.
타입스크립트에서 typeof 연산자도 값에서 쓰일 때와 타입에서 쓰일 때의 역할이 다르다.
타입스크립트의 원시 타입 : 자바스크립트의 원시 값은 타입스크립트에서 원시 타입으로 존재한다.
- boolean
- undefined
- null
- number : NaN이나 Infinity도 포함된다.
- bigInt
- string
- symbol : 어떤 값과도 중복되지 않는 유일한 값을 생성할 수 있다.
타입스크립트의 객체 타입 : 자바스크립트의 원시 값은 타입스크립트에서 원시 타입으로 존재한다.
- object
- {} : 빈 객체임을 의미한다.
- array
- type과 interface
- function
3장 고급 타입
자바스크립트 자료형에 없는 타입스크립트만의 타입 시스템을 소개한다.

any
any 타입은 자바스크립트에 존재하는 모든 값을 오류 없이 받을 수 있다. any는 지양하는 게 좋다.
unknown
unknown 타입은 이름처럼 무엇이 할당될지 아직 모르는 상태의 타입을 말한다.
unknown 타입은 any 타입과 유사하게 모든 타입의 값이 할당될 수 있다. 그러나 any를 제외한 다른 타입으로 선언된 변수에는 unknown 타입 값을 할당할 수 없다.
never
never라는 단어가 내포하고 있는 의미처럼 never 타입은 값을 반환할 수 없는 타입을 말한다.
자바스크립트에서 값을 반환할 수 없는 예
- 에러를 던지는 경우 : throw 키워드를 사용하면 에러를 발생시킬 수 있는데, 이는 값을 반환하는 것으로 간주하지 않는다.
- 무한히 함수가 실행되는 경우
타입 조합
// Index Signature
interface IndexSignatureEx {
[key: string]: number;
}
// Mapped types
type Example = {
a: number;
b: string;
c: boolean;
};
type Subset<T> = {
[K in keyof T]?: T[K];
};
const aExample: Subset<Example> = { a: 3 };
const bExample: Subset<Example> = { b: "hello" };
const acExample: Subset<Example> = { a: 4, c: true };
// Template Literal Types
type Stage =
| "init"
| "select-image"
| "edit-image"
| "decorate-card"
| "capture-image";
type StageName = `${Stage}-stage`;
// Generic
function exampleFunc2<T>(arg: T): number {
return arg.length; // 에러 발생: Property 'length' does not exist on type 'T'
}
interface TypeWithLength {
length: number;
}
function exampleFunc2<T extends TypeWithLength>(arg: T): number {
return arg.length;
}
4장 타입 확장하기, 좁히기
타입 확장하기
타입 확장을 통해 코드 중복을 줄일 수 있다.
type BaseMenuitem = {
itemName: string | null;
itemlmageUrl: string | null;
itemDiscountAmount: number;
stock: number | null;
};
type BaseCartitem = {
quantity: number;
} & BaseMenuitem;
interface EditableCartltem extends BaseCartitem {
isSoldOut: boolean;
optionGroups: SelectableOptionGroup[];
};
interface EventCartltem extends BaseCartltem {
orderable: boolean;
}
유니온 타입과 교차 타입을 사용한 새로운 타입은 오직 type 키워드로만 선언할 수 있다.
교차 타입
type IdType = string | number;
type Numeric = number | boolean;
type Universal = IdType & Numeric; // number;
type DeliveryTip = {
tip: number;
};
type Filter = DeliveryTip & {
tip : string;
}; // never;
extends를 활용한 타입 (교차 타입과 100% 상응하지 않음)
interface DeliveryTip {
tip: number;
}
interface Filter extends DeliveryTip {
tip : string;
// Interface ’Filter' incorrectly extends interface 'DeliveryTip'
// Types of property 'tip' are incompatible
// Type 'string' is not assignable to type 'number'
}
타입 좁히기
- typeof에서 null, array 등은 object로 판별되는 문제가 있어, typeof 연산자는 주로 원시 타입을 좁히는 용도로만 사용할 것을 권장한다.
- 객체의 속성이 있는지 없는지에 따른 구분 : in 연산자 활용하기
- is 연산자로 사용자 정의 타입 가드 만들어 활용하기
// 1
const isDestinationCode = (x: string): x is DestinationCode =>
destinationCodeList.includes(x);
// 2
const isDestinationCode = (x: string): boolean =>
destinationCodeList.includes(x);
const getAvailableDestinationNameList = async (): Promise<DestinationName[]> => {
const data = await AxiosRequest<string[]>("get", ".../destinations");
const destinationNames: DestinationName[] = [];
data?.forEach((str) => {
if (isDestinationCode(str)) {
destinationNames.push(DestinationNameSet[str]);
}
});
return destinationNames;
};
유닛 타입
- 다른 타입으로 쪼개지지 않고 오직 하나의 정확한 값을 가지는 타입
type TextError = {
errorType: "TEXT";
errorCode: string;
errorMessage: string;
};
Exhaustiveness Checking
type ProductPrice = "10000" | "20000" | "5000";
const getProductName = (productPrice: ProductPrice): string => {
if (productPrice === "10000") return "배민상품권 1만 원";
if (productPrice === "20000") return "배민상품권 2만 원";
// if (productPrice === "5000") return "배민상품권 5천 원";
else {
exhaustiveCheck(productPrice); // Error: Argument of type 'string' is not assign able to parameter of type 'never'
return "배민상품권"; }
};
const exhaustiveCheck = (param: never) => {
throw new Error('type error!”);
};
5장 타입 활용하기
타입스크립트의 다양한 기법과 유틸리티 타입을 활용하는 실무 코드를 살펴본다.
조건부 타입
T extends U ? X : Y
타입 T를 U에 할당할 수 있으면 X 타입, 아니면 Y 타입으로 결정됨
infer를 활용해서 타입 추론하기
type UnpackPromise<T> = T extends Promise<infer K>[] ? K : any;
제네릭으로 T를 받아 T가 Promise로 래핑된 경우라면 K를 반환하고, 그렇지 않은 경우에는 any를 반환
Promise<infer K>는 Promise의 반환 값을 추론해 해당 값의 타입을 K로 한다는 의미
활용 예시
export interface SubMenu {
name: string;
path: string;
}
export interface MainMenu {
name: string;
path?: string;
subMenus?: SubMenu[];
}
type MenuItem = MainMenu | SubMenu;
type UnpackMenuNames<T extends ReadonlyArray<MenuItem>> =
T extends ReadonlyArray<infer U>
? U extends MainMenu
? U['subMenus'] extends infer V
? V extends ReadonlyArray<SubMenu>
? UnpackMenuNames<V>
: U['name']
: never
: U extends SubMenu
? U['name']
: never
: never;
PickOne<T>
type One<T> = { [P in keyof T]: Record<P, T[P]> }[keyof T];
type ExcludeOne<T> = { [P in keyof T]: Partial<Record<Exclude<keyof T, P>, undefined>>}[keyof T];
type PickOne<T> = One<T> & ExcludeOne<T>;
type PickOne<T> = {
[P in keyof T]: Record<P, T[P]> & Partial<Record<Exclude<keyof T, P>, undefined>>
}[keyof T];
NonNullable
type NonNullable<T> = T extends null | undefined ? never : T;
function NonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
6장 타입스크립트 컴파일
타입스크립트 컴파일러의 주요 역할과 구조에 대해 알아본다.
타입스크립트 컴파일러가 소스코드를 컴파일하여 프로그램이 실행되기까지의 과정
- 타입스크립트 소스코드를 타입스크립트 AST로 만든다. (tsc)
- 타입 검사기가 AST를 확인하여 타입을 확인한다. (tsc)
- 타입스크립트 AST를 자바스크립트 소스로 변환한다. (tsc)
- 자바스크립트 소스코드를 자바스크립트 AST로 만든다. (런타임)
- AST가 바이트 코드로 변환된다. (런타임)
- 런타임에서 바이트 코드가 평가evaluate되어 프로그램이 실행된다. (런타임)
타입스크립트 컴파일러의 구조

프로그램 : 컴파일할 타입스크립트 소스 파일과 소스 파일 내에서 임포트된 파일을 불러옴
스캐너 : 타입스크립트 소스 파일을 어휘적으로 분석하여 토큰을 생성하는 역할

파서 : 토큰 정보를 이용하여 AST를 생성

바인더 : 타입 검사를 위해 심볼이라는 데이터 구조를 생성
체커, 이미터
- 이미터 : 타입스크립트 소스 파일을 변환하는 역할
- 체커 : AST의 노드를 탐색하면서 심볼 정보를 불러와 주어진 소스 파일에 대해 타입 검사를 진행
타입스크립트의 컴파일 과정
- tsc 명령어를 실행하여 프로그램 객체가 컴파일 과정을 시작한다.
- 스캐너는 소스 파일을 토큰 단위로 분리한다.
- 파서는 토큰을 이용하여 AST를 생성한다.
- 바인더는 AST의 각 노드에 대응하는 심볼을 생성한다. 심볼은 선언된 타입의 노드 정보를 담고 있다.
- 체커는 AST를 탐색하면서 심볼 정보를 활용하여 타입 검사를 수행한다.
- 타입 검사 결과 에러가 없다면 이미터를 사용해서 자바스크립트 소스 파일로 변환한다.
7장 비동기 호출
타입스크립트에서 비동기 요청을 어떻게 처리하고 관리하는지를 다룬다.
API 응답 타입 지정
interface Response<T> {
data: T;
status: string;
serverDateTime: string;
errorCode?: string; // FAIL, ERROR
errorMessage?: string; // FAIL, ERROR
}
interface response {
data: {
cartItems: CartItem[];
forPass: unknown;
};
}
const isTargetValue = () => (data.forPass as ForPass).type === "A";
해당 값에 어떤 응답이 들어있는지 알 수 없거나 값의 형식이 달라지더라도 로직에 영향을 주지 않는 경우에는 unknown 타입을 사용하여 알 수 없는 값임을 표현한다. 다만 이미 설계 된 프로덕트에서 쓰고 있는 값이라면 프론트 로직에서 써야 하는 값에 대해서만 타입을 선언한 다음에 사용하는 게 좋다.
타입 가드 활용
Axios 라이브러리에서는 Axios 에러에 대해 isAxiosError라는 타입 가드를 제공한다.
interface ErrorResponse {
status: string;
serverDateTime: string;
errorCode: string;
errorMessage: string;
}
function isServerError(error: unknown): error is AxiosError<ErrorResponse> {
return axios.isAxiosError(error);
}
8장 JSX에서 TSX로
리액트에서 사용하는 JSX 문법을 타입스크립트에 어떻게 적용하는지를 소개한다.
함수 컴포넌트 타입
// 함수 선언을 사용한 방식
function Welcome(props: WelcomeProps): JSX.Element {}
// 함수 표현식을 사용한 방식 - React.FC 사용
const Welcome: React.FC<WelcomeProps> = ({ name }) => {};
// 함수 표현식을 사용한 방식 - React.VFC 사용
const Welcome: React.VFC<WelcomeProps> = ({ name }) => {};
// 함수 표현식을 사용한 방식 - JSX.Element를 반환 타입으로 지정
const Welcome = ({ name }: WelcomeProps): JSX.Element => {};
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
// props에 children을 추가
(props: PropsWithChildren<P>, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}
type VFC<P = {}> = VoidFunctionComponent<P>;
interface VoidFunctionComponent<P = {}> {
// children 없음
(props: P, context?: any): ReactElement<any, any> | null;
propTypes?: WeakValidationMap<P> | undefined;
contextTypes?: ValidationMap<any> | undefined;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}
리액트 v18에서 React.VFC 가 삭제되고 React.FC에서 children이 사라짐
React.ReactElement
// ReactElement : React.createElement의 반환 타입
interface ReactElement<P = any,
T extends string | JSXElementConstructor<any> =
| string
| JSXElementConstructor<any>
> {
type: T;
props: P;
key: Key | null;
}
React.ReactNode
// ReactNode
type ReactNode =
| ReactChild
| ReactFragment
| ReactPortal
| boolean
| null
| undefined;
type ReactChild = ReactElement | ReactText;
JSX.Element 타입
// JSX.Element
declare global {
namespace JSX {
interface Element extends React.ReactElement<any, any> {}
}
}

// ReactNode
// 리액트 내장 타입인 PropsWithChildren타입도 ReactNode타입으로 children을 선언하고 있다.
// JSX.Element
interface Props {
icon: JSX.Element;
}
const Item = ({ icon }: Props) => {
// prop으로 받은 컴포넌트의 props에 접근할 수 있다
const iconSize = icon.props.size; // 추론은 안됨
return (<li>{icon}</li>);
};
// icon prop에는 JSX.Element 타입을 가진 요소만 할당할 수 있다
const App = () => {
return <Item icon={<Icon size={14} />} />
};
// ReactElement
interface IconProps {
size: number;
}
interface Props {
// ReactElement의 props 타입으로 IconProps 타입 지정
icon: React.ReactElement<IconProps>;
}
const Item = ({ icon }: Props) => {
// icon prop으로 받은 컴포넌트의 props에 접근하면, props의 목록이 추론된다
const iconSize = icon.props.size;
return <li>{icon}</li>;
};
DetailedHTMLProps와 ComponentPropsWithoutRef
React.DetailedHTMLProps
type NativeButtonProps = React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>;
type ButtonProps = {
onClick?: NativeButtonProps["onClick"];
};
ComponentPropsWithoutRef
type NativeButtonType = React.ComponentPropsWithoutRef<"button">;
type ButtonProps = {
onClick?: NativeButtonType["onClick"];
};
DetailedHTMLProps 는 ref가 포함되어있다.
HTML 속성을 확장하는 props를 설계할 때는 ComponentPropsWithoutRef 타입을 사용하여 ref가 실제로 forwardRef와 함께 사용될 때만 props로 전달되도록 타입을 정의하는 것이 안전하다.
type NativeButtonType = React.ComponentPropsWithoutRef<"button">;
// forwardRef의 제네릭 인자를 통해 ref에 대한 타입으로 HTMLButtonElement를, props에 대한 타입으로 NativeButtonType을 정의했다
const Button = forwardRef<HTMLButtonElement, NativeButtonType>((props, ref) => {
return (
<button ref={ref} {...props}>
버튼
</button>);
});
리액트 이벤트
type EventHandler<Event extends React.SyntheticEvent> = (e: Event) => void | null;
type ChangeEventHandler = EventHandler<ChangeEvent<HTMLSelectElement>>;
공변성과 반공변성
공변성 : 타입 A가 B의 서브타입일 때, T<A>가 T<B>의 서브타입이 된다.
반공변성 : T<B>가 T<A>의 서브타입이 되어, 좁은 타입 T<A>의 함수를 넓은 타입 T<B>의 함수에 적용할 수 없다는 것 (ex: 제네릭)
interface Props<T extends string> {
onChangeA?: (selected: T) => void;
onChangeB?(selected: T): void;
}
// 반공변성을 가진다.
// onChangeA는 type 에러가 난다.
9장 훅
리액트에서 제공하는 몇 가지 훅을 사용하여 상태 또는 사이드 이펙트를 다루는 방법을 소개 한다.
// useState Type
function useState<S>(
initialstate: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);
// useEffect, useLayoutEffect
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
type DependencyList = ReadonlyArray<any>;
type EffectCallback = () => void | Destructor;
// useMemo, useCallback
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
// useRef
function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T>;
function useRef<T = undefined>(): MutableRefObject<T | undefined>;
interface MutableRefObject<T> {
current: T;
}
interface RefObject<T> {
readonly current: T ; null;
}
useEffect는 deps가 변경되었는지를 얕은 비교로만 판단하기 때문에, 실제 객체 값이 바뀌지 않았더라도 객체의 참조 값이 변경되면 콜백 함수가 실행된다. 부모에서 받은 인자를 직접 deps로 작성한 경우, 원치 않는 렌더링이 반복될 수 있다. 이를 방지하기 위해서는 실제로 사용하는 값을 useEffeet의 deps에서 사용해야 한다.
10장 상태 관리
리액트 애플리케이션에서 가장 중요한 역할을 하는 상태에 대해 알아본다.
상태란 ?
렌더링 결과에 영향을 주는 정보를 담은 순수 자바스크립트 객체
- 지역 상태
- 전역 상태
- 서버 상태
상태로 정의할 때 고려해야할 사항
- 시간이 지나도 변하지 않는다면 상태가 아니다 → 동일한 객체 참조를 할 때는 useRef를 사용할 것을 권장
- 파생된 값은 상태가 아니다. → 단일 출처를 가지게 하자
useReducer 타입
type Action =
| { payload: ReviewFilter; type: "filter"; }
| { payload: number; type: "navigate"; }
| { payload: number; type: "resize"; };
const reducer: React.Reducer<State, Action> = (state, action) => {
switch (action.type) {
case "filter":
return {
filter: action.payload,
page: 0,
size: state.size,
};
case "navigate":
return {
filter: state.filter,
page: action.payload,
size: state.size,
};
case "resize":
return {
filter: state.filter,
page: 0,
size: action.payload,
};
default:
return state;
}
}
const [fold, toggleFold] = useReducer((v) => !v, true);
11장 CSS-in-JS
CSS-in-JS의 개념과 사용법에 관해 알아본다.
CSS-in-JS vs 인라인 스타일
인라인 스타일은 DOM 노드에 속성으로 스타일을 추가한 반면에 CSS-in-JS는 DOM 상단에 <style> 태그를 추가했다.
12장 타입스크립트 프로젝트 관리
타입스크립트 프로젝트에서 유용하게 활용할 수 있는 개념과 팁을 소개한다.
앰비언트 타입
.d.ts 확장자를 가진 파일에서는 타입 선언만 할 수 있으며 값을 표현할 수는 없다.
declare module "*.png" {
const src: string;
export default src;
}
declare 키워드는 이미 존재하지만 타입스크립트가 알지 못하는 부분을 컴파일러에 ‘이러한 것이 존재해’라고 알려주는 역할을 한다.
• ts 파일 내의 앰비언트 변수 선언은 개발자에게 혼란을 야기할수 있다.
module, namespace, global
모듈이란 자기만의 독립적인 스코프를 가지고 있는 것 ts는 파일 내에 export가 없으면 script 파일로 인식 (전역에 영향), export가 있으면 모듈로 인식한다.
namespace(내부 모듈)는 한 파일 내에서 모듈 방식 구현 가능
// 내부에서 선언한 DOM 이라는 모듈 (네임스페이스)
// .ts
namespace Dom {
// 외부에서 접근 불가
const variable = 123;
// 외부에서 접근 가능하도록 export
export function add(arg1: number, arg2: number): number {
return arg1 + arg2;
}
// export 하지 않은 네임스페이스 내부에 정의된 함수는 외부에서 접근 불가
function subtract(arg1: number, arg2: number): number {
return arg1 - arg2;
}
}
Dom.add(1,2); // 3
공식문서에 따르면 모던 코드들에 대해 Module을 사용할 것을 권장하고 있다.
13장 타입스크립트와 객체 지향
타입스크립트와 리액트 환경에서 객체 지향을 어떻게 활용하고 더 나은 방향으로 발전시킬 수 있는지 알아본다.
타입스크립트 내용은 없어서 생략!
서평
이 책은 주니어 개발자들이 2년여 동안 타입스크립트에 대해 고민하여 집필했다고 한다. 같은 주니어 개발자 입장에서 좀 더 공감하면서 인사이트를 얻을 수 있었다. 물론 나는 모든 웹 프로젝트에서 타입스크립트를 사용했지만, 기본적인 활용만 했고 모르는 내용들이 많았다. 타입 단언으로 타입을 땜빵 치던 내가 이번에 책을 읽으며 내가 몰랐던 개념들을 많이 알 수 있었고 적용할 수 있었다. 이 책은 나중에 한 번 더 읽어볼 생각이고 이펙티브 타입스크립트도 한 번 더 빠르게 읽어야겠다!
책 완전 추천!