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 리팩터링의 성패는 타입 기술이 아니라 순서에서 갈립니다.
경계부터 고치고, 흐름을 드러내고, 마지막에 내부를 정리한다. 이 순서를 기억하며 리팩토링을 하면 도움이 될 것입니다.
