TypeScript 실무 개발 가이드 (2025)
- TYPESCRIPT
- 2025. 9. 28.
반응형
TypeScript 실무 개발 가이드 (2025)
이 문서 하나만 보면 TypeScript를 제대로 사용할 수 있도록 핵심만 정리한 실용적인 가이드입니다.
📋 목차
- 환경 설정
- 핵심 타입 시스템
- 인터페이스와 타입 정의
- 제네릭 활용
- 타입 가드와 안전성
- 유틸리티 타입
- 고급 타입 패턴
- React와 함께 사용하기
- 실무 베스트 프랙티스
- 2025년 최신 기능
🔧 환경 설정
프로젝트 시작하기
# 새 프로젝트 생성
npm init -y
npm install typescript @types/node
npx tsc --init
# 글로벌 설치 (IDE 지원용)
npm install -g typescript
필수 tsconfig.json 설정
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
🎯 핵심 타입 시스템
기본 타입 (매일 사용)
// 원시 타입
const name: string = "김개발";
const age: number = 30;
const isActive: boolean = true;
const nothing: null = null;
const notDefined: undefined = undefined;
// 배열과 튜플
const numbers: number[] = [1, 2, 3];
const fruits: Array<string> = ["apple", "banana"];
const coordinates: [number, number] = [10, 20];
// 리터럴 타입 (매우 유용!)
type Status = "pending" | "completed" | "failed";
type Theme = "light" | "dark";
const currentStatus: Status = "pending";
Union과 Intersection 타입
// Union 타입 (OR 관계)
type StringOrNumber = string | number;
type RequestState = "idle" | "loading" | "success" | "error";
// Intersection 타입 (AND 관계)
type User = {
id: string;
name: string;
};
type Admin = {
permissions: string[];
};
type AdminUser = User & Admin; // 두 타입 모두 포함
// 실무 예시
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
}
return value.toFixed(2);
}
🎨 인터페이스와 타입 정의
인터페이스 (객체 모델링의 핵심)
// 기본 인터페이스
interface User {
readonly id: string; // 읽기 전용
name: string;
email?: string; // 선택적 속성
age: number;
[key: string]: any; // 인덱스 시그니처
}
// 확장 가능
interface AdminUser extends User {
permissions: string[];
lastLogin: Date;
}
// 함수 인터페이스
interface EventHandler<T> {
(event: T): void;
}
// 실무 API 응답 모델링
interface ApiResponse<T> {
data: T;
status: number;
message: string;
success: boolean;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
};
}
Type Alias vs Interface
// Type - 더 유연함
type ButtonSize = "small" | "medium" | "large";
type APIStatus = 200 | 404 | 500;
// Interface - 확장성이 좋음 (선택의 기준: 확장 필요성)
interface ButtonProps {
size: ButtonSize;
variant: "primary" | "secondary";
children: React.ReactNode;
}
// 함수 타입
type AsyncFunction<T> = () => Promise<T>;
type EventCallback = (data: any) => void;
🔄 제네릭 활용
기본 제네릭 (재사용성의 핵심)
// 기본 제네릭 함수
function identity<T>(arg: T): T {
return arg;
}
// 실무에서 자주 사용하는 패턴
function createApiCall<TRequest, TResponse>(
url: string,
transform?: (data: any) => TResponse
) {
return async (data: TRequest): Promise<TResponse> => {
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(data)
});
const result = await response.json();
return transform ? transform(result) : result;
};
}
// 사용 예시
const loginApi = createApiCall<
{ email: string; password: string },
{ token: string; user: User }
>('/api/login');
제네릭 제약조건
// keyof로 객체 키 제한
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 타입 제약
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
// 실무 예시: 폼 데이터 처리
interface FormField {
value: any;
error?: string;
touched: boolean;
}
function createFormState<T extends Record<string, any>>(
initialData: T
): Record<keyof T, FormField> {
const formState = {} as Record<keyof T, FormField>;
for (const key in initialData) {
formState[key] = {
value: initialData[key],
touched: false
};
}
return formState;
}
🛡️ 타입 가드와 안전성
내장 타입 가드
// typeof 가드 (원시 타입용)
function processValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // TS가 string임을 알고 있음
}
return value.toFixed(2); // TS가 number임을 알고 있음
}
// instanceof 가드 (클래스용)
class NetworkError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
function handleError(error: Error | NetworkError) {
if (error instanceof NetworkError) {
console.log(`Network error: ${error.statusCode}`);
} else {
console.log(`General error: ${error.message}`);
}
}
// in 연산자 (객체 속성용)
interface Bird {
fly(): void;
}
interface Fish {
swim(): void;
}
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly();
} else {
animal.swim();
}
}
커스텀 타입 가드 (매우 유용!)
// 사용자 정의 타입 가드
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isUser(obj: any): obj is User {
return obj &&
typeof obj.id === "string" &&
typeof obj.name === "string";
}
// 실무 예시: API 응답 검증
function isApiSuccess<T>(response: any): response is ApiResponse<T> {
return response &&
response.success === true &&
response.data !== undefined;
}
// 사용
async function fetchUserData(id: string) {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
if (isApiSuccess<User>(data)) {
// data.data는 User 타입으로 안전하게 사용 가능
return data.data;
}
throw new Error("Invalid API response");
}
🛠️ 유틸리티 타입
내장 유틸리티 타입 (필수!)
interface Todo {
id: string;
title: string;
completed: boolean;
priority: "high" | "medium" | "low";
createdAt: Date;
}
// Partial - 모든 속성을 선택적으로
type TodoUpdate = Partial<Todo>;
function updateTodo(id: string, updates: TodoUpdate) {
// 일부 필드만 업데이트 가능
}
// Pick - 특정 속성만 선택
type TodoMetadata = Pick<Todo, "id" | "title" | "priority">;
// Omit - 특정 속성 제외
type CreateTodoRequest = Omit<Todo, "id" | "createdAt">;
// Record - 객체 타입 생성
type TodoStatus = "pending" | "in-progress" | "completed";
type StatusColors = Record<TodoStatus, string>;
const colors: StatusColors = {
"pending": "#yellow",
"in-progress": "#blue",
"completed": "#green"
};
// Required - 모든 속성 필수로
type RequiredTodo = Required<Todo>;
// Readonly - 모든 속성 읽기 전용
type ImmutableTodo = Readonly<Todo>;
커스텀 유틸리티 타입
// 깊은 부분 업데이트
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// 함수에서 Promise 타입 추출
type Awaited<T> = T extends Promise<infer U> ? U : T;
// 객체의 값 타입들만 추출
type ValueOf<T> = T[keyof T];
// 실무 예시: 폼 에러 타입
type FormErrors<T> = {
[K in keyof T]?: string;
};
interface LoginForm {
email: string;
password: string;
}
const errors: FormErrors<LoginForm> = {
email: "이메일이 유효하지 않습니다",
password: "비밀번호는 최소 8자 이상이어야 합니다"
};
🔥 고급 타입 패턴
조건부 타입
// 기본 조건부 타입
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
// infer로 타입 추론
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
type FunctionReturn = ReturnType<() => string>; // string
// 실무 예시: API 응답 타입 추출
type ExtractApiData<T> = T extends ApiResponse<infer U> ? U : never;
type UserData = ExtractApiData<ApiResponse<User>>; // User
// 배열 요소 타입 추출
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type StringElement = ArrayElement<string[]>; // string
맵드 타입
// 기본 맵드 타입
type Optional<T> = {
[P in keyof T]?: T[P];
};
// 수정자 활용
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
// 실무 예시: 폼 검증 스키마 생성
type ValidationSchema<T> = {
[K in keyof T]: {
required?: boolean;
validator?: (value: T[K]) => boolean;
message?: string;
};
};
const userSchema: ValidationSchema<User> = {
name: {
required: true,
validator: (value) => value.length > 0,
message: "이름은 필수입니다"
},
email: {
validator: (value) => value?.includes("@") || false,
message: "올바른 이메일 형식이 아닙니다"
}
};
템플릿 리터럴 타입 (2025년 활용도 증가)
// CSS 속성 자동완성
type Direction = "top" | "right" | "bottom" | "left";
type Margin = `margin-${Direction}`;
// "margin-top" | "margin-right" | "margin-bottom" | "margin-left"
// API 엔드포인트 타입 안전성
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiEndpoint = `/api/${string}`;
type ApiCall = `${HttpMethod} ${ApiEndpoint}`;
// 실무 예시: 이벤트 이름 생성
type EventName<T extends string> = `on${Capitalize<T>}`;
type ButtonEvents = EventName<"click" | "hover" | "focus">;
// "onClick" | "onHover" | "onFocus"
// 경로 파라미터 추출
type ExtractPathParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractPathParams<Rest>
: T extends `${string}:${infer Param}`
? Param
: never;
type UserPath = "/users/:id/posts/:postId";
type Params = ExtractPathParams<UserPath>; // "id" | "postId"
⚛️ React와 함께 사용하기
컴포넌트 타입 정의
import React, { useState, useEffect, ReactNode } from 'react';
// Props 인터페이스
interface ButtonProps {
variant: "primary" | "secondary" | "danger";
size?: "sm" | "md" | "lg";
disabled?: boolean;
children: ReactNode;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
// 함수형 컴포넌트
const Button: React.FC<ButtonProps> = ({
variant,
size = "md",
disabled = false,
children,
onClick
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};
// 제네릭 컴포넌트
interface DropdownProps<T> {
items: T[];
value?: T;
onChange: (item: T) => void;
renderItem: (item: T) => ReactNode;
getKey: (item: T) => string;
}
function Dropdown<T>({ items, value, onChange, renderItem, getKey }: DropdownProps<T>) {
return (
<div className="dropdown">
{items.map(item => (
<div
key={getKey(item)}
onClick={() => onChange(item)}
className={value === item ? "selected" : ""}
>
{renderItem(item)}
</div>
))}
</div>
);
}
훅 타입 정의
// useState 타입 추론 활용
const [count, setCount] = useState(0); // number로 추론
const [user, setUser] = useState<User | null>(null); // 명시적 타입
// useEffect 의존성 배열 타입 안전성
useEffect(() => {
fetchUserData(user?.id);
}, [user?.id]); // user가 변경될 때만 실행
// 커스텀 훅 타입 정의
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
function useApi<T>(url: string): UseApiResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
return {
data,
loading,
error,
refetch: fetchData
};
}
// 사용 예시
const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
const { data: user, loading, error } = useApi<User>(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>User not found</div>;
return <div>Welcome, {user.name}!</div>;
};
이벤트 핸들링
// 일반적인 이벤트 타입들
type ClickHandler = (event: React.MouseEvent<HTMLButtonElement>) => void;
type ChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => void;
type SubmitHandler = (event: React.FormEvent<HTMLFormElement>) => void;
// 폼 컴포넌트 예시
interface FormData {
email: string;
password: string;
}
const LoginForm: React.FC = () => {
const [formData, setFormData] = useState<FormData>({
email: '',
password: ''
});
const handleInputChange: ChangeHandler = (event) => {
const { name, value } = event.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit: SubmitHandler = (event) => {
event.preventDefault();
// 폼 제출 로직
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
/>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
/>
<button type="submit">Login</button>
</form>
);
};
💡 실무 베스트 프랙티스
1. 타입 네이밍 컨벤션
// 좋은 네이밍
interface User {} // 파스칼 케이스
type ApiStatus = "loading" | "success" | "error"; // 파스칼 케이스
type UserRole = "admin" | "user"; // 의미있는 이름
// 제네릭 타입 매개변수
// T - Type, U - Another Type, K - Key, V - Value, P - Property
function transform<TInput, TOutput>(input: TInput): TOutput {
// ...
}
// Props와 State 접미사 사용
interface ButtonProps {} // 컴포넌트 Props
interface AppState {} // 상태 타입
2. 타입 구성 전략
// 타입 파일 구조 (types/index.ts)
export interface User {
id: string;
name: string;
email: string;
}
export interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
export type UserResponse = ApiResponse<User>;
export type UsersResponse = ApiResponse<User[]>;
// 도메인별 타입 분리
// types/user.ts
// types/product.ts
// types/api.ts
3. 에러 처리 패턴
// Result 패턴
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function safeApiCall<T>(url: string): Promise<Result<T>> {
try {
const response = await fetch(url);
const data = await response.json();
return { success: true, data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Unknown error')
};
}
}
// 사용
const result = await safeApiCall<User>('/api/user');
if (result.success) {
console.log(result.data.name); // 타입 안전
} else {
console.error(result.error.message);
}
4. 환경 변수와 설정
// env.ts
interface Environment {
API_URL: string;
DEBUG: boolean;
VERSION: string;
}
declare global {
namespace NodeJS {
interface ProcessEnv extends Environment {}
}
}
// 타입 안전한 환경 변수 접근
export const config = {
apiUrl: process.env.API_URL || 'http://localhost:3000',
debug: process.env.DEBUG === 'true',
version: process.env.VERSION || '1.0.0'
};
5. 상태 관리 타이핑
// Redux Toolkit과 함께
interface UserState {
currentUser: User | null;
loading: boolean;
error: string | null;
}
interface AppState {
user: UserState;
posts: PostState;
ui: UIState;
}
// Zustand와 함께
interface UserStore {
user: User | null;
setUser: (user: User) => void;
clearUser: () => void;
isLoggedIn: () => boolean;
}
const useUserStore = create<UserStore>((set, get) => ({
user: null,
setUser: (user) => set({ user }),
clearUser: () => set({ user: null }),
isLoggedIn: () => get().user !== null
}));
🚀 2025년 최신 기능
1. Native TypeScript Compiler (TypeScript 7.0 예정)
// 10배 빠른 컴파일 속도
// 50% 적은 메모리 사용량
// 8배 빠른 에디터 로딩
// 사용법은 동일하지만 성능이 크게 향상
npm install typescript@next
2. 향상된 타입 추론
// 더 정확한 제네릭 추론
function createArray<T>(items: T[]) {
return items;
}
// 이제 더 정확하게 추론됨
const mixedArray = createArray([1, "hello", true]);
// (string | number | boolean)[]
3. 새로운 유틸리티 타입
// Satisfies 연산자로 더 나은 타입 체크
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3
} satisfies Config;
// Using import defer (ES2025)
import defer * as utils from './utils';
// 모듈을 지연 로딩하여 성능 향상
4. 개선된 에러 메시지
// 2025년 버전에서는 더 친화적인 에러 메시지
interface User {
name: string;
age: number;
}
const user: User = {
name: "John"
// Error: Property 'age' is missing in type
// 💡 Suggestion: Add 'age: number' to fix this error
};
⚡ 퀵 레퍼런스
자주 사용하는 패턴들
// 1. Optional Chaining과 Nullish Coalescing
const userName = user?.profile?.name ?? "Unknown";
// 2. Assertion Functions
function assertIsNumber(value: any): asserts value is number {
if (typeof value !== "number") {
throw new Error("Value must be number");
}
}
// 3. Branded Types (더 엄격한 타입)
type UserId = string & { __brand: "UserId" };
type ProductId = string & { __brand: "ProductId" };
function createUserId(id: string): UserId {
return id as UserId;
}
// 4. 함수 오버로딩
function createElement(tag: "button"): HTMLButtonElement;
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
// 5. 조건부 속성
interface BaseConfig {
mode: "development" | "production";
}
interface DevelopmentConfig extends BaseConfig {
mode: "development";
debugTools: boolean;
}
interface ProductionConfig extends BaseConfig {
mode: "production";
optimizations: string[];
}
type Config = DevelopmentConfig | ProductionConfig;
디버깅과 개발 도구
// 타입 확인용 유틸리티
type Debug<T> = { [K in keyof T]: T[K] };
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// 컴파일 타임에 타입 테스트
type Assert<T extends true> = T;
type Test1 = Assert<true>; // ✅
// type Test2 = Assert<false>; // ❌ 컴파일 에러
// 런타임 타입 체크 (개발 환경)
const isDev = process.env.NODE_ENV === "development";
function devAssert(condition: any, message: string): asserts condition {
if (isDev && !condition) {
throw new Error(message);
}
}
📚 추가 학습 리소스
공식 문서와 도구
- TypeScript 공식 문서
- TypeScript Playground - 온라인 테스트
- DefinitelyTyped - 타입 정의 라이브러리
실무에서 자주 설치하는 타입 패키지
# React 관련
npm install @types/react @types/react-dom
# Node.js 관련
npm install @types/node
# 유틸리티 라이브러리
npm install @types/lodash @types/uuid
# 테스팅
npm install @types/jest @types/testing-library__react
성능 최적화 팁
# 컴파일 속도 향상
npx tsc --incremental # 증분 컴파일
npx tsc --watch # 파일 변경 감시
# 타입 체크만 (JS 생성 안함)
npx tsc --noEmit
# 프로젝트 레퍼런스 사용 (큰 프로젝트)
# tsconfig.json에 references 설정
📖 이 가이드는 실무에서 바로 사용할 수 있는 TypeScript 핵심 패턴들을 담았습니다. 처음에는 기본 타입부터 시작해서 점진적으로 고급 기능을 도입하는 것을 추천합니다.
🔄 정기적으로 업데이트되는 이 문서를 북마크하여 TypeScript 개발 시 참고하세요!
반응형
'TYPESCRIPT' 카테고리의 다른 글
| map() 메서드에서는 객체의 얕은 복사가 이루어지므로 리턴된 배열의 각 객체는 원본 배열의 참조를 그대로 가집니다. (0) | 2024.11.10 |
|---|---|
| 우아한 타입스크립트 목차 (0) | 2024.09.10 |
| 타입스크립트에서 구조적 타이핑으로 인해 유니온 타입의 논리가 조금 이상한데 구조적 타이핑, 덕 타아핑의 한계로 인해 어쩔 수 없는 것인가요? (0) | 2024.08.26 |
| 타입스크립트의 고급 타이핑 기능들이 어떻게 보면 더 발전된 언어의 기능이라고 볼 수도 있지 않나요? 단순히 동적언어인 자바스크립트에 타이핑을 하기 위해 추가된 기능들인 건가요? (1) | 2024.08.26 |
| 타입스크립트의 타입 시스템은 타입에 대해 동작하는 순수 함수형 언어로 생각할 수 있습니다. (0) | 2024.08.26 |