smob_logo
smob_logo
header-image
TYPESCRIPTTypescript Class로 상수 관리하기TypeScript에서 클래스를 사용하여 열거형(Enum) 관리 및 확장하기2024.07.16

일반적으로 TypeScript에서 상수를 관리할 때 주로 사용되는 enum은 간결하고 직관적인 방법을 제공하지만, 복잡한 데이터 구조를 표현하기에는 제한적입니다. enum은 기본적으로 키와 값의 쌍만으로 이루어져, 상수에 추가적인 정보를 담기 위해서는 별도의 함수나 변수를 정의해야합니다. 예를 들어:

// 지점 정보
enum Park {
  GOYANG = 0,
  DAEJEON = 1,
}

const PARK_NAME_MAP: Record<Park, string> = {
  [Park.GOYANG]: '고양점',
  [Park.DAEJEON]: '대전점',
};

const Component = () => {
  const [park, setPark] = useState<Park>(Park.GOYANG);

  return (
    <div>
      <h1>{PARK_NAME_MAP[park]}</h1>
      {Object.keys(PARK_NAME_MAP).map((p) => (
        <button key={p} type='button' onClick={() => setPark(p)}>
          {PARK_NAME_MAP[p]}
        </button>
      ))}
    </div>
  );
};

이 방식은 단순한 경우에는 유용하지만, 도메인 지식이 많아지면서 변수 관리가 복잡해지고 코드의 응집력이 떨어질 수 있습니다. 또한, 정보가 분산되어 유지보수가 어려워지며, 팀원 간의 지식 공유가 힘들어져 코드의 가독성과 확장성에 악영향을 미칠 수 있습니다.

Class로 Enum 만들기(?)

쉽게 구현할 수 있도록 ts-enum 라이브러리를 사용하여 Class 방식으로 전환해 보겠습니다.

// ts-enum 간단한 기능 소개

@Enum('uid') // enum에서 고유 식별자(uid) 필드를 사용하도록 설정합니다.
class Park extends EnumType<Park>() {
  // 고양점을 나타내는 상수, uid와 이름을 생성자에 전달합니다.
  static readonly GOYANG = new Park(0, '고양점');
  // 대전점을 나타내는 상수, uid와 이름을 생성자에 전달합니다.
  static readonly DAEJEON = new Park(1, '대전점');

  constructor(
    public readonly uid: number, // 각 enum 인스턴스의 고유 식별자입니다.
    private readonly _name: string, // 각 enum 인스턴스의 이름을 나타내는 필드입니다.
  ) {
    super(); // EnumType의 생성자를 호출합니다.
  }

  getName() {
    return this._name;
  }
}

// valueOf는 @Enum() 에 선언된 필드를 통해 찾을 수 있다
Park.valueOf(0) === Park.GOYANG; // true

// values는 선언된 모든 enum 인스턴스를 배열로 반환합니다.
Park.values(); // [Park.GOYANG, Park.DAEJEON]

위의 방법으로 기존 코드를 아래처럼 수정 할 수 있습니다.

@Enum('uid')
class Park extends EnumType<Park>() {
  static readonly GOYANG = new Park(0, '고양점');
  static readonly DAEJEON = new Park(1, '대전점');

  constructor(public readonly uid: number, private readonly _name: string) {
    super();
  }

  getName() {
    return this._name;
  }
}

const ClassEnumPlayGround = () => {
  const [park, setPark] = useState<Park>(Park.GOYANG);

  return (
    <div>
      <h1>{park.getName()}</h1>
      {Park.values().map((p) => (
        <button key={p.uid} type='button' onClick={() => setPark(p)}>
          {p.getName()}
        </button>
      ))}
    </div>
  );
};

새로운 상수 필드 추가의 용이성

이 방법을 사용하면, 새로운 지점 정보의 필드가 추가될 때도 이미 설계된 객체 구조에 맞춰 TypeScript와 IDE의 지원을 받아 간단히 필드값을 추가할 수 있습니다.

스크린샷 2024-09-05 14.17.34.png

상수 값 추가의 용이성

새로운 상수 값을 기존 객체 구조에 맞게 쉽게 추가할 수 있습니다. 이 방식은 코드를 직관적이고 관리하기 쉽게 만들어 팀원 간 협업을 촉진하고 전체 개발 프로세스의 효율성을 높입니다.

스크린샷 2024-09-05 15.41.30.png

상태에 대한 행위를 한 곳에서 관리가능

클래스로 되어 있기 때문에 지점에 따른 비즈니스 로직을 Class 메소드를 통해서 표현할 수 있어 응집력있고 추상화된 코드를 작성할 수 있습니다. 예를 들어, F&B가 있는 지점을 빨간색으로, 없는 지점을 파란색으로 표시해야하는 요구사항이 주어진다고 가정하면 아래 처럼 작성 할 수 있습니다.

@Enum('uid')
class Park extends EnumType<Park>() {
  ...
  // 지점 정보에 대한 비즈니스 로직 ex) F&B가 있는 지점을 빨간색으로 아니면 파란색으로 표시해줘!
  getColor() {
    return this._existsFnb ? 'red' : 'blue';
  }
}

const ClassEnumPlayGround = () => {
	...
  return (
    <div style={{ backgroundColor: park.getColor() }}>
      ...
    </div>
  );
};

객체들 간의 상호작용 가능

이 방법을 사용하면, Class로 정의된 객체들이 서로 메시지를 주고받을 수 있습니다. 예를 들어, 서버로부터 오는 JSON 형식의 티켓 데이터로 예시를 들어보겠습니다.

interface Ticket {
  name: string;
  price: number;
  park: {
    uid: number; // 티켓을 사용할 수 있는 지점 uid
  };
}

...

const [ticket, setTicket] = useState<Ticket | undefined>(undefined);

useEffect(() => {
  // 서버에서 오는 데이터로 가정
  const json: Ticket = {
    name: '2시간 패스',
    price: 10000,
    park: {
      uid: 1,
    },
  };
  setTicket(json);
}, []);

console.log(ticket); // { name: '2시간 패스', price: 10000, park: { uid: 1 } }

이 데이터를 단순히 JSON으로 직접 사용할 수 있지만, Class로 전환하여 사용함으로써 요구사항에 맞는 로직을 객체 내부에서 작성할 수 있습니다.

class TicketClass {
  private constructor(private readonly _ticket: Ticket) {}

  static fromJSON(json: Ticket): TicketClass {
    return new TicketClass(json);
  }
}

const [ticket, setTicket] = useState<TicketClass | undefined>(undefined);

useEffect(() => {
  // 서버에서 오는 데이터로 가정
  const json: Ticket = {
    name: '2시간 패스',
    price: 10000,
    park: {
      uid: 1,
    },
  };

  // Class로 변환하여 저장
  setTicket(TicketClass.fromJSON(json));
}, []);

console.log(ticket); // TicketClass { _ticket: { name: '2시간 패스', price: 10000, park: { uid: 1 } } }

이와 같이 구현하면, JSON 필드를 직접 사용하지 않고 도메인에 적합한 메서드를 통해 데이터를 처리할 수 있습니다.

class TicketClass {
  private constructor(private readonly _ticket: Ticket) {}

  static fromJSON(json: Ticket): TicketClass {
    return new TicketClass(json);
  }

  getName() {
    return this._ticket.name;
  }

  getDisplayPrice() {
    return Math.max(0, this._ticket.price);
  }
}

지점에 관련된 정보도 TicketClass 내부에서 Park 클래스로 변환하여 객체간의 메세지를 주고 받을 수 있습니다.

class TicketClass {
  private constructor(private readonly _ticket: Ticket) {}

...

	// 위에서 작성한 Park Class로 변환하여 사용
  private _getPark(): Park {
    return Park.valueOf(this._ticket.park.uid);
  }
}

예를 들어 해당 티켓을 사용할 수 있는 지점에 F&B가 존재하는 경우 영수증 출력이 필요한다는 로직을 구현해야 한다면 다음과 같이 구현할 수 있습니다.

class TicketClass {
  ...
  // 해당 티켓이 영수증 프린트가 필요한지 여부
  isNeedPrintReceipt() {
    return this._getPark().isExistsFnb();
  }
}

이렇게 Class 기반으로 데이터를 관리함으로써, 책임과 역할이 분리된 가독성이 높은 코드를 구현할 수 있습니다.

전체 코드

import { useEffect, useState } from 'react';
import { Enum, EnumType } from 'ts-jenum';

@Enum('uid')
class Park extends EnumType<Park>() {
  static readonly GOYANG = new Park(0, '고양점', true);
  static readonly DAEJEON = new Park(1, '대전점', false);
  static readonly Suwon = new Park(2, '수원점', true);

  private constructor(
    public readonly uid: number,
    private readonly _name: string,
    private readonly _existsFnb: boolean,
  ) {
    super();
  }

  getName() {
    return this._name;
  }

  getColor() {
    return this._existsFnb ? 'red' : 'blue';
  }

  isExistsFnb() {
    return this._existsFnb;
  }
}

interface Ticket {
  name: string;
  price: number;
  park: {
    uid: number;
  };
}

class TicketClass {
  private constructor(private readonly _ticket: Ticket) {}

  static fromJSON(json: Ticket): TicketClass {
    return new TicketClass(json);
  }

  getName() {
    return this._ticket.name;
  }

  getDisplayPrice() {
    return Math.max(0, this._ticket.price);
  }

  isNeedPrintReceipt() {
    return this._getPark().isExistsFnb();
  }

  private _getPark(): Park {
    return Park.valueOf(this._ticket.park.uid);
  }
}

const ClassEnumPlayGround = () => {
  const [park, setPark] = useState<Park>(Park.GOYANG);

  const [ticket, setTicket] = useState<TicketClass | undefined>(undefined);

  useEffect(() => {
    // 서버에서 오는 데이터로 가정
    const json: Ticket = {
      name: '2시간 패스',
      price: 10000,
      park: {
        uid: 1,
      },
    };

    setTicket(TicketClass.fromJSON(json));
  }, []);

  return (
    <div style={{ backgroundColor: park.getColor() }}>
      <h1>{park.getName()}</h1>
      <h2>{ticket?.getName()}</h2>
      <h3>{ticket?.getDisplayPrice()}</h3>
      <h4>{ticket?.isNeedPrintReceipt() ? '영수증 출력이 필요합니다' : ''}</h4>
      {Park.values().map((p) => (
        <button key={p.uid} type='button' onClick={() => setPark(p)}>
          {p.getName()}
        </button>
      ))}
    </div>
  );
};
#react
#typescript
avatar
By. 정규재안녕하세요. 잘 부탁 드립니다.