TypeScript 독학 가이드 6 - 클래스

learning by Seven Fingers Studio 18분
TypeScript클래스OOP객체지향웹개발

JavaScript에도 클래스가 있긴 한데, TypeScript의 클래스는 훨씬 강력해요. 접근 제한자, 추상 클래스, 인터페이스 구현까지… 진짜 객체 지향 프로그래밍 제대로 할 수 있어요. 저는 클래스 배우고 나서 코드 구조가 확 깔끔해졌더라고요.

클래스 기본 문법

가장 간단한 클래스부터 시작해볼게요.

class Person {
  name: string;
  age: number;

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

  introduce() {
    console.log(`안녕하세요, 저는 ${this.name}이고 ${this.age}살입니다.`);
  }
}

const person = new Person("김철수", 25);
person.introduce();

실행 결과:

✓ 컴파일 성공
안녕하세요, 저는 김철수이고 25살입니다.

클래스로 객체를 생성하고 메서드를 호출했습니다

JavaScript 클래스랑 거의 비슷한데, 속성에 타입을 명시한 게 차이예요.

접근 제한자

TypeScript의 가장 큰 특징! JavaScript에는 없는 기능이에요.

public (공개)

어디서든 접근 가능해요. 기본값이라 생략해도 됩니다.

class Person {
  public name: string;  // public은 생략 가능

  constructor(name: string) {
    this.name = name;
  }
}

const person = new Person("김철수");
console.log(person.name);  // OK
person.name = "이영희";     // OK

실행 결과:

✓ 컴파일 성공
김철수

public 속성은 외부에서 자유롭게 접근할 수 있습니다

private (비공개)

클래스 내부에서만 접근 가능해요.

class BankAccount {
  private balance: number = 0;

  deposit(amount: number) {
    this.balance += amount;  // 클래스 내부 - OK
    console.log(`${amount}원 입금. 잔액: ${this.balance}원`);
  }

  getBalance() {
    return this.balance;
  }
}

const account = new BankAccount();
account.deposit(10000);
console.log(account.getBalance());
// console.log(account.balance);  // 에러! 외부 접근 불가

실행 결과:

✓ 컴파일 성공
10000원 입금. 잔액: 10000원
10000

private 속성은 메서드를 통해서만 접근할 수 있습니다

balance를 직접 수정하면 에러:

account.balance = 1000000;  // 에러!

실행 결과:

✗ 타입 에러
Property 'balance' is private and only accessible within class 'BankAccount'

private 속성은 외부에서 수정할 수 없습니다

protected (보호됨)

클래스 내부와 상속받은 자식 클래스에서 접근 가능해요.

class Person {
  protected name: string;

  constructor(name: string) {
    this.name = name;
  }
}

class Employee extends Person {
  private department: string;

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

  introduce() {
    console.log(`저는 ${this.name}이고, ${this.department} 부서입니다.`);
    // name은 protected라서 자식 클래스에서 접근 가능
  }
}

const emp = new Employee("김철수", "개발");
emp.introduce();
// console.log(emp.name);  // 에러! 외부 접근 불가

실행 결과:

✓ 컴파일 성공
저는 김철수이고, 개발 부서입니다.

protected 속성은 상속받은 클래스에서 사용할 수 있습니다

생성자 단축 표현

매번 this.name = name 쓰는 게 귀찮죠? TypeScript는 단축 문법이 있어요.

일반 방식

class Person {
  name: string;
  age: number;

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

단축 방식

class Person {
  constructor(
    public name: string,
    public age: number
  ) {}
}

const person = new Person("김철수", 25);
console.log(person.name);
console.log(person.age);

실행 결과:

✓ 컴파일 성공
김철수
25

생성자 매개변수에 접근 제한자를 붙이면 자동으로 속성이 생성됩니다

훨씬 짧죠? 접근 제한자를 매개변수에 붙이면 자동으로 속성이 생성돼요.

Getter와 Setter

속성처럼 보이지만 실제로는 메서드예요. 값을 읽거나 쓸 때 추가 로직을 넣을 수 있어요.

class Circle {
  private _radius: number = 0;

  get radius(): number {
    return this._radius;
  }

  set radius(value: number) {
    if (value < 0) {
      throw new Error("반지름은 0보다 커야 합니다");
    }
    this._radius = value;
  }

  get area(): number {
    return Math.PI * this._radius * this._radius;
  }
}

const circle = new Circle();
circle.radius = 5;
console.log(`반지름: ${circle.radius}`);
console.log(`넓이: ${circle.area.toFixed(2)}`);

실행 결과:

✓ 컴파일 성공
반지름: 5
넓이: 78.54

getter/setter로 값을 검증하고 계산할 수 있습니다

음수 반지름 넣으면 에러:

circle.radius = -1;  // 런타임 에러!

정적 멤버 (static)

클래스 자체에 속하는 멤버예요. 인스턴스 생성 없이 사용할 수 있어요.

class MathUtil {
  static PI: number = 3.14159;

  static square(x: number): number {
    return x * x;
  }

  static circleArea(radius: number): number {
    return this.PI * radius * radius;
  }
}

console.log(MathUtil.PI);
console.log(MathUtil.square(5));
console.log(MathUtil.circleArea(3));

실행 결과:

✓ 컴파일 성공
3.14159
25
28.27431

static 멤버는 클래스 이름으로 직접 호출합니다

유틸리티 함수 만들 때 유용해요. 인스턴스 만들 필요가 없거든요.

상속 (Inheritance)

클래스는 다른 클래스를 상속받을 수 있어요.

class Animal {
  constructor(public name: string) {}

  move(distance: number) {
    console.log(`${this.name}이(가) ${distance}m 이동했습니다.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log("멍멍!");
  }
}

class Bird extends Animal {
  fly(distance: number) {
    console.log(`${this.name}이(가) ${distance}m 날아갑니다.`);
  }
}

const dog = new Dog("바둑이");
dog.move(10);
dog.bark();

const bird = new Bird("짹짹이");
bird.move(5);
bird.fly(20);

실행 결과:

✓ 컴파일 성공
바둑이이(가) 10m 이동했습니다.
멍멍!
짹짹이이(가) 5m 이동했습니다.
짹짹이이(가) 20m 날아갑니다.

부모 클래스의 메서드를 상속받아 사용합니다

메서드 오버라이딩

부모 메서드를 재정의할 수 있어요.

class Animal {
  constructor(public name: string) {}

  makeSound() {
    console.log("동물 소리~");
  }
}

class Dog extends Animal {
  makeSound() {
    console.log("멍멍!");
  }
}

class Cat extends Animal {
  makeSound() {
    console.log("야옹~");
  }
}

const dog = new Dog("바둑이");
dog.makeSound();

const cat = new Cat("나비");
cat.makeSound();

실행 결과:

✓ 컴파일 성공
멍멍!
야옹~

같은 메서드 이름이지만 다르게 동작합니다

추상 클래스 (Abstract Class)

직접 인스턴스를 만들 수 없고, 상속용으로만 쓰는 클래스예요.

abstract class Shape {
  constructor(public name: string) {}

  abstract getArea(): number;  // 추상 메서드 - 자식이 구현해야 함

  printInfo() {
    console.log(`${this.name}의 넓이: ${this.getArea()}`);
  }
}

class Rectangle extends Shape {
  constructor(
    public width: number,
    public height: number
  ) {
    super("사각형");
  }

  getArea(): number {
    return this.width * this.height;
  }
}

class Circle extends Shape {
  constructor(public radius: number) {
    super("원");
  }

  getArea(): number {
    return Math.PI * this.radius * this.radius;
  }
}

const rect = new Rectangle(10, 5);
rect.printInfo();

const circle = new Circle(3);
circle.printInfo();

실행 결과:

✓ 컴파일 성공
사각형의 넓이: 50
원의 넓이: 28.274333882308138

추상 클래스의 메서드를 각 자식이 구현했습니다

추상 클래스는 직접 인스턴스 생성 불가:

const shape = new Shape("도형");  // 에러!

실행 결과:

✗ 타입 에러
Cannot create an instance of an abstract class

추상 클래스는 직접 인스턴스를 만들 수 없습니다

인터페이스 구현

클래스가 특정 인터페이스를 구현하도록 강제할 수 있어요.

interface Printable {
  print(): void;
}

interface Saveable {
  save(): void;
}

class Document implements Printable, Saveable {
  constructor(public content: string) {}

  print() {
    console.log(`인쇄: ${this.content}`);
  }

  save() {
    console.log(`저장: ${this.content}`);
  }
}

const doc = new Document("TypeScript 가이드");
doc.print();
doc.save();

실행 결과:

✓ 컴파일 성공
인쇄: TypeScript 가이드
저장: TypeScript 가이드

클래스가 여러 인터페이스를 구현할 수 있습니다

실전 예제: 쇼핑 카트

배운 내용을 종합해서 쇼핑 카트 만들어볼게요:

interface Product {
  id: number;
  name: string;
  price: number;
}

class CartItem {
  constructor(
    public product: Product,
    private _quantity: number = 1
  ) {}

  get quantity(): number {
    return this._quantity;
  }

  set quantity(value: number) {
    if (value < 1) {
      throw new Error("수량은 1 이상이어야 합니다");
    }
    this._quantity = value;
  }

  get total(): number {
    return this.product.price * this._quantity;
  }
}

class ShoppingCart {
  private items: CartItem[] = [];

  addItem(product: Product, quantity: number = 1) {
    const item = new CartItem(product, quantity);
    this.items.push(item);
    console.log(`${product.name} ${quantity}개 추가됨`);
  }

  getTotal(): number {
    return this.items.reduce((sum, item) => sum + item.total, 0);
  }

  printCart() {
    console.log("\n=== 장바구니 ===");
    this.items.forEach(item => {
      console.log(`${item.product.name} x ${item.quantity} = ${item.total}원`);
    });
    console.log(`총액: ${this.getTotal()}원\n`);
  }
}

const cart = new ShoppingCart();
cart.addItem({ id: 1, name: "노트북", price: 1500000 }, 1);
cart.addItem({ id: 2, name: "마우스", price: 30000 }, 2);
cart.printCart();

실행 결과:

✓ 컴파일 성공
노트북 1개 추가됨
마우스 2개 추가됨
=== 장바구니 ===
노트북 x 1 = 1500000원
마우스 x 2 = 60000원
총액: 1560000원

클래스를 활용한 실용적인 장바구니 시스템입니다

정리하면

  • 접근 제한자: public, private, protected
  • 생성자 단축: 매개변수에 접근 제한자 붙이기
  • getter/setter: 속성처럼 쓰지만 로직 추가 가능
  • static: 클래스 자체의 멤버
  • 상속: extends로 부모 클래스 상속
  • 추상 클래스: 상속용 클래스, 인스턴스 생성 불가
  • 인터페이스 구현: implements로 구현

다음 글에서는 타입 별칭과 유니온 타입에 대해 배워볼게요. type 키워드, | 연산자, & 연산자까지 알아봅시다!


다음 글 보기

← 이전 글
TypeScript 독학 가이드 5 - 인터페이스
다음 글 →
TypeScript 독학 가이드 7 - 타입 별칭과 유니온 타입
← 블로그 목록으로