타입 스코프와 src/@types의 오해

Sharlotte ·2023년 1월 22일
1

@types

@types는 @types/<module>와 같이 서드파티 모듈의 타입 명세를 모아둔 타입 모듈에서 자주 볼 수 있지만 일부 개발자들이 이걸 개발 환경에서 같은 이름으로 쓰는 모습이 보였다.

왜 쓰는가?

@types 디렉토리는 무려 material icons에서 아이콘까지 줄 정도로 꽤나 유명한 방식이고 패턴이다. 하지만 물어본 결과 대부분 개발자들이 아무 생각없이, 즉 자세한 원리나 이유 없이 그렇게 쓰고 있었다. 왜냐하면 진짜로 원리가 없으니깐!

하지만 여기선 @types라는 이름의 디렉토리에 대한 타입스크립트의 특별한 처리가 아니라 왜 타입을 모아두는가, 왜 .d.ts 파일을 사용하는가에 대한 이유를 나열할텐데, 대표적으로 두 방식과

  • 전역 스코프 타입 명시 - @types/module가 module의 타입 명세를 모아둔 것처럼, 로컬의 @types는 로컬의 타입 명세를 모아둔 것이다.
  • 타입 모듈 모음 - 내가 이전까지 쓰던 방식이였는데, @types를 하나의 모듈로 보고 타입을 모두 export하여 사용처에서 import type 문으로 가져오던 방식이다.

공통적으로 아래와 같은 장점이 있다.

  • 일관된 타입 - 선언된 타입이 여러곳에서 쓰이는건 불필요한 import / export를 초례하며 장래에 코드의 비대화와 관심사 분산을 일으킬지도 모른다. 타입 선언 파일에 모아두면 관심사 분산이 줄어들고, 전역 스코프에 선언하면 import / export 문들이 사라지니 코드 비대화도 예방된다.
  • 관심사 분리 - 타입스크립트 개발자에게 중요한건 개발할 때 타입 명시되는 것이지 타입 또한 개발하는 것이 아니다 물론 반대로 타입을 개발하는 사람들이 있긴 하지만. 때문에 스크립트를 짤 때 거대한 타입 선언이 시야에 들어오는건 불필요하며 신경쓰인다. 즉, 관심사가 분산되므로 이럴 땐 타입 선언 파일에 분리해둘 필요가 있는 것이다.
  • 쉬운 유지보수와 관리 - 일관된 타입을 지키면 얻을 수 있는 부수이득이자 정말 중요한 장점이다. 타입 선언이 어느정도 알차면, 가령 API 명세에서 에러 형식이 달라지더라도 사전에 ErrorResponse와 같은 타입으로 일관되게 사용해왔더라면 해당 타입만 편집하면 된다. 이것이 왜 타입 선언 파일의 장점이냐면 바로 분산된 사용처가 한 곳으로 모여들었기 때문이다. 가끔 이 타입이 있는지도 모르고 다시 선언하는 경우가 있는데, 이런 일을 막아준다.

장황하게 늘여놓았지만 결국 다들 납득하는 당연한 부분이고 이러라고 @types를 쓰는 것이다. 패턴의 장점은 확실하다. 그럼 문제는 무엇인가?

전역 타입 명시

앞서 말한 타입 모듈 모음 방식을 잘못되게 사용하는 경우가 있는데, 우선 아래 사진을 보라.

스크립트에서 export문을 사용하지 않으면 최상위 스코프 위치는 스크립트 모듈 스코프 -> 전역 스코프로 이전된다. 그러므로:

  • export문이 없는 스크립트에선 사용되지 않은 const문이 비활성 처리되지 않는다. 즉 어딘가에서 사용되고 있다. (전역)
  • 다른 스크립트에서 export문이 없는 스크립트에서 선언한 변수를 참조할 수 있다. 즉 같은 스코프다.
  • .d.ts 또한 export문을 배제하면 전역 스코프 이므로 자연스레 타입들은 전역 declare가 된다.
    + 그래서 전역 스코프에서 타입 명시를 할 때 declare하는건 불필요하다.

아마 자바스크립트를 사골까지 우려먹었더라면 첫번째와 두번째, 즉 모듈과 스코프의 관계는 알고 있을 것이다. 나 또한 잠시 잊었으나 알고 있었다. 하지만 중요한건 이것이 타입과 상호작용한 결과, 즉 전역 타입 명시다.

잘못된 사용법

과거의 난 위와 같은 방식으로 직접 타입을 export하여 import type하는 방식을 사용했으나, export/import를 하지 않음으로써 모듈의 최상위 스코프를 전역 스코프로 끌어올린다면 굳이 import하지 않더라도 우리가 Number를 import 없이 쓰는 것마냥 사용할 수 있다.
즉, 불필요한 타입 모듈화는 코드를 더럽게 만든다.

전역 스코프의 타입 명시는 머릿 속에서 대혁명처럼 느껴졌다. 하지만 이 전역 명시도 필요할 때 적절하게 써야 하니, 그 오용의 예를 들자면...

전역 스코프의 declare와 모듈 스코프의 declare는 다르다


이전 글에서 가끔 출연하던 declare module문은 위와 같이 모듈의 인터페이스를 declare함으로써 타입 보강을 한다.

따라서 declare module문에 있던 field2 속성은 ./ov1 모듈의 style 인터페이스 타입에 보강 되었다.
이것이 일반적인 모듈 스코프에서 declare module문의 사용처인데, 문제는 이게 전역 스코프에선 타입 보강이 아니라 타입 선언이 된다는 것이다.

예를 들자면
NextAuth는 이와 같이 모듈 스코프에서 선언된 상태였는데

전역 스코프에서 해당 모듈을 통째로 재선언하는 바람에 진짜 next-auth에 있던 NextAuth 함수는 씹혀버렸다.

타입 체커의 동작 알고리즘을 정확히 알 수가 없지만 확실한건 전역 스코프에서 declare module를 하면 해당 모듈 타입이 통째로 갈아치워진다라는 것이다.

처음에 원하던대로 타입 보충, 즉 기존 타입에 덧씌우기/주입하기만 하고 싶다면 선언할 모듈 레벨을 전역에서 블록으로 내리면 된다.

override.d.ts라는 타입 보강용 타입 모듈을 따로 만들었다.
+ 특정 모듈에 국한되는 사정이 아니라 타입 보강이 쓰이는 어디든지 해당된다. 가령 process.env 라든지...

오해

typeRoots에 둬야 한다?

도대체 어디서 시작된건지 모를 고정관념과 오해인데 tsconfigcomplierOptions에 있던 typeRoots를 비롯한 types 속성은 node_modules/@types에 있는 타입 모듈들 중 전역 스코프에 포함될 모듈들을 특정하는 배열 속성이다. 타입 모듈들은 기본적으로 포함되어 있는데 이 이상한 오해로 인해 @types/모듈을 설치해도 가끔 제 일을 못하던 것이였다.

예전에 @types/node를 설치하고 node_modules에 있는걸 확인하고도 타입이 없다고 말하니 너무 억울해서 파해쳐도 힘들어서 그냥 넘어갔던 적이 잇었는데 이제 와서 돌아보니 이런 원인이 있었던 것을 깨달았다.
역시 고정관념이 가장 무섭다..

참고로 두 속성의 차이는 공식 문서에도 있지만 하위 모듈들도 전역에 포함시키냐 아니냐다.

결론적으로 개발 환경의 @types와는 관련이 전혀없다.

경로에 따라 다르다?

@types는 깃허브의 README나 prettier의 .prettierrc처럼 typescript의 .tsconfig같은 존재가 아니다. 그냥 말 그대로 types가 있는 디렉토리일 뿐이다.
그러므로 사실이 아니다.

side note: module path

모듈 경로는 꽤나 특이하다.
asdf/index.d.ts@asdf/index.d.tsasdf.ts와 같게 취급된다.

하지만 그렇다고 둘이 합쳐지는건 아니라 필요하면 import해야 하는 점은 여전하다.

profile
샤르르르

0개의 댓글