[TYPESCRIPT] 리팩터링 우선순위 예제로 보는 타입 개선 전략: 어디서부터 손대야 효과가 나는가

 

TypeScript 리팩터링을 하기로 마음먹었을 때, 가장 먼저 막히는 지점은 기술이 아니라 순서입니다. “이 타입부터 고쳐야 할까, 저 타입부터 고쳐야 할까?” 잘못 시작하면 노력 대비 효과가 거의 없거나, 오히려 리팩터링 피로도만 높아집니다.

 

실무에서 자주 보는 실패 패턴은 다음과 같습니다.

  • 공통 유틸 타입부터 손댔다가 영향 범위가 너무 커진다
  • 도메인 모델을 먼저 고쳐서 연쇄 수정이 발생한다
  • 타입은 예뻐졌는데 실제 버그는 그대로다

 

 “어디서부터 고쳐야 하는가”를 원칙이 아니라 실제 서비스 코드 예제를 통해 설명합니다. 목표는 하나입니다. 가장 적은 수정으로 가장 큰 안정성을 얻는 것입니다.

 

리팩터링 대상 예제: 실제로 흔한 구조

다음은 많은 Node.js/TypeScript 백엔드에서 볼 수 있는 API 호출 구조입니다.

 

// controller
const user = await userService.getUser(userId);
return res.json(user);

 

// service
async function getUser(id: string): Promise<any> {
  const user = await repo.findById(id);
  if (!user) {
    throw new Error('NOT_FOUND');
  }
  return user;
}

 

이 코드는 동작은 합니다. 하지만 타입 관점에서는 문제가 명확합니다.

  • any로 인해 모든 실수가 통과된다
  • throw 기반 에러로 흐름이 숨겨진다
  • 컨트롤러는 실패 가능성을 알 수 없다

 

우선순위 1: 컨트롤러-서비스 경계부터 고친다

가장 먼저 손대야 할 곳은 도메인 내부가 아니라 레이어 경계입니다.

 


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

 


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

 


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

 

async function getUser(
  id: string,
): Promise<Result<User, UserError>> {
  const user = await repo.findById(id);
  if (!user) {
    return { ok: false, error: { type: 'NOT_FOUND' } };
  }
  return { ok: true, data: user };
}

 

이 단계에서 중요한 점은 내부 구현을 거의 건드리지 않았다는 것입니다. 하지만 효과는 큽니다.

  • 실패 가능성이 타입에 드러난다
  • 컨트롤러가 분기 처리를 강제받는다
  • throw 누락 문제가 사라진다

 

우선순위 2: 컨트롤러에서 실패 처리를 명시한다

이제 컨트롤러를 살펴봅니다.

 

const result = await userService.getUser(userId);

if (!result.ok) {
  if (result.error.type === 'NOT_FOUND') {
    return res.status(404).end();
  }
}

return res.json(result.data);

 

이 코드의 장점은 명확합니다.

  • 어떤 에러가 있는지 한눈에 보인다
  • 새 에러 타입이 추가되면 컴파일 에러가 난다
  • 에러 처리가 누락되기 어렵다

 

우선순위 3: 내부 도메인 타입은 마지막에 건드린다

많은 팀이 반대로 접근합니다. 하지만 도메인 타입은 가장 마지막에 고치는 것이 안전합니다.

 

이유는 단순합니다.

  • 도메인은 사용처가 많다
  • 조금만 바꿔도 영향 범위가 넓다
  • 경계가 고정되지 않으면 수정 방향이 흔들린다

 

경계가 안정된 뒤에야, 도메인 타입 개선이 의미를 갖습니다.

 

실무에서 자주 틀리는 우선순위

  • 공통 타입 유틸부터 정리하는 경우
  • 제네릭 구조부터 예쁘게 만드는 경우
  • 도메인 모델을 먼저 일반화하는 경우

 

이 접근은 대부분 “고생은 많이 했는데 효과는 적은” 결과로 끝납니다.

 

실무 기준 리팩터링 우선순위 요약

  • 1순위: 레이어 경계 (controller ↔ service)
  • 2순위: 실패 흐름 타입화 (Result, Error)
  • 3순위: 호출부 분기 강제
  • 4순위: 내부 도메인 타입 정리
  • 마지막: 공통 유틸 타입 정제

 


 

 

TypeScript 리팩터링의 성패는 타입 기술이 아니라 순서에서 갈립니다.

경계부터 고치고, 흐름을 드러내고, 마지막에 내부를 정리한다. 이 순서를 기억하며 리팩토링을 하면 도움이 될 것입니다.