TypeScript 실무 개발 가이드 (2025)

반응형

TypeScript 실무 개발 가이드 (2025)

이 문서 하나만 보면 TypeScript를 제대로 사용할 수 있도록 핵심만 정리한 실용적인 가이드입니다.

📋 목차

  1. 환경 설정
  2. 핵심 타입 시스템
  3. 인터페이스와 타입 정의
  4. 제네릭 활용
  5. 타입 가드와 안전성
  6. 유틸리티 타입
  7. 고급 타입 패턴
  8. React와 함께 사용하기
  9. 실무 베스트 프랙티스
  10. 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);
  }
}

📚 추가 학습 리소스

공식 문서와 도구

실무에서 자주 설치하는 타입 패키지

# 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 개발 시 참고하세요!

반응형

댓글

Designed by JB FACTORY