본문 바로가기
개발공부_Blog/Project

하드코딩 된 카테고리, 중앙 관리로 리팩토링[2]

by 소팡팡 2025. 9. 18.

Problem

카테고리는 라우터, API, UI, 검색 필터 등 여러 레이어에 걸쳐 반복적으로 등장하는 핵심 도메인 개념이다. 처음 블로그 프로젝트를 시작했을 때는 카테고리를 "TechNote", <FcAnswers />처럼 컴포넌트마다 직접 하드코딩해 사용했다.

하지만 이런 중요한 값을 한 곳에 정의하지 않고 중구난방으로 쓰다 보니, 오타가 발생하거나 “이게 맞는 값이었나?”를 일일이 확인해야 하는 번거로움이 생겨났다.

또한 DB에 저장되는 카테고리명과 UI에 보여지는 카테고리명이 달라, 이를 매번 구분하고 맞춰주는 작업이 불필요하게 늘어났다. 새로운 카테고리를 추가하거나 이름을 바꾸려 할 때마다 프로젝트 전역에서 대규모 수정이 필요하겠다는 점도 문제라는 생각이 들었다.

바로 이전에 라우팅을 상수화해 리팩토링했던 것이 떠올랐다. “카테고리도 중앙에서 관리한다면 이 문제를 해결할 수 있지 않을까?”라는 생각으로 리팩토링을 시작했다.

 

Solution

1. 카테고리 타입 정의

우선 카테고리를 리터럴 튜플 타입으로 정의해 오타를 원천 차단했다. as const 를 붙여 배열 원소 하나하나를 타입으로 만들어 Category타입은 "tech-notes", "thoughts", "deepdives", "portfolio" 로 자동 추론되도록 하였다.

// 리터럴 튜플 타입 (배열)
export const CATEGORIES = ["tech-notes","thoughts","deepdives","portfolio"] as const;

// Category는 "tech-notes" | "thoughts" | "deepdives" | "portfolio"로 자동 추론
// 리터럴 타입이 됨 ( 원소 하나의 타입이 됨 ) 
export type Category = (typeof CATEGORIES)[number];

 

이제 허용된 문자열만 카테고리로 사용할 수 있어 타입 안전성이 확보된다.

 

2. 도메인 데이터와 UI 데이터 분리

DB와 라우터에서 사용하는 카테고리 값은 슬러그 형태("tech-notes")가 적합하지만, UI에 그대로 보여주기엔 사용자 친화적이지 않다고 판단했다. 이 두 값을 매번 확인하지 않고 어떻게 사용할 수 있을까 고민했다.

이를 해결하기 위해 **CATEGORY_META** 객체를 만들어, 카테고리 값과 UI 정보를 매핑했다.

export const CATEGORY_META: Record<Category,{label:string; icon:IconType}> = 
{
  "tech-notes": { label: "Tech Notes", icon: FcAnswers }, // icon컴포넌트 함수
  thoughts: { label: "Thoughts", icon: FcIdea },
  deepdives: { label: "Deep Dives", icon: FcComments },
  portfolio: { label: "Portfolio", icon: FcCamera }
};
  • 내부 데이터: "tech-notes" 같은 슬러그 값 (DB, 라우터, 검색 등 내부 식별용)
  • UI 데이터: "Tech Notes" 라벨 + 아이콘 등 카테고리의 UI정보 (사용자 친화적 표시)

이렇게 하면 내부 로직과 UI 표시 정책이 분리되어 확장성과 유지보수성이 높아진다.

 

3. 컴포넌트 리팩토링

기존에는 메뉴 버튼에 경로, 아이콘, 라벨을 하드코딩했다. 이 방식은 오타 위험도 크고, 디자인이 바뀌면 모든 컴포넌트를 수정해야 했다.

<Link to={"/menu/tech-notes"}><FcAnswers size={20} /> TechNote</Link>
<Link to={"/menu/thoughts"}><FcIdea size={20} /> Thought</Link>

리팩토링 후에는 CATEGORY_META와 CATEGORIES를 기반으로 반복 렌더링하도록 변경했다. 이제 메뉴에 카테고리를 추가하거나 변경할 때는 CATEGORY_META만 수정하면 전역 반영된다.

export const MenuButtons = () => (
  <nav>
    {CATEGORIES.map((category) => {
      const Icon = CATEGORY_META[category].icon;
      return (
        <Link to={`/menu/${category}`} onClick={onClose}>
          <Icon size={20} />
          <span>{CATEGORY_META[category].label}</span>
        </Link>
      );
    })}
  </nav>
);

 

 

Result

리팩토링 이후 다음과 같은 효과를 얻을 수 있었다.

⇒ 카테고리를 중앙에서 타입+메타데이터로 관리하여, 프로젝트 전역에 일관성, 안전성, 유지보수성, 확장성을 동시에 확보하였다.

 

단일 출처 관리 (SSOT, Single Source of Truth)

카테고리 값과 UI 정보가 한 곳에 모여 있어, 수정 시 전역적으로 일관성 있게 반영된다.

타입 안전성 확보

Category 타입으로 유효하지 않은 값은 컴파일 단계에서 차단된다.

 

UI/UX 일관성

같은 카테고리가 어느 페이지에서든 동일한 라벨·아이콘으로 표현되어 사용자 경험이 일관되괴 신뢰성이향상된다.

 

표시 정책 분리

내부 데이터("tech-notes")와 사용자에게 보여줄 UI 데이터("Tech Notes", 아이콘, 설명)를 분리하여 가독성·SEO 모두 강화된다.

 

문서화 효과

CATEGORY_META.ts 자체가 프로젝트의 “카테고리 스펙 문서” 역할을 하게 된다.

 

 

TypeScript Tip

자주 사용하게 될 타입스크립트 TIP MEMO

카테고리를 리팩토링 하면서 타입스트립트에 대해 더 깊이 알아가게 되는 시간이기도 했다.

** as const
: 리터럴 튜플 타입을 만들어줌

export const CATEGORIES = ["tech-notes","thoughts","deepdives","portfolio"] as const;

 

 

** (typeof CATEGORIES) [number]
: 위의 튜플 타입의 데이터를 각각의 리터럴로 만들어줌 = 배열 원소 각각을 리터럴 타입으로 추출

export type Category = (typeof CATEGORIES)[number];

 

 

** Record<K,T>
: 특정 키 집합(K)에 대해 값 타입(T)을 강제하는 유틸리티 타입

Category의 tech-notes의 타입에는 label과 icon의 형태가 있는 값이어야 하고

각 타입은 string과 IconType으로 지정한 바와 같다. 라는 의미의 타입 지정이 가능하다.

export const CATEGORY_META: Record<Category,{label:string; icon:IconType}> = 
{
  "tech-notes": { label: "Tech Notes", icon: FcAnswers }, // icon컴포넌트 함수
  thoughts: { label: "Thoughts", icon: FcIdea },
  deepdives: { label: "Deep Dives", icon: FcComments },
  portfolio: { label: "Portfolio", icon: FcCamera }
};

 

 

카테고리 관리 구조를 정리하면서, 이전에 라우팅 리팩토링에서 느꼈던 장점들을 다시 확인할 수 있었다.

지금은 혼자 개발하고 있지만 데이터의 중앙 집중 관리는 협업 상황에서도 더욱 도움이 될 것 같다고 느꼈다.

댓글