TypeScript는 정적 타이핑을 제공하는 JavaScript의 강력한 상위 집합으로, 대규모 애플리케이션의 유지 보수성과 확장성을 크게 향상시킬 수 있습니다. 다음은 실제 프로젝트에서 TypeScript를 효과적으로 사용하기 위한 전략과 메서드입니다
1. 인터페이스를 사용하여 타입 정의하기
인터페이스는 TypeScript의 핵심 기능으로, 객체의 구조를 정의할 수 있습니다. 애플리케이션의 데이터 모델, API 응답, 컴포넌트 props 등의 구조를 정의할 때 특히 유용합니다. 이를 통해 타입에 대한 문서화와 코드 자동 완성을 제공할 수 있습니다.
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
2. 제네릭을 활용하여 유연하고 재사용 가능한 코드 작성하기
제네릭을 통해 타입 안전성을 유지하면서 다양한 타입으로 동작하는 코드를 작성할 수 있습니다. 재사용 가능한 컴포넌트와 함수를 만들 때 특히 유용합니다. 이를 통해 타입 관련 버그를 방지하고 코드 중복을 최소화할 수 있습니다.
function getById<T>(id: number, items: T[]): T | undefined {
return items.find((item) => item.id === id);
}
좀 더 위에 인터페이스 내용과 연관지어 확장해보면
interface Product {
id: number;
name: string;
}
const products: Product[] = [
{ id: 1, name: "Laptop" },
{ id: 2, name: "Phone" },
];
// 데이터 검색 함수
function getById<T extends { id: number }>(id: number, items: T[]): T | undefined {
return items.find((item) => item.id === id);
}
// 사용 예시
const result = getById(1, products);
console.log(result?.name); // "Laptop"
이와 같이 사용할 수 있습니다
3. 엄격한 null 검사 구현하기
null과 undefined 값은 JavaScript에서 자주 발생하는 버그의 원인이 될 수 있습니다. TypeScript의 엄격한 null 검사를 통해 이러한 문제를 개발 초기에 발견할 수 있습니다. 이를 통해 코드의 안정성과 예측성을 높일 수 있습니다
// 엄격한 null 검사 사용
const user: User | null = findUser(id);
if (user) {
console.log(user.name);
}
// 엄격한 null 검사 미사용
const user: User = findUser(id);
console.log(user.name); // user가 null인 경우 오류 발생
4. 유틸리티 타입을 사용하여 일반적인 타입 변환 수행하기
TypeScript는 Partial, Required, Readonly, Pick 등의 유틸리티 타입을 제공하여 타입을 쉽게 변환할 수 있습니다. 이를 통해 반복적인 타입 변환 코드를 줄이고 코드의 가독성을 높일 수 있습니다.
1. Partial<T>
- 설명: 기존 타입 T의 모든 속성을 선택적으로(optional) 변환합니다.
- 사용 예시: 어떤 객체의 특정 속성만 채워도 되는 경우 사용.
interface User {
id: number;
name: string;
age: number;
}
type PartialUser = Partial<User>;
const userUpdate: PartialUser = {
name: "Alice", // `id`와 `age`가 없어도 오류 없음
};
2. Required<T>
- 설명: 기존 타입 T의 모든 속성을 필수(required)로 변환합니다.
- 사용 예시: 선택적 속성을 가진 타입을 모든 속성이 필수인 타입으로 변경할 때 사용.
interface User {
id: number;
name?: string;
age?: number;
}
type RequiredUser = Required<User>;
const newUser: RequiredUser = {
id: 1,
name: "Alice", // 이제 모든 속성이 필수
age: 25,
};
3. Readonly<T>
- 설명: 기존 타입 T의 모든 속성을 읽기 전용(readonly)으로 변환합니다.
- 사용 예시: 객체가 수정되지 않도록 보장할 때 사용
interface User {
id: number;
name: string;
}
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = {
id: 1,
name: "Alice",
};
user.name = "Bob"; // 오류 발생: 'name'은 읽기 전용 속성입니다.
4. Pick<T, K>
- 설명: 타입 T에서 특정 속성 K만 골라서 새 타입을 만듭니다.
- 사용 예시: 필요한 속성만 포함한 타입을 생성할 때 사용.
interface User {
id: number;
name: string;
age: number;
}
type UserNameAndAge = Pick<User, "name" | "age">;
const userInfo: UserNameAndAge = {
name: "Alice",
age: 25,
// id: 1, // 오류 발생: 'id'는 포함되지 않음
};
5. Omit<T, K>
- 설명: 타입 T에서 특정 속성 K를 제거한 타입을 만듭니다.
- 사용 예시: 불필요한 속성을 제외한 타입을 생성할 때 사용.
interface User {
id: number;
name: string;
age: number;
}
type UserWithoutAge = Omit<User, "age">;
const userInfo: UserWithoutAge = {
id: 1,
name: "Alice",
// age: 25, // 오류 발생: 'age'는 제외됨
};
6. Record<K, T>
- 설명: 키 K와 값 T로 이루어진 객체 타입을 생성합니다.
- 사용 예시: 특정 키와 값의 형태를 고정한 객체를 정의할 때 사용.
type Role = "admin" | "user" | "guest";
type RolePermissions = Record<Role, string[]>;
const permissions: RolePermissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};
7. Exclude<T, U>
- 설명: 타입 T에서 U에 해당하는 속성을 제거한 타입을 생성합니다.
- 사용 예시: 특정 값들을 제외한 타입을 만들 때 사용.
type Status = "active" | "inactive" | "banned";
type ActiveStatus = Exclude<Status, "banned">; // "active" | "inactive"
const currentStatus: ActiveStatus = "active";
8. Extract<T, U>
- 설명: 타입 T에서 U에 해당하는 속성만 추출한 타입을 생성합니다.
- 사용 예시: 특정 값들만 포함한 타입을 만들 때 사용.
type Status = "active" | "inactive" | "banned";
type InactiveStatus = Extract<Status, "inactive" | "banned">; // "inactive" | "banned"
const currentStatus: InactiveStatus = "banned";
9. NonNullable<T>
- 설명: 타입 T에서 null과 undefined를 제거한 타입을 생성합니다.
- 사용 예시: null과 undefined를 허용하지 않는 타입을 만들 때 사용.
type User = string | null | undefined;
type NonNullableUser = NonNullable<User>; // string
const user: NonNullableUser = "Alice";
// const invalidUser: NonNullableUser = null; // 오류 발생
5. 타입 안전한 상태 관리 솔루션 구현하기
복잡한 상태를 관리할 때는 TypeScript와 호환되는 Redux 또는 MobX와 같은 라이브러리를 사용하는 것이 좋습니다. 이를 통해 상태 관련 버그를 방지하고 코드의 유지 보수성을 높일 수 있습니다
// Redux Toolkit 사용 예시
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
}
const counterSlice = createSlice({
name: 'counter',
initialState: { value: 0 } as CounterState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});