본문 바로가기

javascript

[Typescript] 클래스 선언 및 인스턴스화

  • 타입스크립트 클래스
    • 타입스크립트 관련 기능 - 클래스 멤버, 액세스 한정자
    • 필수 또는 선택적 매개 변수
    • ES6 기능 확장
  • 필요에 따라 모든 주요 브라우저 및 플랫폼에서 작동하는 자바스크립트로 컴파일 가능

 

타입스크립트 클래스

클래스 개념

(예) 자동차를 빌드한다고 할 때

  • Car 클래스는 청사진
    • 자동차의 특성 - 제조사, 색상, 문 개수 등
    • 자동차의 동작 - 가속, 제동, 회전
  • 자동차 빌드를 위한 계획일 뿐, 실제 개체가 되려면 Car 클래스에서 Car 인스턴스를 빌드해야 한다.
  • Car 클래스를 사용해 고유한 특성이 있는 새로운 Car 객체를 원하는 수만큼 만들 수 있다.
  • Car 클래스를 확장해서 ElectricCar 같은 클래스를 만들 수도 있다. 이 클래스는 Car의 모든 특성과 동작을 포함하지만 주행거리나 충전 옵션 같은 고유한 특성과 동작을 가질 수 있다.

 

  • 클래스는 개체의 데이터를 캡슐화한다 → 데이터와 동작은 클래스에 포함되지만 개체를 사용하는 사람에게 데이터와 동작의 세부 정보를 숨길 수 있다.
  • (예) Car 개체의 turn 메서드를 호출하는 경우 자동차 핸들의 작동 원리를 정확하게 알 필요 없고 명령하면 자동차가 왼쪽이나 오른쪽으로 회전한다는 것만 알고 있으면 된다.
  • 클래스의 모든 특성과 동작이 속성과 메서드를 통해서만 공개되는 블랙박스 역할을 하므로 코드 작성자가 할 수 있는 일이 제한된다.

 

클래스 구성 요소

속성(필드)

  • 개체의 데이터(또는 특성). 속성은 설정하거나 반환할 수 있는 개체의 본질적 특성

생성자(constructor)

  • 클래스를 기반으로 개체를 만들고 초기화하는데 사용하는 특수 함수.
  • 클래스의 새 인스턴스를 만들면 생성자는 클래스 모양을 사용해 새 개체를 만들고 이 개체에 전달된 값을 사용해 초기화한다.

접근자 (accessor)

  • 속성 값을 get하거나 set할 때 사용하는 함수 형식.
  • set 접근자를 생략하면 읽기 전용 속성이 됨.
  • get 접근자를 생략하면 액세스 불가 속성이 됨. (액세스를 시도하면 undefined가 반환됨)

메서드

  • 개체가 할 수 있는 동작이나 작업을 정의한 함수
  • 메서드를 호출하여 개체의 동작을 호출 할 수 있다.
  • 클래스 안에서만 액세스할 수 있고
  • 클래스의 다른 메서드에서 호출하는 메서드를 정의할 수도 있다.

 

모든 구성 요소가 필요하지는 않다.

유틸리티 개체의 경우, 메서드와 생성자만 필요하거나 데이터 관리를 위한 속성만 필요할 수도 있다.

 

 

연습 - Car 클래스 만들기

  • 클래스를 만들기 위해서는 constructor, 접근자, 메서드 등을 정의한다.
  • 클래스 이름을 파스칼 표기법 (시작 글자를 대문자로)
class Car {
    // properties

    // Constructor

    // Accessors

    // Methods
}

 

클래스 속성 선언

  • 클래스 속성은 초기화될 때 개체에 전달하는 원시 데이터라고 생각하면 됨.
  • Car 클래스 속성: 모든 자동차에 적용되는 속성 (제조사, 색상, 문 개수 등)
class Car {
    // properties
    _make: string;
    _color: string;
    _doors: number;

    // Constructor

    // Accessors

    // Methods
}

 

클래스 생성자(constructor) 정의

클래스의 두 가지 형식

  • 인스턴스 형식 : 클래스의 인스턴스에 포함되는 멤버를 정의
  • constructor 형식 : 클래스 함수의 멤버 정의. 클래스의 정적 멤버를 포함하기 때문에 ‘정적 측면’ 형식이라고도 함.
  • constructor 사용하면 클래스트를 간소화하고, 클래스를 사용할 때 보다 쉽게 관리할 수 있다.

 

constructor 함수의 구성

  • constructor 키워드
  • 새 인스턴스를 만들 때 새 개체로 전달되는 매개변수 목록
    • 클래스에서 모든 속성의 매개변수를 정의할 필요는 없다.
    • 매개변수는 필수, 선택, 기본값 등을 가질 수 있고, rest 매개변수일 수도 있다.
    • 매개변수 이름은 속성 이름과 다를 수 있다.
  • 속성 할당: 매개변수의 값을 속성 값에 할당한다. 클래스 멤버(속성)에 액세스하려면 this. 키워드를 사용한다.
  • 클래스에는 최대 하나의 constructor만 사용
  • constructor가 없을 경우 자동으로 제공됨.
class Car {
    // properties
    _make: string;
    _color: string;
    _doors: number;

    // Constructor
    constructor(make: string, color: string, doors = 4) {
        this._make = make;
        this._color = color;
        this._doors = doors;
    }
    
    // Accessors

    // Methods
}
  • 속성 이름 앞에 있는 밑줄(_)은 선언할 때 꼭 필요한 것이 아니라, 생성자(constructor)를 통해 액세스하는 매개변수와 속성 선언을 구분하기 위한 것.

 

접근자 정의

  • 클래스 속성은 기본적으로 public이기 때문에 직접 액세스 가능
  • 타입스크립트는 getter와 setter를 지원하여 속성에 대한 액세스를 가로 챈다. → 각 개체에서 멤버에 액세스하는 방법을 보다 세밀하게 제어 가능
// Accessors
    get make() {
        return this._make;
    }
    set make(make) {
        this._make = make;
    }
  • 프로그램에 반환하기 전에 유효성을 검사하거나 제약 조건을 적용할 수 있다. 다른 데이터 조작을 수행할 수도 있다.
  • color 매개변수의 get/set을 정의하되 _color 속성 값에 문자열 연결
get color() {
        return 'The color of the car is ' + this._color;
    }
    set color(color) {
        this._color = color;
    }
  • doors 매개변수의 get/set 정의. _doors 값을 반환하기 전에 짝수인지 확인. 짝수가 아니라면 오류
get doors() {
   return this._doors;
}
set doors(doors) {
   if ((doors % 2) === 0) {
        this._doors = doors;
   } else {
        throw new Error('Doors must be an even number');
   }
}

 

클래스 메서드 정의

  • 클래스 안에서 타입스크립트 함수 정의
  • 개체나 클래스 안의 다른 함수에서 호출할 수도 있다.
  • 클래스 메서드는 클래스가 수행할 동작을 설명하며 클래스에 필요한 다른 작업을 수행할 수 있다.
  • function 키워드가 없다.
// Methods
    accelerate(speed: number): string {
        return `${this.worker()} is accelerating to ${speed} MPH.`;
    }
    brake(): string {
        return `${this.worker()} is braking with the standard braking system.`;
    }
    turn(direction: 'left' | 'right'): string {
        return `${this.worker()} is turning ${direction}.`;
    }
    // 이 함수는 다른 메서드 안에서 사용하기 위한 함수
    worker(): string {
        return this._make;
    }

 

 

연습 - 클래스 인스턴스화

  • new 키워드를 사용해 Car 클래스를 인스턴스화하고 매개변수 전달
let myCar1 = new Car('Cool Car Company', 'blue', 2);
  • myCar1 개체에서 color에 액세스했을 때와 _color에 액세스했을 때를 비교해 보자
console.log(myCar1.color);
console.log(myCar1._color);
  • _color 멤버는 클래스에 정의된 속성을 나타내고, 
  • color는 생성자에 전달하는 매개 변수
  • _color를 참조하는 경우 'blue'를 반환하는 속성의 원시 데이터에 액세스
  •  color를 참조하는 경우 'The color of the car is blue'를 반환하는 속성에 get 또는 set을 통해 액세스
  • doors는 선택적 매개변수이므로 초기화에서 생략해서 사용할 수 있다.

 

  • myCar3 개체를 만들고, doors 매개변수값을 홀수로 해보자. → constructor에서 유효성 검사를 수행하지 않기 때문에 오류 없음. → (doors의 set 블록이 유효성을 검사한다)
let myCar3 = new Car('Galaxy Motors', 'red', 3);
  • myCars3의 doors 값을 홀수로 설정해 보자 → 오류
let myCar3 = new Car('Galaxy Motors', 'red', 3);
myCar3.doors = 5;

 

 

Car 개체가 초기화될 때 유효성 검사하려면?

⇒ constructor 에 유효성 검사 코드 추가

class Car {
    // properties
    _make: string;
    _color: string;
    _doors: number;

    // Constructor
    constructor(make: string, color: string, doors = 4) {
        this._make = make;
        this._color = color;
        if ((doors % 2) === 0) {
            this._doors = doors;
        } else {
            throw new Error('Doors must be an even number.');
        }
    }

    // Accessors
	......
}

 

  • 이젠 초기화에서 걸러낼 수 있음
let myCar3 = new Car('Galaxy Motors', 'red', 3);

 

클래스의 메서드 테스트

let myCar1 = new Car('Cool Car Company', 'blue', 2); 
console.log(myCar1.color);
console.log(myCar1._color);

console.log(myCar1.accelerate(35));
console.log(myCar1.brake());
 

액세스 한정자

  • 모든 클래스 멤버는 기본적으로 public → 클래스 외부에서 클래스 멤버에 액세스 가능
  • 앞의 Car 클래스에서 _color(클래스에서 정의한 속성)과 color(생성자에서 정의한 매개변수)에 접근할 수 있었다.
  • 일반적으로는 get, set 접근자를 통해서만 원시 데이터에 접근할 수 있도록 하는 것이 좋다.
  • 메서드 함수에 대한 액세스를 제어할 수도 있다.
  • 앞의 Car 클래스에서 메서드 함수 안에 또다른 worker 함수가 포함되어 있는데, 클래스 외부에서 worker 함수를 호출한다면 예상치 못한 결과가 생길 수 있다.

 

액세스 한정자

public

  • 액세스 한정자를 지정하지 않는 경우 기본값은 public
  • public 키워드를 사용하여 명시적으로 멤버를 public으로 설정할 수도 있다.

private

  • private 키워드를 사용. 클래스 외부에서 멤버에 액세스할 수 없습니다.

protected

  • private 한정자와 매우 비슷하게 동작다.
  • 파생 클래스 내에서 protected로 선언된 멤버에도 액세스할 수 있다 (뒤에서 설명)

기타

  • readonly : 멤버를 선언하거나 constructor에서 초기화할 때만 설정 가능

 

타입스크립트는 가져온 위치에 상관없이 두 가지 형식을 비교할 때 모든 멤버의 형식이 호환되는 경우 ‘형식 자체가 호환된다’.

만일 private 멤버나 protected 멤버를 포함하는 형식을 비교할 경우 좀더 다르게 동작함
→ 둘 중 하나에 private/protected 멤버가 있다면 다른 하나의 동일한 선언에도 private/protected 멤버를 포함해야 한다.

 

연습 - 액세스 한정자

1. myCar1에서 액세스할 수 있는 멤버들

2. _color, _doors, _make 속성과 worker 함수를 private으로 지정해 보자

class Car {
    // properties
    private _make: string;
    private _color: string;
    private _doors: number;

    // Constructor
    ......

    private worker(): string {
        return this._make;
    }
}

 

3. 다시 한번 myCar1에서 액세스할 수 있는 멤버를 확인하면?

 

정적 속성 정의

  • 지금까지 정의된 클래스의 속성과 메서드는 인스턴스 속성
    → 클래스 개체의 각 인스턴스에서 인스턴스화되고 호출됨.
  • 정적 속성 : 정적 속성 및 메서드는 클래스의 모든 인스턴스에서 공유됨.
  • 속성을 정적으로 만들려면 속성이나 메서드 이름 앞에 static 키워드 붙임

 

1. Car 클래스가 인스턴스화되는 횟수를 저장하는 numberOfCars 라는 속성을 static으로 지정하기

- 초깃값 0
- 생성자에서 횟수 1씩 증가
정적 속성에 액세스할 때는 this 대신 className.propertyName 구문 사용
 
class Car {
    // properties
    private static numberOfCars: number = 0;
    private _make: string;
    private _color: string;
    private _doors: number;

    // Constructor
    constructor(make: string, color: string, doors = 4) {
        this._make = make;
        this._color = color;
        if ((doors % 2) === 0) {
            this._doors = doors;
        } else {
            throw new Error('Doors must be an even number.');
        }
        Car.numberOfCars++;
    }

    // ......
}

 

2. 정적 메서드를 지정할 수도 있다.

public static getNumberOfCars(): number {
   return Car.numberOfCars;
}

 

3. 인스턴스를 만들고 인스턴스 수를 반환해 보자

let myCar1 = new Car('Cool Car Company', 'blue', 2); 
let myCar2 = new Car('Galaxy Motors', 'blue', 2);
console.log(Car.getNumberOfCars());  // 2 
let myCar3 = new Car('Galaxy Motors', 'white');
console.log(Car.getNumberOfCars());   // 3
 

상속을 사용해 클래스 확장

  • (예) Car 클래스를 확장해 eletricCar 클래스를 만들 수 있다
    • ElectricCar 클래스는 Car 클래스의 속성 및 메서드를 상속하지만 고유 특성 및 동작을 가질 수 있다.
    • Car 클래스를 확장해서 새 클래스를 만든다음 빌드할 수 있다.
  • extends 키워드 사용
  • electricCar 클래스는 Car 클래스의 하위 클래스 (기본 클래스는 슈퍼 클래스, 부모 클래스라고 부름)

 

상속을 사용하는 이유

  • 코드 재사용 가능성. 한 번 개발하여 여러 곳에서 다시 사용할 수 있습니다. 이를 통해 코드의 중복성을 방지할 수도 있습니다.
  • 하나의 기본 클래스를 사용하여 계층 구조에서 원하는 수의 하위 클래스를 파생할 수 있습니다. 예를 들어 Car 계층 구조의 하위 클래스에는 SUV 클래스나 Convertible 클래스도 포함될 수 있습니다.
  • 유사한 기능이 있는 여러 다른 클래스에서 코드를 변경하는 대신 기본 클래스에서 한 번만 변경하면 됩니다.

 

메서드 재정의

  • 기본 클래스의 함수와 동일한 이름으로 하위 클래스에 함수를 만들지만 기능이 서로 다를 때 발생

 

연습 - 클래스 확장

1. Car 클래스를 확장한 ElectricCar 클래스 만들기

class Car { ... }

class ElectricCar extends Car {
    // properties unique to Electric Car


    // Constructor


    // Accessors


    // Methods
}

 

2. ElectricCar의 고유 속성 선언

class ElectricCar extends Car {
    // properties unique to Electric Car
    private _range: number;

    // Constructor


    // Accessors


    // Methods
}

 

3. 하위 클래스의 constructor는 기본 클래스의 constructor와 몇 가지면에서 다르다.

  • 매개 변수 목록에는 기본 클래스와 하위 클래스의 모든 속성이 포함될 수 있습니다. (TypeScript의 모든 매개 변수 목록과 마찬가지로 필수 매개 변수가 선택적 매개 변수 앞에 와야 합니다.)
  • constructor의 본문에서 super() 키워드를 추가하여 기본 클래스의 매개 변수를 포함해야 합니다. super 키워드는 실행될 때 기본 클래스의 constructor를 실행합니다.
  • 하위 클래스의 속성을 참조할 때 super 키워드가 this.에 대한 참조 앞에 와야 합니다.
// Constructor
    constructor(make: string, color: string, range: number, doors = 2) {
        super(make, color, doors);
        this._range = range;
    }

 

4. range 매개변수의 get과 set 접근자 정의

// Accessors
   get range() {
        return this._range;
    }
    set range(range) {
        this._range = range;
    }

5. charge 메서드 정의

// Methods
    charge() {
        console.log(this.worker() + " is charging.");
    }

→ worker는 private으로 정의했기 때문에 Car 클래스 안에서만 액세스 가능

Property 'worker' is private and only accessible within class 'Car'.

 

6. 위 문제를 해결하려면 private 대신 protected로 변경.

→ 이렇게 하면 Car 클래스의 하위 클래스가 함수를 사용할 수 있다.
→ 클래스에서 인스턴스화한 개체에 사용할 수 있는 멤버에는 나타나지 않는다.

 
class Car {
	...
	// 이 함수는 다른 메서드 안에서 사용하기 위한 함수
    protected worker(): string {
        return this._make;
    }

7. electricCar 클래스 테스트하기

 
let spark = new ElectricCar('Spark Motors', 'silver', 124, 2);
let eCar = new ElectricCar('Electric Car Co.', 'black', 263);
console.log(eCar.doors);
spark.charge();

8. 메서드 재정의

  • ElectricCar 클래스에서 brake 메서드 정의. 이 때 brake 메서드의 매개변수 시그니처와 반환 형식은 Car 클래스의 brake 메서드와 같아야 한다.
// Methods
    charge() {
        console.log(this.worker() + " is charging.");
    }
    brake(): string {
        return `${this.worker()} is braking with the regenerative braking system.`;
    }

 

9. 메서드 테스트

 
console.log(spark.brake());

 

연습 - 클래스 모양을 확인하는 인터페이스 선언

  • 인터페이스를 사용하여 개체의 필수 속성과 해당 형식을 설명하는 ‘코드 계약’을 설정한다.

→ 인터페이스를 사용하면 클래스 인스턴스 모양을 확인할 수 있다. 

 

1. Car 클래스의 속성과 메서드를 설명하는 Vehicle 인터페이스 선언

  • 클래스의 속성이 아니라 생성자의 매개변수가 포함된다.
  • 인터페이스는 클래스의 퍼블릭 쪽만 설명할 수 있고 프라이빗 멤버는 포함할 수 없다.
interface Vehicle{
    make: string;
    color: string;
    doors: number;
    accelerate(speed: number): string;
    brake(): string;
    turn(direction: 'left' | 'right'): string;
}

 

2. Car 클래스에서 Vehicle 인터페이스 구현

  • implements 키워드 사용
  • 클래스의 세부 정보를 빌드할 때 Vehicle 인터페이스에 설명된 코드 계약을 따르는지 확인함.
interface Vehicle{
    make: string;
    color: string;
    doors: number;
    accelerate(speed: number): string;
    brake(): string;
    turn(direction: 'left' | 'right'): string;
}

class Car implements Vehicle {
    ......
}

 

디자인 고려 사항

  • 클래스와 인터페이스를 각각 언제 사용하면 좋을까?

인터페이스를 사용해야 하는 경우

  • TypeScript가 JavaScript로 변환 컴파일되면 인터페이스가 제거된다. → 결과 파일에서 공간을 차지하지 않으며 실행될 코드에 부정적인 영향을 주지 않는다.
  • 클래스가 있어야만 인터페이스를 사용할 수 있는 다른 프로그래밍 언어와 달리 TypeScript에서는 클래스 없이 인터페이스를 사용하여 데이터 구조를 정의할 수 있다.
  • 인터페이스를 사용하여 함수의 매개 변수 개체를 정의하고 다양한 프레임워크 속성의 구조를 정의하고, 원격 서비스 또는 API에서 개체가 어떻게 보이는지 정의할 수 있다.
  • 풀스택 애플리케이션을 만들고 있다면, 데이터를 구조화하는 방법을 정의해야 한다.
    (예) 개에 대한 정보를 저장해야 한다면
interface Dog {
    id?: number;
    name: string;
    age: number;
    description: string;
}
  • 인터페이스는 클라이언트와 서버 코드 모두 공유 모듈에서 사용할 수 있다 → 데이터 구조를 동일하게 유지할 수 있다. (예) 클라이언트에서 정의하는 서버 API에서 개를 검색하는 코드를 포함할 수 있다.
async loadDog(id: number): Dog {
    return await (await fetch('demoURL')).json() as Dog;
}​

 

 

클래스를 사용해야 하는 경우

  • 인터페이스와 클래스의 주요 차이점은 클래스를 사용하여 구현 세부 정보를 정의할 수 있다는 것
  • 인터페이스만이 데이터의 구조화 방법을 정의한다.
  • 클래스를 사용하여 메서드, 필드, 속성을 정의할 수 있다. 클래스는 템플릿 개체를 위한 방법도 제공하는데, 기본값을 정의한다.
  • (예) 서버에서 개를 데이터베이스에 로드하거나 저장하는 코드 데이터베이스에서 데이터를 관리하는 일반적인 방법은 ‘활성 레코드 패턴’
    → 데이터 자체에 save, load 등의 메서드가 있음.
  • Dog 인터페이스를 사용해 동일한 속성 및 구조가 있는지 확인하고, 작업을 하는데 필요한 코드를 추가할 수 있다.
interface Dog {
    id?: number;
    name: string;
    age: number;
    description: string;
}

class DogRecord implements Dog {
    id: number;
    name: string;
    age: number;
    description: string;

    constructor( {name, age, description, id = 0}: Dog) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.description = description;
    }

    static load(id: number): DogRecord {
        // code to load dog from database
        return dog;
    }

    save() {
        // code to save dog to database
    }
}

 

인터페이스에 관해 기억해야 할 TypeScript의 주요 기능 중 하나는 클래스가 필요하지 않다는 것입니다. 이 때문에 전체 클래스 구현을 만들지 않고도 데이터 구조를 정의하는 기능이 필요할 때마다 사용할 수 있습니다.

 

 

(이 글의 원문 보기 : Declare and instantiate classes in Typescript)

 

Declare and instantiate classes in TypeScript - Training

Learn how to declare and instantiate classes in TypeScript.

learn.microsoft.com