[TYPESCRIPT] 선언 병합을 쓰게 되는 순간과 그 이후에 남는 것들

 

TypeScript를 쓰다 보면, 이미 정의된 타입이나 인터페이스를 “조금만 확장하고 싶은” 순간이 옵니다. 기존 정의를 건드리기는 애매하고, 그렇다고 완전히 새로 만들기에는 중복이 많습니다.

 

이때 자연스럽게 마주치는 개념이 선언 병합입니다. 처음에는 꽤 편리해 보입니다. 타입을 다시 열어서 필드를 하나 추가하는 것만으로 원하던 동작이 바로 됩니다.

 

문제는 이 패턴이 코드베이스에 남기 시작할 때입니다. 어디까지가 의도된 확장이고, 어디서부터는 추적하기 어려운 변경인지 경험이 쌓일수록 구분하게 됩니다.

 

선언 병합이 가능한 대상

선언 병합은 모든 타입에 적용되는 개념은 아닙니다. 대표적으로 다음 대상에서 동작합니다.

 

  • interface
  • namespace
  • enum

 

반대로 type alias에는 적용되지 않습니다. 이 차이 때문에, 처음 설계할 때 interface를 쓸지 type을 쓸지에 대한 선택이 나중에 영향을 주기도 합니다.

 

가장 흔한 예제

선언 병합은 보통 이런 형태로 처음 접합니다.

 


interface User {
  id: string;
}

 

다른 파일에서 같은 이름의 interface를 다시 선언합니다.

 


interface User {
  email: string;
}

 

결과적으로 User는 다음과 같이 합쳐집니다.

 


interface User {
  id: string;
  email: string;
}

 

문법적으로는 간단합니다. 문제는 이 병합이 “눈에 보이지 않는다”는 점입니다.

 

라이브러리 확장에서 자주 쓰이는 패턴

선언 병합이 가장 많이 쓰이는 곳은 외부 라이브러리 타입 확장입니다.

 


declare global {
  interface Request {
    user?: {
      id: string;
      role: string;
    };
  }
}

 

Express나 Fastify 같은 프레임워크에서 request 객체에 값을 주입하는 경우, 이 방식은 꽤 자연스럽습니다.

 

문제는 이 코드가 프로젝트 어디에서나 영향을 준다는 점입니다. Request를 사용하는 모든 위치에서 이 변경이 전제로 깔리게 됩니다.

 

편리함 뒤에 따라오는 비용

선언 병합은 당장 코드를 수정하기는 쉽습니다. 하지만 시간이 지나면 다음과 같은 상황을 자주 겪게 됩니다.

 

  • 어디에서 타입이 확장됐는지 찾기 어렵다
  • 같은 이름의 interface가 여러 파일에 흩어져 있다
  • 한 곳의 수정이 전혀 예상 못 한 코드에 영향을 준다

 

IDE에서 타입 정의로 이동해도, 하나의 선언만 보이고 병합된 나머지는 놓치기 쉽습니다.

 

의도하지 않은 병합

가끔은 선언 병합을 의도하지 않았는데 우연히 발생하는 경우도 있습니다.

 


interface Config {
  timeout: number;
}

 


interface Config {
  retry: number;
}

 

이 두 선언이 다른 팀, 다른 맥락에서 작성됐다면, Config라는 이름 자체가 문제가 됩니다. 타입 이름 충돌이 컴파일 에러가 아니라 병합으로 처리되기 때문에, 문제를 더 늦게 발견하게 됩니다.

 

type alias를 쓰지 않은 이유가 남는다

이런 경험이 쌓이면, 왜 어떤 타입은 interface로, 어떤 타입은 type으로 선언했는지 이유를 다시 보게 됩니다.

 

선언 병합이 필요한 타입이라면 interface가 맞고, 절대 확장되면 안 되는 타입이라면 type alias가 더 안전한 선택인 경우가 많습니다.

 

운영 코드에서 드러나는 차이

선언 병합이 적절히 쓰인 코드에서는, 확장 지점이 비교적 명확합니다. 대개 다음 조건을 만족합니다.

 

  • 외부 타입을 보완하는 용도
  • 프로젝트 전반에서 공통으로 쓰이는 구조
  • 파일 위치와 네이밍으로 의도가 드러남

 

반대로 도메인 타입까지 병합을 허용하기 시작하면, 타입의 경계가 흐려지고 변경 영향 범위를 예측하기 어려워집니다.

 


 

선언 병합은 TypeScript가 제공하는 강력한 기능입니다. 필요한 순간에는 정말 깔끔한 해결책이 됩니다.

 

다만 이 기능은 “여기서 타입이 확장된다”는 사실이 코드 구조로 드러나지 않는다는 점에서 항상 주의를 요구합니다.

 

외부 타입을 보완하는 정도에서 멈추면 도움이 되고, 도메인 내부까지 자연스럽게 허용하기 시작하면 나중에 되돌아보기 어려운 선택이 되는 경우가 많습니다. 선언 병합은 쓰지 말아야 할 기능이 아니라, 써야 할 위치가 분명한 기능에 가깝습니다.