[TYPESCRIPT] 리팩터링 예제로 보는 타입 개선 과정: 나쁜 TypeScript 타입을 좋은 타입으로 바꾸는 방법

 

앞선 글에서 타입 리뷰 체크리스트를 정리했다면, 이제 자연스럽게 이런 요구가 생깁니다. “이 기준을 실제 코드에 어떻게 적용해야 하는가?”입니다.

 

실무에서는 대부분 이런 상태의 코드를 마주합니다.

  • 이미 동작하고 있어서 쉽게 손대기 어렵다
  • 타입이 불편하지만, 어디가 문제인지 명확하지 않다
  • 한 번에 고치려다 리팩터링 범위가 커진다

 

추상적인 원칙 설명을 최소화하고, 실제 서비스 코드에서 자주 등장하는 타입을 단계적으로 개선해 나가는 과정을 예제로 보여주는 것에 집중합니다.

 

예제 배경: 흔히 볼 수 있는 서비스 코드

다음은 실무에서 매우 흔한 서비스 함수 시그니처입니다.

 

async function getUser(id: string): Promise<any> {
  // ...
}

 

처음에는 빠르게 만들기 좋지만, 이 타입은 아무 정보도 제공하지 않습니다.

  • 실패 가능 여부를 알 수 없다
  • 반환 데이터 구조를 알 수 없다
  • 호출부가 실수해도 컴파일 단계에서 막히지 않는다

 

1단계: 반환 구조부터 고정한다

가장 먼저 해야 할 일은 “반환값의 형태를 숨기지 않는 것”입니다.

 

type User = {
  id: string;
  email: string;
};

async function getUser(
  id: string,
): Promise<User | null> {
  // ...
}

 

이 단계에서 얻는 효과는 명확합니다.

  • 데이터 구조가 드러난다
  • null 처리 필요성이 호출부에 강제된다

 

2단계: 실패를 값으로 분리한다

User | null 구조는 단순하지만, 실패 이유를 알 수 없다는 한계가 있습니다.

 


type UserError =
  | { type: 'NOT_FOUND' }
  | { type: 'UNAUTHORIZED' };

 

type Result<T, E> =
  | { ok: true; data: T }
  | { ok: false; error: E };

async function getUser(
  id: string,
): Promise<Result<User, UserError>> {
  // ...
}

 

이제 호출부에서는

  • 실패 가능성을 무시할 수 없고
  • 실패 유형에 따라 분기 처리가 가능해집니다

 

3단계: 도메인과 규칙 타입을 분리한다

이 시점에서 중요한 점은 User 타입에는 어떤 제네릭도 붙지 않았다는 것입니다.

 

도메인 타입은 끝까지 단순하게 유지하고, 규칙은 바깥에서 감쌉니다.

 

type WithAudit<T> = T & {
  audit: {
    requestId: string;
  };
};

async function getUser(
  id: string,
): Promise<Result<WithAudit<User>, UserError>> {
  // ...
}

 

이 타입 시그니처만 보고도 다음을 알 수 있습니다.

  • 정상/실패 흐름이 존재한다
  • 실패 유형이 제한되어 있다
  • 운영을 위한 메타 정보가 포함된다

 

4단계: 과한 타입을 제거한다

리팩터링을 하다 보면 다음과 같은 유혹이 생깁니다.

 

// 과한 예시
type ServiceResponse<T, E, M> = {
  ok: boolean;
  data?: T;
  error?: E;
  meta?: M;
};

 

이 타입은 유연해 보이지만, 실제로는 아무 실수도 막아주지 않습니다.

 

이 시점에서 체크리스트를 다시 적용합니다.

  • 실제 실수를 막는가? → 아니다
  • 변경 비용이 합리적인가? → 아니다

 

그래서 이 타입은 제거 대상입니다.

 

운영 관점에서 달라지는 점

이런 단계적 리팩터링을 거치면, 운영에서도 변화가 생깁니다.

  • 로그에서 실패 유형을 바로 분류할 수 있다
  • 호출부에서 실패 누락이 줄어든다
  • 핫픽스 시 영향 범위를 예측하기 쉬워진다

 

실무 리팩터링 요약 체크리스트

  • any부터 제거했는가
  • 반환 구조가 타입에 드러나는가
  • 실패가 값으로 표현되는가
  • 도메인 타입이 단순하게 유지되는가
  • 불필요한 제네릭을 제거했는가

 


 

 

좋은 타입 리팩터링은 한 번에 완성되지 않습니다.

any 제거 → 흐름 노출 → 실패 분리 → 과한 타입 제거 이 순서를 지키면, 기존 코드를 망가뜨리지 않으면서도 타입 안정성을 단계적으로 끌어올릴 수 있습니다.