TypeScript 완전 가이드: 기초부터 고급까지

TypeScript 개요 및 기본 설정

TypeScript는 Microsoft에서 개발한 JavaScript의 슈퍼셋으로, 정적 타이핑을 지원하여 대규모 애플리케이션 개발을 더욱 안정적으로 만들어줍니다.

설치 방법:

  • 전역 설치: npm install -g typescript
  • 프로젝트별 설치: npm install --save-dev typescript

기본 명령어:

  • tsc -h - 도움말 확인
  • tsc --init - tsconfig.json 설정 파일 생성
  • tsc 파일명.ts - 단일 파일 컴파일
  • tsc - tsconfig.json 설정 기반 전체 컴파일

tsconfig.json 주요 설정:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

기본 타입 시스템

TypeScript는 JavaScript의 모든 타입을 지원하며, 추가적인 타입 시스템을 제공합니다.

기본 타입들:

// 숫자 타입
let price: number = 99.99;
let count: number = 100;

// 문자열 타입
let name: string = "홍길동";
let greeting: string = `안녕하세요, ${name}님!`;

// 불리언 타입
let isActive: boolean = true;

// 배열 타입 (두 가지 표기법)
let numbers: number[] = [1, 2, 3];
let items: Array<string> = ["사과", "바나나"];

// 튜플 타입
let userInfo: [string, number, boolean] = ["김철수", 25, true];

// 열거형
enum Status {
  Pending = "PENDING",
  Approved = "APPROVED",
  Rejected = "REJECTED"
}

let currentStatus: Status = Status.Approved;

특수 타입들:

// any 타입 - 타입 검사 우회
let flexible: any = { name: "임의" };
flexible = 123;

// void 타입 - 반환값 없음
function logMessage(msg: string): void {
  console.log(msg);
}

// never 타입 - 절대 반환하지 않음
function throwError(message: string): never {
  throw new Error(message);
}

// null과 undefined
let emptyValue: null = null;
let notDefined: undefined = undefined;

타입 단언:

// 두 가지 방식
let data: any = "이것은 문자열입니다";

// 꺽쇠 괄호 방식
let length1: number = (<string>data).length;

// as 키워드 방식 (JSX와 호환)
let length2: number = (data as string).length;

인터페이스와 타입

인터페이스는 객체의 구조를 정의하는 강력한 방법입니다.

기본 인터페이스:

interface User {
  id: number;
  name: string;
  email?: string;  // 선택적 속성
  readonly createdAt: Date;  // 읽기 전용 속성
}

const user: User = {
  id: 1,
  name: "박지성",
  createdAt: new Date()
};

함수 타입 인터페이스:

interface SearchFunction {
  (query: string, page: number): string[];
}

const searchProducts: SearchFunction = function(q, p) {
  return [`제품${q}_${p}`];
};

인덱서블 타입:

interface StringMap {
  [key: string]: string;
}

const translations: StringMap = {
  hello: "안녕하세요",
  goodbye: "안녕히 가세요"
};

인터페이스 확장:

interface Employee extends User {
  department: string;
  salary: number;
}

const employee: Employee = {
  id: 2,
  name: "이영희",
  department: "개발팀",
  salary: 5000,
  createdAt: new Date()
};

클래스와 객체 지향 프로그래밍

TypeScript는 완전한 클래스 기반 객체 지향 프로그래밍을 지원합니다.

기본 클래스 정의:

class Vehicle {
  protected brand: string;
  private mileage: number = 0;
  
  constructor(brand: string) {
    this.brand = brand;
  }
  
  public drive(distance: number): void {
    this.mileage += distance;
    console.log(`${this.brand}가 ${distance}km 주행했습니다.`);
  }
  
  public getMileage(): number {
    return this.mileage;
  }
}

class Car extends Vehicle {
  private model: string;
  
  constructor(brand: string, model: string) {
    super(brand);
    this.model = model;
  }
  
  public showDetails(): void {
    console.log(`차종: ${this.brand} ${this.model}`);
  }
}

접근자(Getter/Setter):

class Product {
  private _price: number = 0;
  
  get price(): number {
    return this._price;
  }
  
  set price(newPrice: number) {
    if (newPrice > 0) {
      this._price = newPrice;
    } else {
      console.log("가격은 0보다 커야 합니다.");
    }
  }
}

const product = new Product();
product.price = 1000;
console.log(product.price);

추상 클래스:

abstract class Shape {
  abstract calculateArea(): number;
  
  display(): void {
    console.log(`면적: ${this.calculateArea()}`);
  }
}

class Rectangle extends Shape {
  constructor(private width: number, private height: number) {
    super();
  }
  
  calculateArea(): number {
    return this.width * this.height;
  }
}

함수 타입과 고급 기능

TypeScript에서 함수는 더 엄격한 타입 검사와 고급 기능을 제공합니다.

함수 타입 정의:

// 함수 선언
function addNumbers(a: number, b: number): number {
  return a + b;
}

// 함수 표현식
const multiply = (a: number, b: number): number => a * b;

// 함수 타입 별칭
type MathOperation = (a: number, b: number) => number;

const divide: MathOperation = (a, b) => a / b;

선택적 매개변수와 기본값:

function createUser(
  name: string,
  age: number = 20,
  role?: string
): { name: string; age: number; role?: string } {
  return { name, age, role };
}

const user1 = createUser("김민준"); // { name: "김민준", age: 20 }
const user2 = createUser("이서연", 30, "관리자");

나머지 매개변수:

function formatMessage(template: string, ...values: any[]): string {
  return template.replace(/{(\d+)}/g, (match, index) => {
    return values[index] || match;
  });
}

console.log(formatMessage("{0}님, {1}에 오신 것을 환영합니다!", "손님", "2024년"));

함수 오버로딩:

function processInput(input: string): string;
function processInput(input: number): number;
function processInput(input: boolean): boolean;
function processInput(input: string | number | boolean): string | number | boolean {
  if (typeof input === "string") {
    return input.toUpperCase();
  } else if (typeof input === "number") {
    return input * 2;
  } else {
    return !input;
  }
}

console.log(processInput("hello")); // "HELLO"
console.log(processInput(5)); // 10
console.log(processInput(true)); // false

제네릭 프로그래밍

제네릭은 타입을 매개변수처럼 사용하여 재사용 가능한 코드를 작성할 수 있게 합니다.

제네릭 함수:

function createArray<T>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

const numberArray = createArray(3, 0); // number[]
const stringArray = createArray(3, "default"); // string[]

function identity<T>(arg: T): T {
  return arg;
}

const result = identity<string>("제네릭 테스트");

제네릭 인터페이스:

interface Repository<T> {
  findById(id: number): T | null;
  save(entity: T): T;
  findAll(): T[];
}

class UserRepository implements Repository<User> {
  private users: User[] = [];
  
  findById(id: number): User | null {
    return this.users.find(u => u.id === id) || null;
  }
  
  save(user: User): User {
    this.users.push(user);
    return user;
  }
  
  findAll(): User[] {
    return this.users;
  }
}

제네릭 제약 조건:

interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): void {
  console.log(`길이: ${arg.length}`);
}

logLength("문자열"); // 가능
logLength([1, 2, 3]); // 가능
// logLength(123); // 에러: 숫자는 length 속성이 없음

keyof 제약 조건:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const person = { name: "홍길동", age: 30 };
console.log(getProperty(person, "name")); // "홍길동"
// console.log(getProperty(person, "email")); // 에러: email 속성 없음

타입 조작과 고급 타입

TypeScript는 타입을 조작하고 결합하는 다양한 고급 기능을 제공합니다.

유니언 타입과 교차 타입:

// 유니언 타입 (OR)
type ID = string | number;
let userId: ID = "user123";
userId = 456; // 둘 다 가능

// 교차 타입 (AND)
interface Person {
  name: string;
}

interface Employee {
  id: number;
  department: string;
}

type EmployeePerson = Person & Employee;

const employee: EmployeePerson = {
  name: "김사원",
  id: 100,
  department: "개발팀"
};

타입 가드:

function processValue(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // 여기서 value는 string 타입
  } else {
    return value.toFixed(2); // 여기서 value는 number 타입
  }
}

// 사용자 정의 타입 가드
interface Cat {
  meow(): void;
}

interface Dog {
  bark(): void;
}

function isCat(animal: Cat | Dog): animal is Cat {
  return (animal as Cat).meow !== undefined;
}

매핑 타입:

type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type ReadonlyTodo = Readonly<Todo>;
type PartialTodo = Optional<Todo>;

조건부 타입:

type NonNullable<T> = T extends null | undefined ? never : T;

type ExtractType<T, U> = T extends U ? T : never;

type Test1 = NonNullable<string | null>; // string
type Test2 = ExtractType<string | number, string>; // string

열거형(Enums) 심화

TypeScript의 열거형은 더 구조化的된 상수 집합을 정의할 수 있습니다.

숫자 열거형:

enum Direction {
  North,  // 0
  South,  // 1
  East,   // 2
  West    // 3
}

// 값 초기화
enum StatusCode {
  Success = 200,
  NotFound = 404,
  Error = 500
}

// 역 매핑 지원
console.log(Direction.North); // 0
console.log(Direction[0]); // "North"

문자열 열거형:

enum LogLevel {
  ERROR = "ERROR",
  WARN = "WARN",
  INFO = "INFO",
  DEBUG = "DEBUG"
}

function log(level: LogLevel, message: string): void {
  console.log(`[${level}]: ${message}`);
}

log(LogLevel.ERROR, "오류 발생!");

상수 열거형:

const enum Colors {
  Red = "#FF0000",
  Green = "#00FF00",
  Blue = "#0000FF"
}

// 컴파일 시 값으로 치환됨
let backgroundColor = Colors.Blue;

모듈과 네임스페이스

대규모 애플리케이션에서 코드를 구성하는 방법입니다.

ES6 모듈:

// mathUtils.ts
export const PI = 3.14159;

export function calculateCircleArea(radius: number): number {
  return PI * radius * radius;
}

export class Calculator {
  add(a: number, b: number): number {
    return a + b;
  }
}

// main.ts
import { PI, calculateCircleArea, Calculator } from "./mathUtils";

console.log(PI);
console.log(calculateCircleArea(5));

const calc = new Calculator();
console.log(calc.add(10, 20));

네임스페이스:

namespace MyApplication {
  export interface Config {
    apiUrl: string;
    timeout: number;
  }
  
  export class ApiClient {
    constructor(private config: Config) {}
    
    request(endpoint: string): void {
      console.log(`Requesting ${this.config.apiUrl}/${endpoint}`);
    }
  }
}

// 사용
const client = new MyApplication.ApiClient({
  apiUrl: "https://api.example.com",
  timeout: 5000
});

client.request("/users");

타입 호환성

TypeScript의 구조적 타이핑 시스템 이해하기.

구조적 타이핑:

interface Point {
  x: number;
  y: number;
}

interface Point3D {
  x: number;
  y: number;
  z: number;
}

let point2D: Point = { x: 0, y: 0 };
let point3D: Point3D = { x: 0, y: 0, z: 0 };

// Point3D는 Point 호환 (추가 속성 허용)
point2D = point3D; // 가능
// point3D = point2D; // 에러: z 속성 없음

함수 호환성:

let fn1 = (a: number, b: number) => a + b;
let fn2 = (a: number) => a;

// fn2는 fn1 호환 (매개변수 수 적음 허용)
fn1 = fn2; // 가능
// fn2 = fn1; // 에러: 매개변수 수 불일치

제네릭 호환성:

interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;

x = y; // 가능: 구조가 동일

디코레이터와 메타데이터

메타 프로그래밍을 위한 고급 기능입니다.

클래스 디코레이터:

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
}

메서드 디코레이터:

function log(target: any, propertyName: string, descriptor: PropertyDescriptor) {
  const method = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    console.log(`호출: ${propertyName} with args: ${JSON.stringify(args)}`);
    const result = method.apply(this, args);
    console.log(`결과: ${result}`);
    return result;
  };
  
  return descriptor;
}

class Calculator {
  @log
  multiply(a: number, b: number): number {
    return a * b;
  }
}

선언 병합

기존 타입을 확장하는 방법입니다.

인터페이스 병합:

interface User {
  name: string;
}

interface User {
  age: number;
}

// 두 인터페이스가 병합됨
const user: User = {
  name: "홍길동",
  age: 30
};

네임스페이스 병합:

namespace Validation {
  export const isEmailValid = (email: string): boolean => {
    return email.includes("@");
  };
}

namespace Validation {
  export const isPhoneValid = (phone: string): boolean => {
    return /^\d{3}-\d{4}-\d{4}$/.test(phone);
  };
}

// Validation.isEmailValid와 Validation.isPhoneValid 모두 사용 가능

실용적인 팁과 모범 사례

1. 엄격 모드 사용:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true
  }
}

2. 타입 좁히기 활용:

function process(data: string | number | null): void {
  if (data === null) {
    console.log("데이터 없음");
    return;
  }
  
  if (typeof data === "string") {
    console.log(data.toUpperCase());
  } else {
    console.log(data.toFixed(2));
  }
}

3. 유틸리티 타입 활용:

interface UserProfile {
  id: number;
  name: string;
  email: string;
  age: number;
  address: {
    street: string;
    city: string;
  };
}

// 필요한 속성만 선택
type UserSummary = Pick<UserProfile, "id" | "name">;

// 특정 속성 제외
type UserWithoutAddress = Omit<UserProfile, "address">;

4. 타입 추론 활용:

// 명시적 타입보다 추론에 의존
const products = [
  { id: 1, name: "노트북", price: 1000 },
  { id: 2, name: "마우스", price: 50 }
]; // 타입이 자동으로 추출됨

type Product = typeof products[number]; // 타입 별칭으로 재사용

JavaScript 상식 보충

변수 호이스팅:

console.log(x); // undefined (에러 아님)
var x = 5;

// 실제 동작:
var x; // 선언이 호이스팅됨
console.log(x);
x = 5;

함수 호이스팅:

hoisted(); // "호이스팅된 함수"

function hoisted() {
  console.log("호이스팅된 함수");
}

// 화살표 함수는 호이스팅되지 않음
// notHoisted(); // 에러
const notHoisted = () => console.log("호이스팅 안됨");

this 바인딩:

const obj = {
  value: 42,
  regularMethod: function() {
    console.log(this.value); // this는 obj
  },
  
  arrowMethod: () => {
    console.log(this.value); // this는 전역 객체
  }
};

obj.regularMethod(); // 42
obj.arrowMethod(); // undefined

클로저:

function createCounter() {
  let count = 0;
  
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2

9월 22일 15:53에 게시됨