Lsiron

Type Script로 Class와 prototype에 타입지정하기(interface) 본문

언어/Type Script

Type Script로 Class와 prototype에 타입지정하기(interface)

Lsiron 2024. 7. 12. 08:00

먼저 Type Script로 Class에 타입을 지정 해 보자.

 

그 전에 Class 문법이란 Java Script에서 수 많은 객체를 생성할 때, 여러번 코드를 입력하여 수 많은 객체를 하나하나 만드는 것이 아닌 객체를 만드는 기계를 만들어 놓고 그 기계에 재료만 넣으면 원하는 객체를 만들어 주는 문법이다. 

 

간단하게 아이스크림 기계를 만들고 , 아이스크림 기계에 재료를 넣어보자.

// 옛날 class 문법
function IceCream (x, y) {
    this.맛 = x;
    this.종류 = y;
}

let iceCream1 = new IceCream('딸기', '막대')
let iceCream2 = new IceCream('포도', '콘')

console.log(iceCream1) // IceCream {맛 : '딸기', 종류 '막대'} 출력
console.log(iceCream2) // IceCream {맛 : '포도', 종류 '콘'} 출력

 

위 코드는 class 문법이 새로 개편되기 전, 사용하던 class 문법이다.

// 현재 사용하는 class 문법
class IceCream {
    constructor (x, y) {
        this.맛 = x;
        this.종류 = y;
    }
}

let iceCream1 = new IceCream('딸기', '막대')
let iceCream2 = new IceCream('포도', '콘')

console.log(iceCream1) // IceCream {맛 : '딸기', 종류 '막대'} 출력
console.log(iceCream2) // IceCream {맛 : '포도', 종류 '콘'} 출력

 

이 전에 사용하던 class 문법과 차이점은 function IceCream을 class와 constructor를 이용하여 둘로 나눈 것 밖에 없다.

 

위와 같이 class를 사용하여 아이스크림을 만드는 기계를 만들어 놓고, new를 사용하여 기계에 재료를 삽입하면 끝이다.

 

이 때, class를 사용한 IceCream을 부모, new를 사용한 IceCream을 자식이라고 한다.

 

부모 자식 개념이 있다면 상속 개념이 나온다. 즉, 부모가 자식에게 물려주는 것.

 

이 상속을 prototype이라고 한다.

( 정확히는 '객체가 다른 객체로부터 속성과 메서드를 상속받는 메커니즘' 인데 말이 어렵다; )

 

자식이 돈을 못 갚으면 부모한테 찾아가고, 그 부모도 없으면 부모의 부모한테도 간다.

 

이렇게 object에서 자료를 뽑을땐 먼저 해당 object를 먼저 뒤져보고 없으면 부모 object, 부모의 부모 object 까지 뒤져본다.

 

이런 과정을 prototype chain이라고 한다.

( 정확히는 ' 속성이나 메서드를 검색할 때 자바스크립트가 따라가는 경로' 인데 말이 어렵다; )

 

그럼 위 class 예시를 가져와보자. 

 

아이스크림을 만드는 기계인데 각 아이스크림 마다 브랜트를 붙이고 싶다. 어떻게 해 줘야할까? 

 

물론 아래와 같이 기계를 직접 조작해서 브랜드가 나오도록 할 수 있다.

class IceCream {
    브랜드 = '해태'
    constructor (x, y) {
        this.맛 = x;
        this.종류 = y;
    }
}


let iceCream1 = new IceCream('딸기', '막대')
let iceCream2 = new IceCream('포도', '콘')

console.log(iceCream1) // IceCream {브랜드: '해태', 맛 : '딸기', 종류 '막대'} 출력
console.log(iceCream2) // IceCream {브랜드: '해태', 맛 : '포도', 종류 '콘'} 출력

 

허나 기계를 직접 조작하지 않고 브랜드를 붙이도록 할 수 있는 설정이 바로 prototype이다.

class IceCream {
    constructor (x, y) {
        this.맛 = x;
        this.종류 = y;
    }
}

IceCream.prototype.브랜드 = '해태'

let iceCream1 = new IceCream('딸기', '막대')
let iceCream2 = new IceCream('포도', '콘')

console.log(iceCream1) // IceCream {맛 : '딸기', 종류 '막대'} 출력
console.log(iceCream2) // IceCream {맛 : '포도', 종류 '콘'} 출력

 

그러면 아래 사진과 같이 아이스크림 기계로 만드는 아이스크림들은 prototype으로 브랜드가 붙여져서(상속되어) 나온다.

 

또한 아이스크림의 브랜드를 찍어보면 직접 기계를 조작했을 때와 같이 아래 사진처럼 브랜드 이름이 나온다.

 

전문용어로 iceCream이라는 자식들이 IceCream class의 부모로부터 브랜드를 상속 받았다고 할 수 있다. 

 

저렇게 문자열만 넣는것이 아닌 함수도 들어 갈 수 있고, 다양하게 활용할 수 있다.

 

이 prototype은 아마 이제까지 자주 봤을 것이다. 바로 .sort() , .map(), .forEach() 등등 함수들이 있는데, 이런 메서드가 바로 prototype으로 상속 되었기 때문에 바로바로 사용할 수 있는것 이다. ( 내장함수라고 하는 이유. )

 

let arr = [1, 2, 3]
let arr = new Array(1, 2 ,3);

 

위 아래 둘 다 똑같다. 즉, 우리가 자주 본 배열은 그저 배열 기계로 찍어낸 것을 사용하는 것 일 뿐. 이러니 저 Array의 부모에 prototype으로 sort() 같은 함수를 넣어놓았으니 자식인 배열들은 저런 함수를 사용할 수 있는 것이다. 

( 그래서 내장함수 구글링을 해보면 Array.prototype.sort() 이런식으로 뜸 )

 

이 말은 우리도 코드를 짤 때, Array.prototype.함수 = function(){} 이런식으로 코드를 짜서 한 프로젝트에서 array에 사용할 수 있는 나만의 함수를 만들 수 있다는 것이다.

 

그럼 이제 본격적으로 Type Script로 Class에 타입지정을 해보자.

 

그대로 위에 있는 아이스크림 기계를 가져와보자.

class IceCream {
    브랜드:string = '해태'
}

let iceCream1 = new IceCream()
console.log(iceCream1.브랜드) // '해태' 출력

 

class로 지정한 IceCream에 필드값만 넣었을 땐 기존에 변수 타입을 지정했을 때와 같이 타입을 넣어주면 된다.

 

허나 앞서 배운것 처럼 굳이 타입을 하나만 지정 할 때는 타입지정을 해 주지 않아도 '해태'  문자열로 값을 할당했기 때문에 알아서 string으로 타입이 지정된다.

 

그러면 이제 class로 지정한 IceCream에 constructor를 넣었을 때 타입을 지정해보자.

class IceCream {
    맛 :string;
    종류 :string;
    constructor (x:string, y:string) {
        this.맛 = x;
        this.종류 = y;
    }
}

let iceCream1 = new IceCream('딸기', '막대')
let iceCream2 = new IceCream('포도', '콘')

console.log(iceCream1) // IceCream {맛 : '딸기', 종류 '막대'} 출력
console.log(iceCream2) // IceCream {맛 : '포도', 종류 '콘'} 출력

 

TypeScript 에서 constructor() 는 필드값에 필드를 미리 입력해줘야 this.필드 를 써줄 수 있다.

(JS에서는 필드를 입력해주지 않아도 됐었음. = JS와 TS의 차이)

 

필드에 타입을 지정하는 방법은 기존에 변수 타입을 지정했을 때와 같이 타입을 넣어주면 되고,

파라미터에는 기존에 함수 파라미터에 타입을 지정했을 때와 같이 타입을 넣어주면 된다.

(당연히 파라미터에는 rest parameter, default parameter 등등 모두 가능하다.)

 

단, return 값에 타입을 지정할 필요는 없다. 왜냐하면 복제되는게 항상 object이기 때문이다.

 

Object에 타입을 지정하려면 interface를 사용하는 방식도 있다.

 

기존에 Object에 타입을 지정하려면 아래와 같이 type alias를 지정해서 사용하는 등 여러 방법을 사용했다.

type Ice = { 맛 : string }
type IceCream = { 맛 : string, 종류 : string};

let 아이스:Ice = { 맛 : '딸기' }
let 아이스크림:IceCream = { 맛 : '딸기', 종류 : '막대'}

 

허나 type 키워드를 쓰는 대신 아래와 같이 interface를 사용해도 된다.

interface Ice { 맛 : string }
interface IceCream { 맛 : string, 종류 : string};

let 아이스:Ice = { 맛 : '딸기' }
let 아이스크림:IceCream = { 맛 : '딸기', 종류 : '막대'}

 

그러면 type을 쓸 때와 다를바가 없는데 interface를 썼을 때의 장점을 무엇일까?

 

바로 class를 사용할 때 처럼 extends로 복사가 가능하다.

interface Ice {맛 : string}
interface IceCream extends Ice {종류 : string};

let 아이스:Ice = { 맛 : '딸기' }
let 아이스크림:IceCream = { 맛 : '딸기', 종류 : '막대'}

 

맛에 대해 타입지정을 해 줄 때 Ice와 IceCream이 중복됐으나, extends로 중복을 없애주었다.

 

이와 비슷하게 type 으로 타입지정을 해 줄때 중복을 막을 수 있는 방법이 있다. 바로 '&' 기호를 사용 해 주는 것.

 

이를 intersection type 이라고 한다.

type Ice = { 맛 : string }
type IceCream = {종류 : string} & Ice;

let 아이스:Ice = { 맛 : '딸기' }
let 아이스크림:IceCream = { 맛 : '딸기', 종류 : '막대'}

 

type alias로 extends 기능을 만드는 방법이라고 할 수 있다. 허나 extends랑은 약간 다른게 이는 두 타입을 전부 만족하는 타입이라는 뜻을 가지고있다.

 

그럼에도 불구하고 type과 interface 사이에는 아주 큰 차이점이 있다.

 

바로 interface는 중복선언이 가능하지만, type은 중복선언이 불가능 하다는 점이다.

 

interface

interface Ice {맛 : string}
interface Ice {가격 : number} // 에러 발생하지 않음
interface IceCream extends Ice {종류 : string}

let 아이스:Ice = { 맛 : '딸기', 가격 : 3000 }
let 아이스크림:IceCream = { 맛 : '딸기', 가격: 500, 종류 : '막대'}

 

이렇게 중복 선언을 하면 자동으로 두 개의 interface Ice가 하나로 합쳐진다고 생각하면 된다.

 

type

type Ice {맛 : string}
type Ice {가격 : number} // 에러 발생!!!
type IceCream extends Ice {종류 : string}

let 아이스:Ice = { 맛 : '딸기', 가격 : 3000 }
let 아이스크림:IceCream = { 맛 : '딸기', 가격: 500, 종류 : '막대'}

 

type으로 중복 선언을 하면 Ice가 중복으로 선언되었다고 바로 에러가 발생하는 것을 확인 할 수 있다.

 

즉, interface는 유연하고 type은 엄격하다고 볼 수 있겠다.

 

그러면 interface의 장점이 부각될 때는 언제일까?

 

TypeScript의 외부 라이브러리 같은 경우 interface로 거의 도배가 되어있다.

 

이 때, 라이브러리에서 설정된 interface에 더해서 커스터마이징으로 타입을 추가하고 싶을 때가 있는데, 이 interface의 경우에는 추후에 타입을 추가하는게 훨씬 수월하다.

 

프로젝트를 제작할 시, 다른 사람이 이용을 많이 할 것으로 예상될 때, object타입은 모두 interface로 지정 해 주는게 좋다.

 

단, 주의할 점은 extends를 사용할 때 중복속성이 발생하면 에러로 잡아준다.

interface Ice {맛 : string}
interface IceCream extends Ice {맛 : number, 종류 : string} // 중복선언 에러발생!!!

 

즉 위와같이 타입의 속성이 중복 될 때 에러가 발생하는 것.

 

허나 '&' 기호로 intersection type을 사용할 땐 중복속성이 발생할 시, 타입을 선언할 땐 에러가 나지않고, 사용하려고 할 때 에러가 발생하므로 주의해야한다.

type Ice = { 맛 : string }
type IceCream = {맛 : number, 종류 : string} & Ice;

let 아이스:Ice = { 맛 : '딸기' } // 중복속성 오류발생!!
let 아이스크림:IceCream = { 맛 : '딸기', 종류 : '막대'} // 중복속성 오류발생!!

 

그럼 interface도 알았으니, 마지막으로 짚고 넘어가야 할 것이 있다.

 

Type Script로 prototype 필드와 prototype 함수에 타입 지정을 어떻게 해 줄까?

 

먼저 prototype 필드를 알아보자.

class IceCream {
    맛: string;
    종류: string;

    constructor() {
        this.맛 = '딸기';
        this.종류 = '막대';
    }
}

// 프로토타입에 필드 추가
IceCream.prototype.가격 = 2000;

// 타입 정의
interface IceCream { 가격: number }

const iceCream1 = new IceCream();
console.log(iceCream1.가격) // 출력: 2000

 

prototype을 통해 가격을 선언 해 준 뒤에 interface를 이용하여, 해당 필드의 타입을 지정할 수 있다.

 

다음으로 prototype 함수를 알아보자.

class IceCream {
    맛: string;
    종류: string;

    constructor() {
        this.맛 = '딸기';
        this.종류 = '막대';
    }
}

// 프로토타입에 함수 추가
IceCream.prototype.후기 = function(a){
    return '아이스크림 ' + a;
};

// 타입 정의
interface IceCream { 후기(a:string):string }

const iceCream1 = new IceCream();
console.log(iceCream1.후기('은 맛있다')); // 아이스크림 은 맛있다 출력

 

prototype을 통해 함수로 후기를 선언 해 준 뒤에 interface를 이용하여, 해당 함수의 파라미터 및 반환 값 타입을 지정할 수 있다.

 

 

참조 : 코딩애플