«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
Recent Posts
Recent Comments
관리 메뉴

뉴히의 개발 로그

[TIL] 20230726 - TypeScript 유틸리티 타입: Partial, Required, ReadOnly, Pick, Omit / 객체지향프로그래밍, 클래스, 상속(inheritance), 추상클래스, 인터페이스(Interface), 객체 지향 설계 원칙 - S.O.L.I.D 본문

개발일지/TIL

[TIL] 20230726 - TypeScript 유틸리티 타입: Partial, Required, ReadOnly, Pick, Omit / 객체지향프로그래밍, 클래스, 상속(inheritance), 추상클래스, 인터페이스(Interface), 객체 지향 설계 원칙 - S.O.L.I.D

뉴히 2023. 7. 26. 23:53

enum과 object literal 비교

enum → 상수 값이기 때문에 각 멤버의 값이 변하면 안된다는 조건 이 있다
               간단한 상수 값의 경우 적합
object literal → 멤버의 값이나 데이터 타입을 맘대로 변경 할 수 있어
                         복잡한 구조 + 다양한 데이터 타입이 필요한 경우에 적합

enum

  • 열거형 데이터 타입
  • number, string 타입의 값만 대입
  • 컴파일 시에 값이 할당되어 있지 않으면 자동으로 숫자 값으로 매핑

object literal(객체 리터럴)

const obj = {
  a: [1,2,3],
  b: 'b',
  c: 4
}
  • 키 + 값의 쌍(pair)으로 구성된 객체를 정의
  • enum과 매우 흡사 하지만 enum이라는 키워드가 아니고 const라는 키워드를 사용 (const / let 사용가능)
  • 어떤 타입의 값도 대입을 할 수있다! 다양한 데이터 타입을 지원하며 유연한 구조를 가질 수 있다
  • 코드 내에서 사용하기 전에 값이 할당되어야 하므로, 런타임 에러를 방지

 

 

[ 타입 스크립트 유틸리티 타입 ]

partial<T>

  • 타입은 타입 T의 모든 속성을 선택적으로 만든다
  • 이를 통해 기존 타입의 일부 속성만 제공하는 객체를 쉽게 생성할 수 있다
interface Person {
  name: string;
  age: number;
}

const updatePerson = (person: Person, fields: Partial<Person>): Person => {
  return { ...person, ...fields };
};

const person: Person = { name: "Spartan", age: 30 };
const changedPerson = updatePerson(person, { age: 31 });

// 2번째 인자로 Partial<Person> 타입의 fields를 받고 한개의 속성만 있거나 두개다 있거나 해도 되지만. 추가적 속성은 넣을 수 없다.

----------------------------------------

// 필수 타입과 옵셔널 타입을 나눠서 쓸수 있다
type UserInformation = RequiredUserInformation & Partial<OptionalUserInformation>;

// 필수 타입
interface RequiredUserInformation {
  id: string;
  uid: string;
  name: string;
}
// 옵셔널 타입
interface OptionalUserInformation {
  age: number;
  profile: string;
  phone: string;
}

Required<T>

  • Required는 위의 Partial과 반대되는 개념이다. 제네릭 타입 T의 모든 프로퍼티에 대해 속성을 필수적으로 만들어준다
  • 모든 속성이 반드시 전부 제공이 되는 객체를 생성해야 할 때 쓴다
interface Person {
  name: string;
  age: number;
  address?: string; // 속성 명 뒤에 붙는 ?가 뭘까요
}

그런데 address?: 는 선택적 속성. 있어도 되고 없어되는 속성이다.

이럴때

type RequiredPerson = Required<Person>;

이렇게 Required<T> 타입을 통해 선언하면 address 입력도 필수가 되는 것

type Required<T> = {
  [P in keyof T]-?: T[P];
};

마이너스 연산자는 Optional을 제거해준다는 의미의 연산자이다.
Partial 타입과 동일하게 기존의 타입은 유지된 상태에서 Required 타입으로 변경된다.

 

Readonly<T>

  • Readonly<T> 타입은 타입 T의 모든 속성이 읽기 전용(read-only)
  • readonly 타입의 속성들로 구성된 객체가 아니어도 완전한 불변 객체로 취급할 수 있다.
interface DatabaseConfig {
  host: string;
  readonly port: number; // 인터페이스에서도 readonly 타입 사용 가능해요!
}

// 가변적 설정
const mutableConfig: DatabaseConfig = {
  host: "localhost",
  port: 3306,
};

// 불변적 설정
const immutableConfig: Readonly<DatabaseConfig> = {
  host: "localhost",
  port: 3306,
};

mutableConfig.host = "somewhere";
immutableConfig.host = "somewhere"; // 오류!

위코드를 보면

interface DatabaseConfig {
           host: string;
           readonly port: number; 
}

는 host가 readonly가 아니니 불변객체라고할 수 없지만.

const immutableConfig: Readonly<DatabaseConfig> = {
  host: "localhost",
  port: 3306,
};

ReadOnly<T> 타입으로 불변 객체로 만들 수 있다.

 

Pick<T,K>

  • 타입 T에서 K 속성들만 선택하여 새로운 타입을 만든다. 이를 통해 타입의 일부 속성만을 포함하는 객체를 쉽게 생성 가능
interface Person {
  name: string;
  age: number;
  address: string;
}

type SubsetPerson = Pick<Person, "name" | "age">;   //Person에서 "name" | "age"만 받을게

const person: SubsetPerson = { name: "Spartan", age: 30 };

SubsetPerson은 Person이라는 인터페이스에서 name, age 속성만 선택해서 구성된 새로운 타입이다

 

Omit<T,K>

  • Omit<T, K> 유틸리티 타입은 타입 T에서 K 속성들만 제외한 새로운 타입을 만든다. Pick과 반대의 동작
  • 기존 타입에서 특정 속성을 제거한 새로운 타입을 쉽게 생성할 수 있다
interface Person {
  name: string;
  age: number;
  address: string;
}

type SubsetPerson = Omit<Person, "address">; //Person 에서 "address" 이거만 제외하고 받을게

const person: SubsetPerson = { name: "Alice", age: 30 };

여기서 SubsetPerson 타입은 Person 타입에서 address 속성만 제외한 새로운 타입이다.

 

더만은 유틸리티 타입 링크

https://www.typescriptlang.org/ko/docs/handbook/utility-types.html

 

유틸리티 타입 참고 블로그 링크

https://velog.io/@ggong/Typescript%EC%9D%98-%EC%9C%A0%ED%8B%B8%EB%A6%AC%ED%8B%B0-%ED%83%80%EC%9E%85-2-Partial-Required-ReadOnly-Omit-NonNullable-ReturnType

 

 

 

TypeScript 프로젝트 생성

1) mkdir 폴더명 -> 해당 폴더로 이동
2) npm init -y
3) tsc --init --rootDir ./src --outDir ./dist --esModuleInterop --module commonjs --strict true --allowJS true --checkJS true
          --rootDir ./src            : 프로그램의 소스 파일이 들어가는 경로는 src 디렉토리입니다.
          --outDir ./dist             : 컴파일이 된 파일들이 들어가는 디렉토리는 dist 디렉토리입니다.
          --esModuleInterop    : CommonJS 방식의 모듈을 ES모듈 방식의 import 구문으로 가져올 수 있습니다!
4) 스크립트 코드 수정
"scripts": {
        "start": "tsc && node ./dist/index.js",
        "build": "tsc --build",
        "clean": "tsc --build --clean"
},

5) 프로젝트 내 src 폴더 생성
6) src/index.ts 파일 생성

코드 완료후 
빌드 npm run build
코드실행 npm run start

 

[ 객체 지향 프로그래밍 ]

 

객체 지향 프로그래밍(OOP)의 핵심! 클래스

 

클래스란?

객체를 만들기 위한 틀(template)

클래스에서는 같은 종류의 객체들이 공통으로 가지는 속성(attribute)과 메서드(method)를 정의

  • 속성은 객체의 성질을 결정
  • 메서드는 객체의 성질을 변화시키거나 객체에서 제공하는 기능들을 사용하는 창구
    • 객체는 클래스를 기반으로 생성되며 클래스의 인스턴스(instance)라고도 한다

클래스 정의

클래스의 속성과 메서드를 정의하고, new 키워드를 사용하여 객체를 생성할 수 있다.

class Person {
  name: string;
  age: number;

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

  sayHello() {
    console.log(`안녕하세요! 제 이름은 ${this.name}이고, 나이는 ${this.age}살입니다.`);
  }
}

const person = new Person('Spartan', 30);
person.sayHello();
  • 생성자(constructor)
    • 생성자는 클래스 내에 오직 하나만 존재
    • 생성자로 객체 속성을 초기화 하는것 뿐 아니라 객체가 생성이 될 떄 꼭 되어야 하는 초기화 로직을 집어넣기도 한다
    • 생성자는 인스턴스를 생성할 때 자동으로 호출된다

 

클래스 접근 제한자

클래스에서는 속성메서드접근 제한자를 사용해 접근을 제한할 수 있다.

 

1) public

  • 클래스 외부에서도 접근이 가능한 접근 제한자
  • 접근 제한자가 선언이 안되어있다면 기본적으로 접근 제한자는 public
  • 클래스의 함수 중 민감하지 않은 객체 정보를 열람할 때나 누구나 해당 클래스의 특정 기능을 사용해야 할 때 (사용자에게 노출이 될 수 있는 메서드를 만들때 주로 씀)

2) private

  • 클래스 내부에서만 접근이 가능한 접근 제한자
  • 클래스의 속성은 대부분 private으로 접근 제한자를 설정한다. 즉, 외부에서 직접적으로 객체의 속성을 변경할 수 없게 제한하는 것
  • 클래스의 속성을 보거나 편집하고 싶다면 별도의 getter/setter 메서드를 준비해놓는 것이 관례
    • getter : 값을 return 
    • setter : 전달된 값의 유효성 검사를 하거나, 이전 값의 history나 이런것 참고해서 값을 업데이트 해도 되는지 체크후 setting

3) protected

  • 클래스 내부와 해당 클래스를 상속받은 자식 클래스에서만 접근이 가능한 접근 제한자

 

상속(inheritance)

  • 기존 클래스의 속성과 메서드를 물려받아 새로운 클래스를 정의
  • 상속을 구현하려면 extends 키워드를 사용
// 부모 클래스
class Animal {
  name: string;

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

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

// 자식 클래스 : Animal 을 상속받을 Dog 클래스
class Dog extends Animal {
  age: number;
	
   // 나이가 추가된 클래스 생성
  constructor(name: string) {
    super(name); // super 키워드는 자식 클래스가 부모 클래스를 참조하는데 사용하는 키워드
    this.age = 5;
  }

  // 메서드 재정의 : 오버라이딩(override)
  makeSound() {
    console.log('멍멍!'); // '동물 소리~'  -> '멍멍!'
  }

  eat() { // Dog 클래스만의 새로운 함수 정의
    console.log('강아지가 사료를 먹습니다.');
  }
}

// 자식 클래스 2
class Cat extends Animal { // 자식 클래스가 부모 클래스의 생성자나 메서드를 그대로 사용
}

const dog = new Dog('누렁이');
dog.makeSound(); // 출력: 멍멍!

const cat = new Cat('야옹이');
cat.makeSound(); // 출력: 동물 소리~

 

서브타입, 슈퍼타입

  • 부모클래스 - 슈퍼타입
  • 자식클래스 - 서브타입

upcasting, downcasting

upcasting : 서브타입 → 슈퍼타입으로 변환하는것

  • 타입 변환은 암시적으로 이루어져 별도의 타입 변환 구문 필요 없이 슈퍼타입 변수에 대입만 하면됨 : TypeScript가 자동으로 해줌 
let dog: Dog = new Dog('또순이');
let animal: Animal = dog; // upcasting 발동! 
animal.eat(); // 에러. 슈퍼타입(Animal)으로 변환이 되어 eat 메서드를 호출할 수 없어요!

upcasting이 필요한 이유는 서브타입 객체를 슈퍼타입 객체로 다루면 유연하게 활용할 수 있기 때문

예를 들어, Dog, Cat, Lion 그리고 기타 등등 다양한 동물을 인자로 받을 수 있는 함수를 만들고 싶다면?

=> Animal 타입의 객체를 받으면 모두 다 받을 수 있음

 

downcasting : 슈퍼타입 → 서브타입으로 변환

let dog: Dog = new Dog('또순이');
let animal: Animal = dog; // upcasting 발동! 
animal.eat(); // 에러. 슈퍼타입(Animal)으로 변환이 되어 eat 메서드를 호출할 수 없어요!

as 키워드로 명시적으로 타입 변환을 해줘야 함

 

추상클래스

  • 인스턴스화를 할 수 없는 클래스 : 상속을 통해 자식 클래스에서 메서드를 제각각 구현하도록 강제를 하는 용도
  • 최소한의 기본 메서드는 정의 가능 : 핵심 기능의 구현은 전부 자식 클래스에게 위임
abstract class Shape {
  abstract getArea(): number; // 추상 함수 정의!!!

  printArea() {
    console.log(`도형 넓이: ${this.getArea()}`);
  }
}

class Circle extends Shape {
  radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  getArea(): number { // 이부분이 추상함수 : 원의 넓이를 구하는 공식은 파이 X 반지름 X 반지름
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle extends Shape {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    super();
    this.width = width;
    this.height = height;
  }

  getArea(): number { // 이부분이 추상함수 : 사각형의 넓이를 구하는 공식은 가로 X 세로
    return this.width * this.height;
  }
}

const circle = new Circle(5);
circle.printArea();

const rectangle = new Rectangle(4, 6);
rectangle.printArea();
  • 추상 클래스를 상속 받은 자식 클래스들은 반드시 getArea 함수를 구현해야 한다. 
  • 같은 도형이지만 구하는 공식은 다르기 때문에 추상함수 사용.

 

인터페이스(Interface)

  • 객체가 가져야 하는 속성과 메서드를 정의
  • 인터페이스를 구현한 객체는 인터페이스를 반드시 준수 해야한다. 어길 수 없음!
  • 인터페이스를 사용하면 코드의 안정성을 높이고 유지 보수성을 향상

interface(인터페이스) 사용하기
interface는 클래스나 객체에서 사용할 프로퍼티와 메소드를 클래스 외부에 따로 분리하여 선언할 수 있는 방법. 선언한 interface implements를 사용하여 클래스나 객체에 사용할 수 있습니다.

 

블로그 참조

https://webisfree.com/2022-01-17/[typescript]-2%ED%8E%B8-%ED%83%80%EC%9E%85%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%ED%81%B4%EB%9E%98%EC%8A%A4-%EC%83%9D%EC%84%B1-%EB%B0%8F-interface 

 

 

객체 지향 설계 원칙 - S.O.L.I.D

1) S(SRP. 단일 책임 원칙) →  매우 중요

  • 클래스는 하나의 책임만 가져야 한다는 매우 기본적인 원칙 : ex) 유저 서비스에서는 유저 관련된 액션만 해야되고 다른 액션을 해서는 안됨

2) O(OCP. 개방 폐쇄 원칙) → 인터페이스 혹은 상속을 잘 쓰자!

  • 클래스는 확장에 대해서는 열려 있어야 하고 수정에 대해서는 닫혀 있어야 한다
  • 인터페이스나 상속을 통해 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 함

3) L(LSP. 리스코프 치환 원칙)

  • 서브타입은 기반이 되는 슈퍼타입을 대체할 수 있어야 한다는 원칙
  • 자식 클래스는 부모 클래스의 기능을 수정하지 않고도 부모 클래스와 호환되어야 하며 논리적으로 엄격하게 관계가 정립 되어야 한다.

4) I(ISP. 인터페이스 분리 원칙)

  • 클래스는 자신이 사용하지 않는 인터페이스의 영향을 받지 않아야 한다.
  • 즉, 해당 클래스에게 무의미한 메소드의 구현을 막자

5) D(DIP. 의존성 역전 원칙)

  • 하위 수준 모듈(구현 클래스)보다 상위 수준 모듈(인터페이스)에 의존을 해야한다