이 글에선 Typescript에서 사용되는 형식인 Union 형식, Intersection 형식에 대해서 알아보고,
필자가 가졌던 의문인 Union형식을 사용할거면, 상속을 사용하는 것이 낫지 않은가? 에 대해 설명을 하고,
Union 형식이 사용되는 간단한 예시를 소개할 예정이다.
Union형식과 Intersection형식
Union 형식
Union 형식은 여러 타입 중 하나의 타입을 가질 수 있도록 정의한다. 이는 상속의 개념에서 부모 클래스와 비슷하게 동작한다. | 연산자를 사용해 여러 타입을 조합할 수 있다.
예를 들어, Circle과 Rectangle을 대표하는 Shape 타입을 Union 형식으로 정의할 수 있다.
type Circle = {
radius: number;
};
type Rectangle = {
x: number;
y: number;
};
type Shape = Circle | Rectangle;
function area(shape: Shape) {
// 계산..
}
위와 같은 상황 뿐만 아니라 아래처럼 기본형으로도 유니온형식을 만들 수 있다.
function foo(bar: number | string){
//
}
kind 속성
위처럼 구현하게 되면, 좋다 부모클래스를 만든것 까지는 좋다 하지만, area 내부에서 대체 이들을 어떻게 구별할 것인가? 위의 예제에서는 Circle과 Rectangle 구분하기 쉽기 때문에 아래와 같이 구분할 수 있다.
function area(shape: Shape){
if("radius" in shape) { // Circle의 경우
return Math.PI*shape.radius*shape.radius;
} else { // 나머지 즉 Rectangle의 경우
return shape.x*shape.y;
}
}
하지만 유니온 형식에 포함되는 타입이 많아져서 프로퍼티가 구별하기 힘들정도라면? 어떻게 구별할 것인가. 그래서, kind 속성이 등장했다. 유니온형식의 요소를 구별하기 위함이다.
type Circle = {
radius: number;
kind: "circle";
};
type Rectangle = {
x: number;
y: number;
kind: "rectangle"
};
type Shape = Circle | Rectangle;
위 방식을 통해 유니온형식의 구성요소 하나하나를 구별할 수 있고, switch문을 사용해 아래와 같이 함수를 작성할 수 있다.
function area(shape: Shape){
switch (shape.kind) { // kind를 통해 구성요소를 구별
case "circle":
return Math.PI*shape.radius*shape.radius;
case "rectangle"
return shape.x*shape.y;
default:
throw Error("불가능한 방식입니다.");
}
}
Intersection 형식
Intersection형식은 자식클래스를 만드는것에 비유할 수 있다. & 키워드를 통해 지정한다. 또한 예시를 통해 살펴보자
type Toy = {
price: number;
quantity: number;
};
type BoardGame = Toy & {
players: number;
};
type Puzzle = Toy & {
pieces: number;
};
vs 상속
TypeScript를 공부하며, "Union 형식을 사용할 바에 차라리 상속을 사용하는 것이 더 나은 선택이 아닐까?"라는 의문이 들었다. 하지만 Union 형식과 상속은 서로 다른 상황에서 더 적합한 방법으로 사용될 수 있다. 이를 가독성과 확장성 측면에서 비교해 보았다.
가독성
Union 형식은 코드가 간결하고 직관적이다. 특히 타입이 간단한 경우, 클래스 문법을 사용하는 것보다 가독성이 좋다.
아래 두 코드를 비교했을때 Union 형식을 사용하는 쪽이 어떤 type이 있는지, 어떤 동작을하는지 한눈에 알아보기가 쉽다. 상속을 사용하는 경우 코드의 규모가 커질수록 복잡하고 알아보기 힘들어진다.
// Union 형식
type Shape =
{kind: "circle"; radius: number} |
{kind: "rectangle"; x: number; y: number} |
{kind: "triangle"; base: number; height: number};
function area(shape: Shape){
switch (shape.kind) { // kind를 통해 구성요소를 구별
case "circle":
return Math.PI*shape.radius*shape.radius;
case "rectangle":
return shape.x*shape.y;
case "triangle":
return shape.base*shape.height/2;
default:
throw Error("불가능한 방식입니다.");
}
}
// 상속
interface Shape {
area(): number;
}
class Circle implements Shape{
constructor(public radius: number){}
area(): number {
return Math.PI*this.radius*this.radius;
}
}
class Rectangle implements Shape{
constructor(public x: number; public y: number;){}
area(): number {
return this.x*this.y;
}
}
class Triangle implements Shape{
constructor(public base: number; public height: number){}
area(): number {
return this.base*this.height/2;
}
}
확장성
확장성의 면에서도 이점이 있다. type과 Union 형식은 OOP의 철학을 따르진 않지만, OCP(개방폐쇄의 원칙)을 자연스럽게 만족한다. 새로운 타입을 추가할 때 기존 코드를 수정할 필요 없이 새로운 타입만 정의하면 된다.
type Shape =
{kind: "circle"; radius: number} |
{kind: "rectangle"; x: number; y: number} |
{kind: "triangle"; base: number; height: number} |
{kind: "square"; x: number}; // 새로운 타입을 추가
function area(shape: Shape){
switch (shape.kind) { // kind를 통해 구성요소를 구별
case "circle":
return Math.PI*shape.radius*shape.radius;
case "rectangle":
return shape.x*shape.y;
case "triangle":
return shape.base*shape.height/2;
case "square": // 새로 추가된 타입에 대한 처리
return shape.x*shape.x;
default:
throw Error("불가능한 방식입니다.");
}
}
결론
Union형식은 때에 따라선 OOP를 따르지 않고 사용할 수 있는 하나의 방법이다.
- 데이터 중심적 모델, 메서드(즉, 행동)이 중점이 아닌 상황
- 그 구조가 매우 간단하여 class문법을 쓰기엔 오버헤드가 큰 상황
결론적으로, Union 형식은 OOP를 따르지 않아도 효율적이고 간단한 해결책을 제공할 수 있는 강력한 도구이다. 상황에 따라 Union 형식을 선택하면 코드의 간결성과 확장성을 모두 확보할 수 있다.
위 내용을 공부하면서 내가 너무 OOP에 갇혀있다는 생각을 했다. type과 Union을 보면서 이는 OOP의 원칙을 만족하는가 (애초에 Union은 OOP가 말하는 객체조차도 아니었지만) 와 같은 생각을 하며 계속 Union에서 OOP의 편린을 찾으려고 애를 썼다. 처음에는 심지어 Union 형식을 보며 화도 났다. 어떻게 이렇게 코드를 짤 수 있냐며. 위 내용을 공부하면서 내 갇혀있던 생각도 좀 더 넓게 생각하게 될 수 있어진 것 같다.
'Web' 카테고리의 다른 글
Storybook (0) | 2024.12.20 |
---|---|
프론트엔드를 좀 더 자세히 들여다 보기 위해서 (0) | 2023.09.12 |