카테고리 없음

타입스크립트 클래스 심층 탐구

ai-one 2025. 4. 25. 13:04

안녕하세요! 타입스크립트 시리즈 네 번째 시간입니다. 오늘은 타입스크립트에서 객체 지향 프로그래밍의 핵심인 **클래스(Class)**에 대해 자세히 알아보겠습니다. ES6에서 도입된 자바스크립트 클래스를 타입스크립트는 더욱 강력하게 확장합니다.

기본 클래스 구문

타입스크립트에서 기본적인 클래스 선언은 다음과 같습니다:

class Person {
  // 프로퍼티
  name: string;
  age: number;

  // 생성자
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  // 메서드
  greet(): string {
    return `안녕하세요, 제 이름은 ${this.name}이고 ${this.age}살입니다.`;
  }
}

// 클래스 인스턴스 생성
const person = new Person("홍길동", 30);
console.log(person.greet()); // "안녕하세요, 제 이름은 홍길동이고 30살입니다."

접근 제한자(Access Modifiers)

타입스크립트는 세 가지 접근 제한자를 제공합니다:

  1. public: 어디서나 접근 가능 (기본값)
  2. private: 클래스 내부에서만 접근 가능
  3. protected: 클래스 내부와 상속받은 하위 클래스에서 접근 가능
class Employee {
  public name: string;      // 어디서나 접근 가능
  private salary: number;   // 클래스 내부에서만 접근 가능
  protected department: string; // 상속받은 클래스에서도 접근 가능

  constructor(name: string, salary: number, department: string) {
    this.name = name;
    this.salary = salary;
    this.department = department;
  }

  // private 멤버에 접근하는 public 메서드
  public getSalary(): number {
    return this.salary;
  }
}

const employee = new Employee("김영희", 5000000, "개발");
console.log(employee.name);       // "김영희" - public이므로 접근 가능
// console.log(employee.salary);  // 오류: 'salary'는 private이므로 'Employee' 클래스 외부에서 접근할 수 없습니다.
// console.log(employee.department); // 오류: 'department'는 protected이므로 'Employee' 클래스 및 하위 클래스 외부에서 접근할 수 없습니다.
console.log(employee.getSalary()); // 5000000 - public 메서드를 통해 접근 가능

생성자 매개변수 약식 표기

클래스의 프로퍼티를 선언하고 생성자에서 할당하는 패턴은 매우 일반적이어서, 타입스크립트는 이를 간결하게 표현할 수 있는 방법을 제공합니다:

// 약식 표기를 사용한 동일한 Employee 클래스
class Employee {
  constructor(
    public name: string,
    private salary: number,
    protected department: string
  ) {
    // 자동으로 프로퍼티 선언 및 초기화됨
  }

  public getSalary(): number {
    return this.salary;
  }
}

읽기 전용 프로퍼티(Readonly Properties)

초기화 후 변경할 수 없는 프로퍼티를 선언할 수 있습니다:

class Car {
  readonly model: string;
  readonly brand: string;
  color: string;

  constructor(brand: string, model: string, color: string) {
    this.brand = brand;
    this.model = model;
    this.color = color;
  }

  changeColor(newColor: string): void {
    this.color = newColor; // 가능
    // this.model = "New Model"; // 오류: readonly 프로퍼티에 할당할 수 없음
  }
}

const myCar = new Car("Tesla", "Model 3", "red");
myCar.color = "blue"; // 가능
// myCar.brand = "BMW"; // 오류: readonly 프로퍼티를 변경할 수 없음

 

상속(Inheritance)

클래스는 다른 클래스를 상속받아 기능을 확장할 수 있습니다:

class Animal {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  makeSound(): void {
    console.log("일반적인 동물 소리");
  }
}

class Dog extends Animal {
  breed: string;
  
  constructor(name: string, breed: string) {
    super(name); // 부모 클래스의 생성자 호출
    this.breed = breed;
  }
  
  // 메서드 오버라이딩
  makeSound(): void {
    console.log("멍멍!");
  }
  
  // 추가 메서드
  fetch(): void {
    console.log(`${this.name}이(가) 물건을 가져옵니다!`);
  }
}

const dog = new Dog("바둑이", "진돗개");
console.log(dog.name);  // "바둑이"
console.log(dog.breed); // "진돗개"
dog.makeSound();        // "멍멍!"
dog.fetch();            // "바둑이이(가) 물건을 가져옵니다!"

추상 클래스(Abstract Classes)

추상 클래스는 직접 인스턴스화할 수 없고, 다른 클래스가 상속받아 구현해야 하는 기본 클래스입니다:

abstract class Shape {
  color: string;
  
  constructor(color: string) {
    this.color = color;
  }
  
  // 추상 메서드 - 하위 클래스에서 반드시 구현해야 함
  abstract calculateArea(): number;
  
  // 일반 메서드
  displayColor(): void {
    console.log(`이 도형의 색상은 ${this.color}입니다.`);
  }
}

class Circle extends Shape {
  radius: number;
  
  constructor(color: string, radius: number) {
    super(color);
    this.radius = radius;
  }
  
  // 추상 메서드 구현
  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

// const shape = new Shape("red"); // 오류: 추상 클래스는 인스턴스화할 수 없음
const circle = new Circle("빨강", 5);
console.log(circle.calculateArea()); // 약 78.54
circle.displayColor();              // "이 도형의 색상은 빨강입니다."

인터페이스 구현(Implementing Interfaces)

클래스는 인터페이스를 구현하여 특정 구조를 준수하도록 강제할 수 있습니다:

interface Movable {
  speed: number;
  move(): void;
}

interface Resizable {
  resize(width: number, height: number): void;
}

// 여러 인터페이스 구현
class GameCharacter implements Movable, Resizable {
  name: string;
  speed: number;
  width: number;
  height: number;
  
  constructor(name: string, speed: number, width: number, height: number) {
    this.name = name;
    this.speed = speed;
    this.width = width;
    this.height = height;
  }
  
  move(): void {
    console.log(`${this.name}이(가) ${this.speed}의 속도로 이동합니다.`);
  }
  
  resize(width: number, height: number): void {
    this.width = width;
    this.height = height;
    console.log(`${this.name}의 크기가 ${width}x${height}로 변경되었습니다.`);
  }
}

const character = new GameCharacter("전사", 10, 50, 100);
character.move();           // "전사이(가) 10의 속도로 이동합니다."
character.resize(60, 120);  // "전사의 크기가 60x120로 변경되었습니다."

정적 프로퍼티와 메서드(Static Properties and Methods)

인스턴스가 아닌 클래스 자체에 속하는 프로퍼티와 메서드를 정의할 수 있습니다:

class MathUtils {
  // 정적 프로퍼티
  static PI: number = 3.14159;
  
  // 정적 메서드
  static add(x: number, y: number): number {
    return x + y;
  }
  
  static calculateCircleArea(radius: number): number {
    return MathUtils.PI * radius * radius;
  }
}

console.log(MathUtils.PI);  // 3.14159
console.log(MathUtils.add(5, 3));  // 8
console.log(MathUtils.calculateCircleArea(5));  // 약 78.54

게터와 세터(Getters and Setters)

프로퍼티 접근을 제어하기 위한 게터와 세터를 정의할 수 있습니다:

class BankAccount {
  private _balance: number;
  
  constructor(initialBalance: number = 0) {
    this._balance = initialBalance;
  }
  
  // 게터
  get balance(): number {
    return this._balance;
  }
  
  // 세터
  set balance(amount: number) {
    if (amount < 0) {
      throw new Error("잔액은 0보다 작을 수 없습니다.");
    }
    this._balance = amount;
  }
  
  // 일반 메서드
  deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error("입금액은 0보다 커야 합니다.");
    }
    this._balance += amount;
  }
  
  withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error("출금액은 0보다 커야 합니다.");
    }
    if (amount > this._balance) {
      throw new Error("잔액이 부족합니다.");
    }
    this._balance -= amount;
  }
}

const account = new BankAccount(1000);
console.log(account.balance);  // 1000 - 게터를 통해 접근
account.balance = 2000;        // 세터를 통해 설정
console.log(account.balance);  // 2000
account.deposit(500);
console.log(account.balance);  // 2500
account.withdraw(1000);
console.log(account.balance);  // 1500
// account.balance = -100;     // 오류: "잔액은 0보다 작을 수 없습니다."

제네릭 클래스(Generic Classes)

다양한 타입에 대해 재사용 가능한 클래스를 만들 수 있습니다:

class Queue<T> {
  private items: T[] = [];
  
  enqueue(item: T): void {
    this.items.push(item);
  }
  
  dequeue(): T | undefined {
    return this.items.shift();
  }
  
  peek(): T | undefined {
    return this.items[0];
  }
  
  get length(): number {
    return this.items.length;
  }
}

// 문자열 큐
const stringQueue = new Queue<string>();
stringQueue.enqueue("Hello");
stringQueue.enqueue("TypeScript");
console.log(stringQueue.dequeue());  // "Hello"
console.log(stringQueue.length);     // 1

// 숫자 큐
const numberQueue = new Queue<number>();
numberQueue.enqueue(10);
numberQueue.enqueue(20);
console.log(numberQueue.peek());     // 10
console.log(numberQueue.length);     // 2

믹스인(Mixins)

타입스크립트에서는 여러 클래스의 기능을 하나의 클래스로 결합하는 믹스인 패턴을 구현할 수 있습니다:

// 믹스인 클래스들
class Timestamped {
  timestamp = Date.now();
}

class Activatable {
  isActive = false;
  
  activate() {
    this.isActive = true;
  }
  
  deactivate() {
    this.isActive = false;
  }
}

// 믹스인을 적용할 베이스 클래스
class User {
  name: string;
  
  constructor(name: string) {
    this.name = name;
  }
}

// 믹스인 함수
type Constructor<T = {}> = new (...args: any[]) => T;

function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    timestamp = Date.now();
  };
}

function Activatable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    isActive = false;
    
    activate() {
      this.isActive = true;
    }
    
    deactivate() {
      this.isActive = false;
    }
  };
}

// 믹스인 적용
const TimestampedUser = Timestamped(User);
const TimestampedActivatableUser = Activatable(TimestampedUser);

// 사용
const user = new TimestampedActivatableUser("Alice");
console.log(user.name);        // "Alice"
console.log(user.timestamp);   // 현재 타임스탬프
console.log(user.isActive);    // false
user.activate();
console.log(user.isActive);    // true

마치며

타입스크립트 클래스는 자바스크립트 클래스의 모든 기능을 포함하면서도, 정적 타입 검사와 추가적인 기능들을 제공합니다. 접근 제한자, 추상 클래스, 인터페이스 구현 등을 통해 더 안전하고 구조화된 객체 지향 코드를 작성할 수 있습니다.

다음 포스팅에서는 타입스크립트의 제네릭(Generics)에 대해 자세히 알아보겠습니다. 타입스크립트 시리즈를 계속 지켜봐 주세요!