cf. 우아한 타입스크립트 - 2장
목차
타입스크립트의 Namespace(네임스페이스)
| 특징 | NameSpace | ES Modules |
|---|---|---|
| 목적 | 특수 용도 | 일반적 사용 |
| 구조 | 전역 스코프에 객체 생성 | 파일 단위로 스코프 격리 |
| 의존성 관리 | <reference> 태그 혹은 번들러 | import |
| Tree Shaking | 어려움 (불필요한 코드 제거 힘듦) | 매우 잘 됨 (최적화 유리) |
| 사용처 | "레거시 코드, 타입 정의(.d.ts)" | 모든 현대적 앱 개발 |
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/; // 외부에서 접근 불가능 (private 처럼 동작)
export interface StringValidator { // 외부에서 접근 가능 (export 사용)
isAcceptable(s: string): boolean;
}
export const variable = 123;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
}
// 사용 방법: '네임스페이스이름.멤버'
let myValidator = new Validation.LettersOnlyValidator();
console.log(Validation.variable); // 123
namespace Shapes {
export namespace Polygons {
export class Triangle { /* ... */ }
export class Square { /* ... */ }
}
}
// 접근: 점(.)으로 연결
import Polygon = Shapes.Polygons; // 별칭(alias) 사용 가능
let sq = new Polygon.Square();
// Validation.ts
namespace Validation { export interface StringValidator { ... } }
// LettersOnlyValidator.ts
/// <reference path="Validation.ts" />
namespace Validation { export class LettersOnlyValidator { ... } }
변하지 않는 상수이며, 가장 구체적인 값(리터럴) 그 자체로 타입을 지정
as const 없음
- 배열은
string[]- 객체 속성은 string 같은 일반적인 타입으로 추론
as const 있음
- 배열은
readonly 튜플- 객체 속성은 특정 문자열 값 그 자체로 고정(리터럴 타입)
const colors = ["red", "blue"]; // 타입: string[] (변경 가능)
const colorsConst = ["red", "blue"] as const; // 타입: readonly ["red", "blue"] (변경 불가, 순서 및 값 고정)
변수나 객체(값)를 읽어서 그 구조를 타입으로 변환
const user = {
name: "철수",
age: 25
};
// user 객체의 구조를 그대로 타입으로 가져옴
// type UserType = { name: string; age: number; }
type UserType = typeof user;
객체 타입에서 키(Key)들만 뽑아서 유니온 타입(Union Type, OR 조건)으로 만듦.
// 1. 데이터 정의 [ 읽기 전용, 값은 string이 아닌 '/', '/about' 리터럴 그 자체 ]
const ROUTES = {
HOME: '/',
ABOUT: '/about',
CONTACT: '/contact'
} as const;
type RoutesType = typeof ROUTES; // 2. 객체의 전체 타입 추출 (typeof)
type RouteKey = keyof RoutesType; // 3. 키(Key)만 추출 ("HOME" | "ABOUT" | "CONTACT")
type RouteValue = RoutesType[RouteKey]; // 4. 값(Value)만 추출 ("/" | "/about" | "/contact") ★ 가장 많이 쓰는 패턴
// 사용 예시
function moveTo(path: RouteValue) {
console.log(`이동 중: ${path}`);
}
moveTo(ROUTES.HOME); // ✅ 성공
moveTo('/about'); // ✅ 성공 (값이 정확히 일치하므로)
// moveTo('/login'); // ❌ 에러: RouteValue 타입에 '/login'은 없습니다.
const name: string = "지민";
const greeting: string = `안녕하세요, ${name}`;
new String()을 통해 생성되는 객체const nameObj: String = new String("지민");
console.log(typeof "지민"); // 'string' (원시 타입)
console.log(typeof nameObj); // 'object' (객체)
원시 타입이 메서드를 가질 수 있도록 하기 위함 (= Auto-boxing)
Auto-boxing
자바스크립트 엔진이 순간적으로 원시 타입(string)을 래퍼 객체(String)로 감싸서(Wrapping) 메서드를 실행하고 다시 원시 타입으로 돌려놓음
- 자바스크립트에서
"hello".toUpperCase()처럼 원시 타입 문자열이 메서드를 가질 수 있는 이유
true, false
진짜 불리언(Boolean) 값
Truthy, Falsy
불리언이 아니더라도, 참이나 거짓처럼 "취급"되는 값
false0 (숫자 0)-0 (음수 0)0n (BigInt 0)"" (빈 문자열)nullundefinedNaN (Not a Number)"0" (문자열 0)"false" (문자열 false)[] (빈 배열) → 객체 타입은 무조건 참{} (빈 객체) → 객체 타입은 무조건 참Infinity= "키는 문자열일 수 있지만, 값은 절대 존재할 수 없다(never)"
= 아무 속성도 넣을 수 없는 객체
사용처
- React 컴포넌트에서 Props를 받지 않음을 명시할 때
- API 요청 바디가 비어있어야 할 때
- 제네릭 타입에서 "아무 옵션도 받지 않음"을 표현할 때
const emptyObject: Record<string, never> = {}; // ✅ 성공: 빈 객체는 할당 가능
const notEmpty: Record<string, never> = { // ❌ 실패: 속성이 하나라도 있으면 에러 발생!
id: 1,
}; // 에러: Type 'number' is not assignable to type 'never'.
const user: Record<string, never> = { // ❌ 실패: 문자열도 안 됨
name: "철수"
};
// 1. 일반 {} 타입
const weakEmpty: {} = { name: "철수" }; // ⚠️ 성공 (엄밀히 말하면 '객체'라는 뜻에 가깝기 때문)
// (단, 객체 리터럴로 바로 넣을 때는 잉여 속성 체크가 작동하지만, 변수를 통해 넣으면 통과됨)
// 2. Record<string, never>
const strictEmpty: Record<string, never> = { name: "철수" }; // ❌ 무조건 에러 발생
interface - extends
명확한 계층 관계 (= 상속) - "A는 B를 상속받는다"
컴파일러가 해당 관계를 미리 예측하여, 재계산하지 않고 캐싱 용이
속성 충돌 발생 시, 선언 시점에 즉시 에러 호출
type - &
논리적인 연산 수행 필요 - "A와 B를 합친 새로운 모양 생성"
컴파일러가 A & B를 마주칠 때 마다, 두 타입의 모든 속성을 재귀적으로 검사해서 합치는 플랫화(Flattening) 작업 진행할 수 있음
타입이 복잡할 경우 컴파일러 부하 커짐 ( A & B & C & & ,,,)
interface
생성될 때 고유한 이름 가짐
컴파일러는 내부적으로 이 이름을 식별자로 사용하여 비교 수행
type (Alias)
이름은 붙어있지만, 실제로는 그 내용물(구조)을 가리키는 별칭
경우에 따라 컴파일러가 타입의 구조를 완전히 풀어서 비교해야 할 때가 있음
interface
같은 이름으로 여러 번 선언하면 자동으로 합쳐짐 (= 선언 병합)
컴파일러 입장에서 인터페이스는 "열려 있는 객체"로 취급하여, 나중에 속성이 추가될 것을 대비한 최적화 진행
type
선언되는 순간 닫혀 있는 완전한 타입으로 계산
유연한 최적화가 덜 들어감
구별된 유니온(Discriminated Union) = "태그된 유니온(Tagged Union)"
// 1. 각각의 인터페이스 정의 (kind 속성이 핵심!)
interface Circle {
kind: "circle"; // 리터럴 타입 (그냥 string이 아님)
radius: number;
}
interface Square {
kind: "square"; // 리터럴 타입
sideLength: number;
}
// 2. 유니온으로 합치기
type Shape = Circle | Square;
// 3. 사용하기
function getArea(shape: Shape) {
// 처음에는 shape가 Circle인지 Square인지 모름 (접근 불가)
// console.log(shape.radius); // ❌ 에러! Square일 수도 있으니까
// 4. 구분자(kind)를 통해 "타입 좁히기(Narrowing)" 수행
switch (shape.kind) {
case "circle":
// ✅ 여기 들어오면 TypeScript는 shape가 무조건 'Circle'임을 앎
return Math.PI * shape.radius ** 2;
case "square":
// ✅ 여기 들어오면 shape는 무조건 'Square'임
return shape.sideLength ** 2;
}
}
구별된 유니온을 사용할 때, 모든 케이스를 다 처리했는지 컴파일러가 검사하게 만드는 방법
type Shape = Circle | Square | Triangle; // Triangle이 새로 추가됨!
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return shape.radius ** 2 * Math.PI;
case "square":
return shape.sideLength ** 2;
// ⚠️ 실수로 Triangle 케이스를 작성하지 않음
default:
// 여기서 에러가 발생합니다!
// Triangle 타입은 never에 할당할 수 없기 때문입니다.
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
// 1. 상태별 타입 정의
interface LoadingState {
status: "loading"; // 구분자
}
interface SuccessState {
status: "success"; // 구분자
data: { id: number; name: string }; // 성공했을 때만 데이터가 있음
}
interface ErrorState {
status: "error"; // 구분자
error: Error; // 실패했을 때만 에러 객체가 있음
}
// 2. 전체 상태 타입
type ApiState = LoadingState | SuccessState | ErrorState;
// 3. 컴포넌트나 함수에서의 처리
function renderUI(state: ApiState) {
// state.data // ❌ 에러! (Loading이나 Error일 때는 data가 없으므로)
if (state.status === "loading") {
return "로딩 중...";
}
if (state.status === "error") {
// 여기서는 state.error에 안전하게 접근 가능
return `에러 발생: ${state.error.message}`;
}
if (state.status === "success") {
// 여기서는 state.data에 안전하게 접근 가능
return `사용자 이름: ${state.data.name}`;
}
}
zod의 safeParse 활용
import { z } from 'zod';
// 1. 스키마 정의
const UserConfigSchema = z.object({
theme: z.enum(['light', 'dark', 'system']),
notifications: z.object({
email: z.boolean(),
sms: z.boolean(),
}),
lastLogin: z.string().datetime().optional(), // ISO 날짜 문자열
});
// 2. 타입 추출 (TypeScript용)
type UserConfig = z.infer<typeof UserConfigSchema>;
// 3. 기본값 정의 (데이터가 깨졌을 때 사용할 안전장치)
const DEFAULT_CONFIG: UserConfig = {
theme: 'light',
notifications: { email: true, sms: false },
};
const STORAGE_KEY = 'app-user-config';
이 함수는 절대 에러를 던지지 않고 항상 유효한 UserConfig 객체를 반환함을 보장
function getSafeUserConfig(): UserConfig {
// 1. LocalStorage에서 Raw 데이터 가져오기 (string | null)
const rawData = localStorage.getItem(STORAGE_KEY);
if (!rawData) {
return DEFAULT_CONFIG; // 저장된 게 없으면 기본값 반환
}
try {
// 2. JSON 파싱 (여기서 문법 에러가 날 수 있으므로 try-catch)
const parsedJson = JSON.parse(rawData);
// 3. Zod로 구조 검증 (safeParse 사용!)
// parse()를 썼다면 여기서 형식이 안 맞을 때 에러(Throw)가 발생하여 앱이 멈춤
const result = UserConfigSchema.safeParse(parsedJson);
if (result.success) { // ✅ 검증 성공: Zod가 보장하는 안전한 데이터 반환
return result.data;
} else { // ❌ 검증 실패: 형식이 맞지 않음 (예: 사용자가 값을 조작함, 구버전 데이터)
console.warn('Storage data is invalid. Falling back to default.', result.error);
// (선택 사항) 잘못된 데이터는 지우거나 덮어쓰기
// localStorage.removeItem(STORAGE_KEY);
return DEFAULT_CONFIG; // 앱이 죽는 대신 기본값으로 우아하게 처리
}
} catch (e) { // JSON.parse 자체가 실패한 경우 (예: "undefined" 문자열 등)
console.error('JSON parsing failed', e);
return DEFAULT_CONFIG;
}
}
import { useState, useEffect } from 'react';
// 위에서 만든 Schema, DEFAULT_CONFIG 활용
export function useUserConfig() {
// 초기값 설정 시 위에서 만든 안전한 함수 사용
const [config, setConfig] = useState<UserConfig>(() => getSafeUserConfig());
// config가 변경될 때마다 LocalStorage에 저장
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
}, [config]);
// 설정 업데이트 함수
const updateConfig = (newConfig: Partial<UserConfig>) => {
setConfig((prev) => ({ ...prev, ...newConfig }));
};
return { config, updateConfig };
}
// result 변수의 타입
type SafeParseReturnType<T> =
| { success: true; data: T }
| { success: false; error: ZodError };
정리 굿굿굿 감사합니다
앞으로도 잘 부탁합니다 성준님!ㅎㅎ