[TypeScript] 타입스크립트한테 고백하는 중 🫨

hyeonbin·2023년 12월 4일

FE-log

목록 보기
6/9
post-thumbnail

📃 TypeScript

💡 인터페이스 (Interface)

  • 타입스크립트에서 인터페이스는 다양한 요소에 대한 약속을 정의하는 데 사용되고, 이를 통해 타입 체크를 수행한다. 주로 변수, 함수, 클래스 등 다양한 코드 요소에 적용될 수 있다.

  • 인터페이스는 프로퍼티와 메소드를 가질 수 있다는 점에서 클래스와 유사하나 직접 인스턴스를 생성할 수 없고, 모든 메소드는 선언만 가능하며 구현은 인터페이스를 구현하는 클래스에서 제공되어야 한다. 단, 추상 클래스의 추상 메소드와 달리 abstract 키워드를 사용하지 않는다.


⚙️ 인터페이스 예제

  • logAge 함수에서 받는 인자의 형태는 age를 속성으로 갖는 객체다.

  • 인자를 받을 때 단순한 타입 뿐만 아니라 객체의 속성 타입까지 정의할 수 있다.

  let person = { name: '덕수', age: 35 };

  function logAge(obj: { age: number }) {
    console.log(obj.age); // 35
  }
  logAge(person); // 28

  • 여기에 인터페이스를 적용해보자!
    logAge 함수의 인자는 personAge라는 타입을 가져야하며, 좀 더 명시적으로 바뀌었다.

  • 더불어 다음과 같이 추론할 수 있다. 인터페이스를 인자로 받아 사용할 때, 인터페이스의 속성 개수와 인자로 받는 객체의 속성 개수를 일치시키지 않아도 된다. 또한 인터페이스에 선언된 속성 순서를 지키지 않아도 된다.

  interface personAge {
    age: number;
  }

  function logAge(obj: personAge) {
    console.log(obj.age);
  }

  let person = { name: '덕수', age: 35 };
  logAge(person); // 35

⚙️ 변수와 인터페이스

  • 인터페이스는 변수의 타입으로 사용할 수 있다.

  • 인터페이스를 타입으로 선언한 변수는 해당 인터페이스를 준수해야한다. 그래서 새로운 타입을 정의하는 것과 유사하다.

  // 인터페이스 정의
  interface Todo {
    id: number;
    content: string;
    completed: boolean;
  }
  
  // 변수 todo의 타입으로 Todo 인터페이스 선언
  let todo: Todo;

  // 변수 todo는 Todo 인터페이스를 준수해야함
  todo = { id: 1, content: 'typescript', completed: true };

  • 인터페이스를 사용해 함수의 파라미터의 타입을 선언할 수 있다.

  • 예시에서 Todo라는 인터페이스를 정의하고, 이 인터페이스를 함수의 파라미터 todo 의 타입으로 사용한다.

  • addTodo 함수는 Todo 인터페이스를 준수하는 파라미터 todo를 받아 배열에 추가한다. 이렇게 하면 함수를 호출할 때 Todo 인터페이스의 구조를 따르는 객체만 전달할 수 있다.

  • 따라서 함수에 객체를 전달할 때 복잡한 매개변수 체크가 필요없어 유용하다.

  // 인터페이스의 정의
  interface Todo {
    id: number;
    content: string;
    completed: boolean;
  }

  let todos: Todo[] = [];

  // 파라미터 todo의 타입으로 Todo 인터페이스를 선언
  function addTodo(todo: Todo) {
    todos = [...todos, todo];
  }

  // 파라미터 todo는 Todo 인터페이스를 준수해야함
  const newTodo: Todo = { id: 1, content: 'typescript', completed: false };
  addTodo(newTodo);
  console.log(todos)
  // [ { id: 1, content: 'typescript', completed: false } ]

⚙️ 함수와 인터페이스

  • 인터페이스는 함수의 타입으로 사용할 수 있다.

  • 함수의 인터페이스에는 타입이 선언된 파라미터 리스트와 리턴 타입을 정의한다.

  • 예시를 보면, SuareFunc 이라는 함수 타입의 인터페이스를 정의하고, 해당 인터페이스를 구현하는 함수 squarFunc 을 선언했다.

  • 이 함수는 파라미터 num을 받고, number 타입을 반환하는 것이 해당 함수 인터페이스의 규칙이다.

  // 인터페이스의 정의
  interface SquareFunc {
    (num: number): number;
  }

  // 함수 인테페이스를 구현하는 함수는 인터페이스를 준수해야함
  const squareFunc: SquareFunc = function (num: number) {
    return num * num;
  }

  console.log(squareFunc(10)); // 100

⚙️ 클래스와 인터페이스

  • 클래스 선언문의 implements 뒤에 인터페이스를 선언하면, 해당 클래스는 지정된 인터페이스를 반드시 구현해야 한다.

  • 클래스가 특정 인터페이스를 구현한다면, 해당 클래스를 사용하는 코드에서는 인터페이스에서 정의한 메소드와 프로퍼티를 안전하게 활용할 수 있다.

  • 인터페이스와 클래스의 연결은 코드의 일관성을 유지하고, 재사용성과 확장성을 높인다.

  // 인터페이스 정의
  interface MemoI {
    id: number;
    content: string;
    completed: boolean;
  }

  // Memo 클래스는 MemoI 인터페이스를 구현해야함
  class Memo implements MemoI {
    constructor(public id: number, public content: string, public completed: boolean) {}
  }

  const memo = new Memo(1, 'typescript', false);
  console.log(memo);

  • 인터페이스는 프로퍼티 뿐만 아니라 메소드도 포함할 수 있다.

  • 위에서 말했듯이, 인터페이스를 구현하는 클래스는 인터페이스에서 정의한 프로퍼티와 추상 메소드를 반드시 구현해야 한다.

  // 인터페이스 정의
  interface NoteI {
    name: string;
    sayHoo(): void;
  }

  // 인터페이스를 구현하는 클래스는 인터페이스에서 정의한 프로퍼티와 추상 메소드를 반드시 구현해야함
  class Note implements NoteI {
    // 인터페이스에서 정의한 프로퍼티의 구현
    constructor(public name: string) {}

    // 인터페이스에서 정의한 추상 메소드 구현
    sayHoo() {
      console.log(`안녕, ${this.name} 노트필기를 하자!`);
    }
  }

  // 파라미터 note의 타입으로 NoteI 인터페이스를 선언
  function greeter(note: NoteI): void {
    note.sayHoo();
  }

  const no = new Note('덕배');
  greeter(no); // 안녕, 덕배! 노트필기를 하자! 

⚙️ 덕 타이핑 (duck typing)

" 오리처럼 꽥꽥 소리를 내면 오리다 "

  • 덕 타이핑은 위 말처럼 객체의 타입을 명시적으로 선언하지 않고도 객체의 사용 방식을 통해 타입을 결정하는 것이다.

  • 인터페이스를 구현했다는 것만이 타입 체크를 통과하는 유일한 방법은 아니다. 객체의 실제 타입보다는 객체가 가져야 하는 메소드나 프로퍼티의 존재 여부에 중점을 둔다.

  • 아래 예제를 보면, 인터페이스 DuckI는 quack 메소드를 정의하고 있다.
    makeNoise 함수는 인터페이스 DuckI를 구현한 클래스의 인스턴스 duck을 인자로 전달받는다.

  • 클래스 OneDuck은 인터페이스 DuckI를 구현했다. 클래스 TwoDuck은 인터페이스 DuckI를 구현하지 않았지만, quack 메소드를 갖는다.

  • makeNoise 함수에 인터페이스 DuckI를 명시적으로 구현하지 않은 클래스 TwoDuck의 인스턴스를 인자로 전달해도 에러 없이 처리된다.

  • 왜? 타입스크립트는 해당 인터페이스에서 정의한 프로퍼티나 메소드를 가지고 있다면, 그 인터페이스를 구현한 것으로 인정하기 때문이다.

  • 이것을 바로 덕 타이핑(duck typing) 또는 구조적 타이핑(structural typing)이라고 한다.

  interface DuckI {
    quack(): void;
  }

  class OneDuck implements DuckI {
    quack() {
      console.log('꽥꽥!');
    }
  }

  class TwoDuck {
    quack() {
      console.log('꽤애액꽤애액');
    }
  }

  function makeNoise(duck: DuckI): void {
    duck.quack();
  }

  makeNoise(new OneDuck()); // 꽥꽥!
  makeNoise(new TwoDuck()); // 꽤애액꽤애액

  • 변수에 사용할 경우의 예제도 보자! 변수 me 는 인터페이스 DuckI와 일치하지는 않지만, DuckI의 name 프로퍼티를 가지고 있기에 인정된다.

  • 참고로 이러한 기능은 개발 단계에서 도움을 주기 위해 제공되는 것이지 자바스크립트의 표준이 아니다. 따라서 아래의 예제의 타입스크립트 파일을 자바스크립트 파일로 트랜스파일링하면 인터페이스가 삭제된다.

  interface DuckI {
    name: string;
  }

  function sayHello(duck: DuckI): void {
    console.log(`안녕! ${this.name}`);
  }

  const me = { name: '덕배', age: 30 };
  sayHello(me); // 안녕! 덕배

⚙️ 선택적 프로퍼티 (Optional Property)

  • 인터페이스의 프로퍼티는 반드시 구현되어야 하지만, 선택적으로 필요한 경우가 있을 수 있다. 이럴 때는 프로퍼티명 뒤에 ? 를 붙이며 생략해도 에러가 발생하지 않는다.
  interface UserInfo {
    username: string;
    password: string;
    age?: number;
    address?: string;
  }

  const userInfo: UserInfo = {
    username: '최강덕배',
    password: '123456',
  };

  console.log(userInfo); 
  // { username: '최강덕배', password: '123456' }

⚙️ 인터페이스 상속

  • 인터페이스는 extends 키워드를 사용해 인터페이스 또는 클래스를 상속받을 수 있다.
  interface Mother {
    name: string;
    age?: number;
  }

  interface Son extends Mother {
    grade: number;
  }

  const son: Son = {
    name: '덕배',
    age: 30,
    grade: 3,
  };

  • 복수의 인터페이스를 상속받을 수도 있다.
  interface Mother {
    name: string;
    age?: number;
  }

  interface Father {
    phone: string;
  }

  interface Son extends Mother, Father {
    grade: number;
  }

  const son: Son = {
    name: '덕배',
    age: 30,
    grade: 3,
    phone: '010-1234-1234',
  };

  • 인터페이스는 클래스도 상속받을 수 있다. 단, 클래스의 모든 멤버가 상속되지만 구현까지 상속하지는 않는다.
  class Person {
    constructor(public name: string, public age: number) {}
  }

  interface Developer extends Person {
    skills: string[];
  }

  const developer: Developer =  {
    name: 'Lee',
    age: 20,
    skills: ['HTML', 'CSS', 'JavaScript']
  }


💡 클래스 (Class)

  • 타입스크립트에서 클래스는 객체 지향 프로그래밍의 핵심 요소로 사용된다.

  • 클래스는 데이터와 해당 데이터를 처리하는 메소드를 함께 묶어 놓는 구조를 제공한다.

  • ES6 클래스는 클래스 몸체에 메소드만 포함할 수 있고, 클래스 프로퍼티는 반드시 생성자 내부에서 선언하고 초기화한다.

  • 아래 예제는 ES6에서 문제없이 실행되는 코드지만, 확장자를 ts로 바꿔 타입스크립트 파일로 변경한 후, 컴파일하면 에러가 발생한다.

  // js파일
  class Person {
    constructor(name) {
      // 클래스 프로퍼티의 선언과 초기화
      this.name = name;
    }

    sayHoo() {
      console.log(`${this.name} 헬로우`);
    }
  }

  • 타입스크립트 클래스는 클래스 몸체에 클래스 프로퍼티를 사전 선언해야 한다.
  // ts파일
  class Person {
    // 클래스 프로퍼티 사전 선언
    name: string;
    
    constructor(name: string) {
      // 클래스 프로퍼티에 값을 할당
      this.name = name;
    }

    sayHoo() {
      console.log(`${this.name} 헬로우`);
    }
  }

  const person = new Person('덕배');
  person.sayHoo(); // 덕배 헬로우

⚙️ 접근 제어자 (Access modifier)

  • 타입스크립트 클래스는 클래스 기반 객체 지향 언어가 지원하는 private, public, protected 등의 접근 제어자를 사용하여 멤버 변수의 접근 권한을 제어할 수 있다.

  • 타입스크립트의 경우, 접근 제어자를 생략한 클래스 프로퍼티와 메소드는 암묵적으로 public이 선언된다. 따라서 public으로 지정하고자 하는 멤버 변수와 메소드는 접근 제한자를 생략한다.

  • private해당 클래스 내부에서만 접근 가능하다.

  • public기본값이며 어디서나 접근 가능하다.

  • protected해당 클래스와 해당 클래스를 상속한 자식 클래스에서 접근 가능하다.

  class Car {
    public speed: number;
    protected size: string;
    private color: string;

    constructor(speed: number, size: string, color: string) {
      this.speed = speed;
      this.size = size;
      this.color = color;
    }

    accelerate() {
      this.speed += 10;
    }

    getSpeed() {
      return `${this.speed}스피드로 ${this.color} 차를 타고 달려보자!`;
    }
  }

  const car = new Car(50, '대형차', 'black');

  car.accelerate();
  console.log(car.getSpeed()); // 60

  // public은 클래스 인스턴스를 통해 클래스 외부에서 참조 가능
  console.log(car.speed);

  // protected는 클래스 인스턴스를 통해 클래스 외부에서 참조 불가능
  // console.log(car.size); // error

  // private는 클래스 인스턴스를 통해 클래스 외부에서 참조 불가능
  // console.log(car.color); // error

  class Bike extends Car {
    constructor(speed: number, size: string, color: string) {
      super(speed, size, color);

      // public은 자식 클래스 내부에서 참조 가능
      console.log(this.speed);

      // protected는 자식 클래스 내부에서 참조 가능
      console.log(this.size);

      // private는 자식 클래스 내부에서 참조 불가능
      // console.log(this.color); // error
    }
  }

  • 접근 제어자는 생성자 파라미터에도 선언할 수 있다.

  • 접근 제어자가 사용된 생성자 파라미터는 암묵적으로 클래스 프로퍼티로 선언되고, 생성자 내부에서 별도의 초기화가 없어도 암묵적으로 초기화가 수행된다.

  class Car {
    // 접근 제어자가 선언된 생성자 파라미터 speed는 클래스 프로퍼티로 선언되고 자동으로 초기화됨
    // public이 선언되었으니, speed는 클래스 외부에서도 참조 가능
    constructor(public speed: number) {}
  }

  const car = new Car(50);
  console.log(car); // Car { speed: 50 }
  console.log(car.speed); // 50

  class Bike {
    // 접근 제어자가 선언된 생성자 파라미터 speed는 멤버 변수로 선언되고 자동으로 초기화됨
    // private가 선언되었으니, speed는 클래스 내부에서만 참조 가능
    constructor(private speed: number) {}
  }

  const bike = new Bike(50);
  console.log(bike); // Bike { speed: 50 }

  // private로 선언된 bike.speed는 클래스 내부에서만 참조 가능
  console.log(bike.speed); 
  // Property 'speed' is private and only accessible within class 'Bike'

  • 만일 생성자 파라미터에 접근 제어자를 선언하지 않으면 생성자 파라미터는 생성자 내부에서만 유효한 지역 변수가 되어 생성자 외부에서 참조가 불가능하다.
  class Car {
    // speed는 생성자 내부에서만 유효한 지역 변수다.
    constructor(speed: number) {
      console.log(speed);
    }
  }

  const car = new Car(50);
  console.log(car); // Car { }

⚙️ readonly 키워드

  • 타입스크립트는 readonly 키워드를 사용할 수 있고, readonly가 선언된 클래스 프로퍼티는 선언 시 또는 생성자 내부에서만 값을 할당할 수 있다.

  • 그 외의 경우에는 값을 할당할 수도, 변경할 수도 없고 오직 읽기만 가능한 상태가 된다.

  class Person {
    readonly name: string;

    constructor(name: string) {
      this.name = name; // 생성자에는 값을 할당할 수 있음
    }

    changeName(newName: string): void {
      // 오류: readonly 프로퍼티는 다른 메소드에서 값을 변경할 수 없음
      console.log('이름 변경 불가! 현재 이름은 ${this.name}')
    }
  }

  const person = new Person('덕배');
  console.log(person.name); // 읽기는 가능

  // person.name = '덕수'; 
  // 오류: readonly 프로퍼티는 외부에서 값을 변경할 수 없음

⚙️ static 키워드

  • ES6 클래스에서 static 키워드는 클래스의 정적(static) 메소드를 정의한다.

  • 정적 메소드는 클래스의 인스턴스가 아닌 클래스 이름으로 호출한다. 따라서 클래스의 인스턴스를 생성하지 않아도 호출할 수 있다.

  // js파일
  class Person {
    constructor(name) {
      this.name = name;
    }

    static staticMethod() {
      // 정적 메소드는 this를 사용 불가
      // 정적 메소드 내부에서 this는 클래스의 인스턴스가 아닌 클래스 자신을 가리킴
      return 'staticMethod';
    }

    prototypeMethod() {
      return this.name;
    }
  }

  // 정적 메소드는 클래스 이름으로 호출함
  console.log(Person.staticMethod()); // staticMethod

  const person = new Person('덕배');

  console.log(person.prototypeMethod()); // 덕배

  // 정적 메소드는 인스턴스로 호출할 수 없음
  // console.log(person.staticMethod());

  • 타입스크립트에서는 static 키워드를 클래스 프로퍼티에도 사용할 수 있다.

  • 정적 메소드와 마찬가지로 정적 클래스 프로퍼티는 인스턴스가 아닌 클래스 자체에 속하며, 클래스 이름으로 호출하고, 클래스의 인스턴스를 생성하지 않아도 된다.

  // ts파일
  class Person1 {
    static counter = 0;
    constructor() {
      Person1.counter++;
    }
  }

  const person1 = new Person1();
  const person2 = new Person1();

  console.log(Person1.counter); // 2

  // 정적 프로퍼티 및 메소드는 클래스의 인스턴스가 아니라,
  // 클래스 자체에 속해 있기 때문에 인스턴스를 통해 직접 접근할 수 없음
  // console.log(person2.counter); // error

⚙️ 추상 클래스 (abstract class)

  • 추상 클래스는 하나 이상의 추상 메소드를 가질 수 있고, 추상 메소드는 내용이 없이 메소드 이름과 타입만이 선언된 메소드를 말하며 선언할 때 abstract 키워드를 사용한다.

  • 추상 클래스는 일반 메소드도 포함할 수 있고, 이 일반 메소드는 구현을 가지며 하위 클래스에서 필요에 따라 오버라이딩될 수 있다.

  • 추상 클래스는 직접 인스턴스를 생성할 수 없고, 상속만을 위해 사용된다. 추상 클래스를 상속한 클래스는 추상 클래스의 추상 메소드를 반드시 구현하여야 한다.

  abstract class Animal {
    // 추상 메소드
    abstract makeSound(): void;
    // 일반 메소드
    move(): void {
      console.log('동물은 움직인다.')
    }
  }

  // 직접 인스턴스 생성 불가
  // new Animal();

  class Cat extends Animal {
    // 추상 클래스를 상속한 클래스는 추상 클래스의 추상 메소드를 반드시 구현해야함
    makeSound() {
      console.log('야옹~');
    }
  }

  const myCat = new Cat();
  myCat.makeSound(); // 야옹~
  myCat.move(); // 동물은 움직인다.
profile
할 수 있다고 믿는 사람은 결국 그렇게 된다 😄😊

0개의 댓글