본문 바로가기

타입스크립트

타입스크립트 왕초보의 '클래스' 개념 정리

반응형

기본 자바스크립트에서 개발을 시작한 프엔들에게 클래스라는 개념은 너무나 거리가 멀다. 그래서 처음 클래스를 접하면서 납득하기? 어려웠던 개념 정리를 해보고자 한다.

 

 

 

1. 자바스크립트 클래스 vs 타입스크립트 클래스

 

클래스는 객체를 생성하기 위한 템플릿이자 '함수'다.

자바스크립트에서는 es6 이후 현재 사용되는 클래스 기반의 개발이 시작됐고, 타입스크립트로 넘어가면서 보다 엄격한 객체 지향형 개발이 가능해졌다. 타입스크립트의 클래스가 자바스크립트의 그것과의 가장 큰 차이는 프로퍼티에 타입을 선언해야 한다는 점이다.

 

/* 자바스크립트 */

class Animal {

    constructor(name) {
        this.name = name
    }

    call () {
        return this.name
    }
}

let dog = new Animal('bomi')

console.log(dog.call())		// 결과값 bomi

 

위에서처럼 자바스크립트에서는 프로퍼티를 선언할 시 생성자 안에서만 가능하며 별도의 타입 선언을 할 필요가 없다.

 

/* 타입스크립트 */

class Animal {
    name: string;   // 프로퍼티 타입 선언

    constructor(name: string) {	// 매개변수 역시 타입 선언
        this.name = name
    }

    call () {
        return this.name
    }
}

let dog = new Animal('bomi')

console.log(dog.call())  // 결과값 bomi

 

하지만 타입스크립트로 넘어오면서 프로퍼티는 클래스 상단에 초기화 및 사전 선언을 해야하며 타입 역시 지정해야 한다.

생성자 내부에서도 상속받은 프로퍼티에 타입을 선언해줘야 한다.

 

** VS Code의 왼쪽 하단에 위치한 'OUTLINE'에는 선언된 프로퍼티의 타입을 확인할 수 있는 기능이 들어있다. 타입스크립트 공부시 유용하니 꼭 확인해보자.

 

 

 

2. 인스턴스 생성

 

클래스는 데이터와 이를 조작하는 메서드 등을 하나로 뭉쳐놓은 추상화된 '몸체'다. 이는 아직 객체라 할수 없으며 객체화를 위해선 인스턴스 생성이 필요하다. 인스턴스 생성은 new + 클래스명을 코드 하단에 입력하면 된다. 클래스 입력 후 가장 하단에서 불러와야 인스턴스가 생성된다. (잠정적 데드존)

'new + 클래스명'을 통해, 클래스 내부의 코드들을 호출하고 생성자(constructor)를 실행 및 초기화한다.

 

 

 

3. 상속에 대하여

 

클래스는 'extends'를 붙여 상속을 주고 받으며 부모-자식관계의 클래스들을 생성할 수 있다.

먼저 부모가 될 클래스를 생성한 뒤, 그 뒤 이를 상속받을 자식 클래스는 class 자식명 extends 부모클래스명 순으로 입력하면 된다.

 

class Animal {
    _name: string;   // 프로퍼티 초기화

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

    aging (age: number = 3) {
        console.log(`${this._name} is ${age} years old`);
    }
}

class Dog extends Animal {
    constructor(name: string) {
        super(name); 	// 부모 클래스의 constructor 실행, name이 파라미터 값으로 넘어간다.
    }
    aging (age = 5) { 	// 타입 추론으로 number가 생략됐다
        super.aging(age)
    }
}

class Cat extends Animal {
    constructor(name: string) {
        super(name); 	// 부모 클래스의 constructor 실행, name이 파라미터 값으로 넘어간다.
    }
    aging (age = 2) { 	// 타입 추론으로 number가 생략됐다
        super.aging(age)
    }
}

let dog = new Dog('Rio')
let cat: Animal = new Cat('Tany')

dog.aging(); 	// Rio is 5 years old
cat.aging(); 	// Tany is 2 years old

 

여기서 부모 클래스는 당연히 Animal 이며, 그 뒤로 extends를 통해 Dog와 Cat이라는 자식 클래스들을 만들었다.

new Dog('Rio'), new Cat('Tany') 를 통해 자식 클래스인 Dog와 Cat의 인스턴스가 생성됐고, Rio와 Tany는 파라미터 값으로 넘겨진다. 인스턴스가 생성되면 바로 constructor가 실행되는데, 여기서 super() 라는 메서드가 등장한다.

 

super()는 부모의 함수를 실행하는 메서드로, 사용법은 아래와 같다

 

super([arguments]); // 부모 생성자 호출
super.부모영역메서드([arguments]);

 

기본적으로 super는 파라미터값을 부모 생성자(constructor)로 넘기면서 코드를 호출한다.

또한 부모 클래스 내부에 생성한 함수를 . 와 함께 붙여 바로 실행시킬 수도 있다.

super() 메서드를 입력한 뒤에는 this.프로퍼티 입력이 가능해지는데, super()보다 먼저 this.프로퍼티가 입력되면 참조오류가 발생한다.

 

constructor(name: string) {
    this._name; 	// 참조오류
    super();
    this._name = 'Peter';
}

 

결과적으로 콘솔에 찍힌 값은 모두 자식 클래스에서 입력된 파라미터로 넘어간 것을 확인할 수 있다. 심지어 변수 cat의 타입은 부모 클래스인 Animal로 입력했으나 자식 클래스 Cat의 인스턴스가 생성되면서 값이 오버라이딩Overriding 됐다. 이처럼 오버라이딩은 앞서 부모 클래스에서 입력된 메서드를 자식 클래스에서 재정의하는 것을 말한다.

 

 

 

3. 접근 제한자

 

클래스 속성 또는 메서드에는 외부로부터의 접근을 제어할 수 있는 명령어 public, protected, private 가 있다. 

  • public : 접근 제한자를 별도로 설정하지 않으면 디폴트 값으로 public이 들어간다. 클래스 내부는 물론, 인스턴스로도 접근이 가능하다.
  • protected : 선언된 클래스와 그 클래스를 상속받은 자식 클래스까지 접근이 가능해진다. 인스턴스에서는 접근이 불가하다.
  • private : 선언된 클래스 외에선 접근이 불가하다. private로 제한된 프로퍼티는 클래스 내에서 getter/setter로 값이 수정된다.

이 접근 제한자 설정을 통해 속성 및 메서드의 접근을 제어하는데, 이 접근 제한자도 부모-자식 간의 클래스에서는 상속을 통해 호환되기도 한다.

 

class Animal {
    private _name: string;

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

    call () {
        console.log(`My name is ${this._name}`);
    }
}

class Dog extends Animal {
    constructor(name: string) {
        super(name);
    }
}

class Insect {
    _name: string;

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

    call () {
        console.log(`My name is ${this._name}`);
    }
}

 

위처럼 Animal 이라는 클래스와 이를 상속받는 자식 클래스 Dog, 그리고 별도의 Insect 클래스를 유사한 구조로 코드를 짰다.

 

let animal = new Animal('any')
let dog = new Dog('Rio')
let butterfly = new Insect('Butterfly')

animal = dog;
animal.call();   // 'My name is Rio'

animal = butterfly;     // Property '_name' is private in type 'Animal' but not in type 'Insect'
animal.call();

console.log(dog._name) // Property '_name' is private and only accessbile within class 'Animal'

 

그리고 각각 인스턴스를 생성했다. Animal 인스턴스를 생성한 변수 animal에 Dog 인스턴스 값을 넣었을 때, 메서드 call()에서 Dog의 이름이 잘 출력되는 것을 확인했다. 이는 _name 속성에 private로 제어했지만, 자식 클래스 Dog가 _name 이라는 속성을 부모 클래스로부터 이어받기 때문에 호환이 되는 것이다.

반면 animal에 Insect 인스턴스 값을 재정의하자, private한 _name 속성이 Insect 클래스에는 존재하지 않는다고 에러가 뜬다.

그렇다면 Insect 클래스 내부의 _name 속성 앞에 private를 넣으면 해결될까? 아니다.

 

Type 'Insect' is not assignable to type 'Animal'. 

Types have separate declarations of a private property '_name'.

 

이같은 에러가 뜬다. Insect 클래스는 Animal 클래스에 할당되어 있지 않다. 상속받은 속성이 아니기 때문에 호환이 안되는 것이다.

 

끝으로 dog._name 역시 에러가 뜨는데, 이는 당연하게도 private이 인스턴스와 자식 클래스 모두 사용이 막혀있기 때문이다. private가 붙은 속성 _name은 오로지 Animal 클래스 안에서만 사용 가능하다.

 

 

 

4. Readonly

 

프로퍼티 앞에 readonly를 붙이면 읽기 전용 속성으로 만들 수 있다. 마치 const 로 상수를 만드는 것과 같이, 일단 초기화되면 재정의를 통한 수정은 안된다. 이는 앞서 설명한 접근제한자와 유사하게 프로퍼티를 제어할 수 있는데, 차이점은 접근 제한자의 경우 getter/setter를 통해 프로퍼티를 수정할 수 있다는 것이다. 때문에 접근 제한자를 사용할지 readonly를 사용할지 상황에 따라 판단하는 것이 중요하다. 

 

// private - getter/setter를 이용해 속성 제어

class BoyGroup {
    private _memberName: string = '';
    private memberNameMaxLength:number = 5;

    get memberName(): string {
        return this._memberName;
    }

    set memberName(newName: string) {
        if (newName && newName.length > this.memberNameMaxLength) {
            throw new Error(`멤버의 이름이 ${this.memberNameMaxLength}자를 넘습니다!`);
        }

        this._memberName = newName;
    }
}

let _boyGroup = new BoyGroup();
try {
    _boyGroup.memberName = "김석진";
    console.log(_boyGroup.memberName)
} catch (error) {
    console.log(error);
}

 

 

5. Interface

 

interface는 앞서 사용될 프로퍼티 및 메서드의 타입을 지정해 보다 엄격한 타입관리를 가능하게 한다. interface은 추상적 의미를 가지나 abstract가 앞에 붙진 않는다. interface에 지정된 프로퍼티 및 메서드는 반드시 상속된 함수에서 재정의 되어야 한다.

 

interface의 속성 앞에도 readonly가 붙을 수 있다. 하지만 접근제한자는(public제외) 붙을 수 없다. 선언된 함수 내에서만 읽고 쓸 수 있기 때문에 interface에서 private이나 protected가 붙으면 의미가 없기 때문. private, protected는 클래스 내에서 지정하는 것이 맞다.

 

interface Container {
    readonly name: string;
    readonly age: number;
    protected skill: string[];  // 'protected' modifier cannot appear on a type member.
    job: string;
}

class Person implements Container {     // Property 'job' is missing in type 'Person' but required in type 'Container'
    name = 'John';
    age: number;
    skill: string[];

    constructor(age: number, skill: string[]) {
        this.age = age;
        this.skill = skill;

        console.log(`${this.name} is ${this.age} years old. He can ${this.skill}.`)
    }
}

let one = new Person(30, ['walk', 'eat', 'talk'])

 

interface 프로퍼티 앞에 protected를 붙이니, protected는 타입 멤버에 나타날 수 없다는 에러 문구가 뜬다.

또 job 프로퍼티가 Person 클래스에서 재정의 되지 않으니, job 프로퍼티가 없으며 이는 요구된다는 에러 문구가 뜬다. 때문에 이런 상황에서 job이라는 프로퍼티 선언이 필요하다면 ? 물음표 문법을 사용해 선택적 프로퍼티를 만들면 된다. job 이라는 프로퍼티가 존재하면 정의되고 그렇지 않으면 정의되지 않는다.

 

 

 

5. 정적 메서드 static

 

인스턴스끼리의 관계와 상관없이 특정 클래스에서 특정 메소드를 바로 실행시키고 싶은 경우, 우린 static 이라는 정적 메서드를 사용할 수 있다.

속성 또는 메서드 앞에 static을 붙이면, 클래스의 인스턴스 생성 없이도 호출할 수 있다. 때문에 인스턴스에서는 호출이 불가하지만 클래스를 앞에 붙이면 값에 접근이 가능해진다. 

 

class Exemple {

    static staticMethod() {
        console.log('static !!!')
    }
}

let ex = new Exemple()

Exemple.staticMethod();     // static !!!
ex.staticMethod();          // Property 'staticMethod' does not exist on type 'Exemple'

 

클래스명에 . 을 찍고 정적 메서드를 불러오면 바로 실행되는 것을 확인할 수 있었다.

반면 인스턴스에서는 staticMethod가 존재하지 않다고 말하며, 덧붙여 친절하게도 'ex' 대신 클래스명인 'Exemple'을 써보는게 어떻겠냐고 조언까지 해준다. 여러모로 vscode는 타입스크립트에 최적화된 프로그램이 아닌가 싶다.

 

static 메서드 내부에서 this는 클래스 함수 자체를 불러오기 때문에 this 사용을 지양한다. 또한 클래스 내부에서 static 속성이나 메서드를 호출할 때는 this가 아닌 클래스명을 붙인다.

 

class Exemple {

    constructor() {
        console.log(Exemple.staticMethod()) // static !!!
        console.log(Exemple.staticMethod2()) // static !!!
        console.log(this.defaultMethod())  // default
        console.log(this.staticMethod())  // Error !
    }

    static staticMethod() {
        return 'static !!!'
    }

    static staticMethod2() {
        return this.staticMethod()
    }

    defaultMethod() {
        return 'default'
    }
}

let ex = new Exemple()

 

static 함수와 static 함수끼리는 this로 호출이 가능하다. 하지만 이외에는 모두 클래스명을 붙여 Exemple.메서드명 과 같은 형식으로 작성한다.

 

 

 

6. Abstract 추상 클래스

 

추상클래스는 상속하는 클래스 반드시 하나 혹은 하나 이상의 추상 메서드 혹은 프로퍼티를 수반해야 한다. 또한 추상클래스는 단독으로 사용이 안되므로 인스턴스 생성이 불가하다. 따라서 추상클래스에는 이를 상속받는 자식 클래스가 있어야 한다.

추상클래스에 추상 메서드 혹은 프로퍼티 선언은, 자식 클래스에서 동일명의 메서드 혹은 프로퍼티를 반드시 실행해야 함을 선언하는 것과 같다. 

 

abstract class Customer {
    constructor (private name: string) {}

    abstract totalBuy(): number; 		// 추상 메서드 선언

    payment(): string {
        return `${this.name} must pay ${this.totalBuy()} won.`
    }
}

class Alex extends Customer {
    constructor (name: string, private setMenu: number) {
        super(name);
    }

    totalBuy(): number { 			// 추상메서드에 상속될 값
        return this.setMenu
    }
}

class Amy extends Customer {
    constructor (name: string, private setMenu: number, private num: number) {
        super(name);
    }

    totalBuy(): number {			// 추상메서드에 상속될 값
        return this.setMenu * this.num
    }
}

let alexReceipt = new Alex('Alex', 10000);
let amyReceipt = new Amy('Amy', 10000, 2);
console.log(alexReceipt.payment()); 	// Alex must pay 10000 won.
console.log(amyReceipt.payment());		// Amy must pay 20000 won.

 

추상클래스를 어떤 상황에서 쓰는게 적합할까 고민했는데, 위의 예제와 같은 상황에서 유용할 것 같다는 생각이 들었다.

한 부모 클래스를 상속받는 여러 자식 클래스들 사이에 같은 메서드를 다른 방식으로 사용할 경우, 부모 클래스에서 먼저 이 메서드를 추상적 선언을 해주고 자식 클래스에서 그 선언을 이어 받아 별도의 함수를 작성하게 하는 것이다.

 

 

여기까지 타입스크립트의 일부인 클래스를 정리했는데.. 아주 일부이고 초읽기 부분이지만 자바스크립트와 닮은듯 다른 모습이 예제를 짤 때마다 재밌고 신선하게 느껴진다. (그치만 아직 많이 어렵다..) 좀더 연구하고 공부해서 능숙해지는 그 날을 고대한다!!

 

 

 

 

참조 문서 - 

https://www.tutorialsteacher.com/typescript/

https://typescript-kr.github.io/pages/classes.html,

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/super,

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes,

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes/constructor

반응형