뉴히의 개발 로그
[TIL] 20230726 - TypeScript 유틸리티 타입: Partial, Required, ReadOnly, Pick, Omit / 객체지향프로그래밍, 클래스, 상속(inheritance), 추상클래스, 인터페이스(Interface), 객체 지향 설계 원칙 - S.O.L.I.D 본문
[TIL] 20230726 - TypeScript 유틸리티 타입: Partial, Required, ReadOnly, Pick, Omit / 객체지향프로그래밍, 클래스, 상속(inheritance), 추상클래스, 인터페이스(Interface), 객체 지향 설계 원칙 - S.O.L.I.D
뉴히 2023. 7. 26. 23:53enum과 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
유틸리티 타입 참고 블로그 링크
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를 사용하여 클래스나 객체에 사용할 수 있습니다.
블로그 참조
객체 지향 설계 원칙 - S.O.L.I.D
1) S(SRP. 단일 책임 원칙) → ⭐ 매우 중요 ⭐
- 클래스는 하나의 책임만 가져야 한다는 매우 기본적인 원칙 : ex) 유저 서비스에서는 유저 관련된 액션만 해야되고 다른 액션을 해서는 안됨
2) O(OCP. 개방 폐쇄 원칙) → 인터페이스 혹은 상속을 잘 쓰자!
- 클래스는 확장에 대해서는 열려 있어야 하고 수정에 대해서는 닫혀 있어야 한다
- 인터페이스나 상속을 통해 기존 코드를 변경하지 않고도 기능을 확장할 수 있어야 함
3) L(LSP. 리스코프 치환 원칙)
- 서브타입은 기반이 되는 슈퍼타입을 대체할 수 있어야 한다는 원칙
- 자식 클래스는 부모 클래스의 기능을 수정하지 않고도 부모 클래스와 호환되어야 하며 논리적으로 엄격하게 관계가 정립 되어야 한다.
4) I(ISP. 인터페이스 분리 원칙)
- 클래스는 자신이 사용하지 않는 인터페이스의 영향을 받지 않아야 한다.
- 즉, 해당 클래스에게 무의미한 메소드의 구현을 막자
5) D(DIP. 의존성 역전 원칙)
- 하위 수준 모듈(구현 클래스)보다 상위 수준 모듈(인터페이스)에 의존을 해야한다