[TS] import type을 사용해야 하는 이유

Jane·2023년 8월 15일
4

TypeScript

목록 보기
15/17
post-thumbnail

⛄ Type-Only Imports and Exports

🤖 사용방법

  • import type

    • 타입 표기와 선언에 사용될 선언만 import 한다.
  • export type

    • 타입 문맥에서 사용할 export만 제공하며, 추후 TS의 출력물에서 제거된다.
  • 원래의 import, export 문법과 동일하나 뒤에 type만 덧붙여주면 된다.

import type { SomeThing } from "./some-module.js";
export type { SomeThing };

⚠️ 주의사항

🚨 import type으로 받아온 class는 확장이 불가능하다.

  • 클래스는 값은 런타임에, 타입은 디자인 타임(개발할 때)에 가지므로 상황에 따라 사용이 다르다.
    • 컴파일러는 디자인 타임에 변수와 함수 등의 타입 정보를 정적으로 분석하고 체크한다.
    • 런타임에는 코드가 실행되고, 인스턴스가 생성되어 메모리를 할당받으며, 이를 제어하기 위한 로직이 동작한다.
    • 클래스는 디자인타임에 선언된 타입 정보를 기반으로 런타임에는 객체로서 존재한다.
// 클래스가 선언된 파일
export class sampleTypes {
  first!: string;
  second!: number;
  third!: boolean;
}
export class sampleTypes2 {
  first!: string;
  second!: number;
  third!: boolean;
}

// 클래스가 import 되어 확장된 파일
import { sampleTypes } from "types/new";
import type { sampleTypes2 } from "types/new";

class newTypes extends sampleTypes {
  hi!: string;
  hello!: number;
}

class newTypes2 extends sampleTypes2 {
  hi!: string;
  hello!: number;
}
// 에러 발생!
// 'sampleTypes2'은(는) 'import type'을 사용하여 가져왔으므로 값으로 사용할 수 없습니다.
  • 따라서 이미 타입으로 가져온 클래스는 다시 값으로서 확장될 수 없다.

🚨 기본 import와 default import는 한 번에 할 수 없다!

export class sampleTypes {
  first!: string;
  second!: number;
  third!: boolean;
}
export default class sampleTypes2 {
  first!: string;
  second!: number;
  third!: boolean;
}
  • 하나는 일반 export, 하나는 default로 export 한 경우를 가정해보자.
// 에러 발생!
// 형식 전용 가져오기는 기본 가져오기 또는 명명된 바인딩을 지정할 수 있지만,
// 둘 다 지정할 수는 없습니다.
import type sampleTypes2, { sampleTypes } from "types/new";
  • 이때 위처럼 두 개를 한 번에 가져오려 할 경우 에러가 발생한다.
  • 이는 'sampleTypes2만 타입인지, 모든 import 선언이 타입인지'가 모호하게 해석될 여지가 있으므로 애초에 오류로 처리하도록 제한을 둔 것이다.

⚔️ import type으로 edge-case 문제에 대비하기

// TS 코드
import sampleTypes from 'types/new'

let newType: sameTypes = // ~
// 변환 후 JS 코드
// type import 부분은 다른 부수효과를 발생 시키지 않을 것으로 예상되어 삭제됨
let newType = // ~

✂️ import elision

  • TS 코드가 컴파일 될 때, 인터페이스와 타입은 사라져서 방출된 결과물에 존재하지 않는다.

  • 만약 import가 이를 위해서만 이루어졌다고 파악될 경우 JS로 변환된 파일에는 해당 import 선언문도 들어가지 않는다.

    // 타입이 선언된 파일
    export type sampleTypes = {
      first: string;
      second: number;
      third: boolean;
    };
    
    export const showTypes = (config: sampleTypes) => {
      console.log(config);
    };
    
    // 타입을 호출하는 파일
    import { sampleTypes, showTypes } from "types/new";

🧐 Edge-Case

  • TS의 transpileModule API, Babel을 통해 프로젝트의 모듈을 하나씩 컴파일하려 할 때 생기는 문제가 있다.
// import 된 후 export 되는 경우 이것은 단순 타입이 맞을까? 아니면 값으로 취급되어야 할까?
import { reExportedType } from "./foo.js";

export { reExportedType };
  • 정답은 이 파일에 국한된 경우 알 수 있는 방법이 없다는 것이다.

  • TS 컴파일러는 실시간으로 타입 정보를 생략하려 하지만 re-export 된 타입이 해당 파일에서만 사용된다면 이것을 인지하지 못해 문제가 발생한다.

    • 만약 다른 모듈에서 사용할 수 있는 공통적인 타입을 정의할 때, 이 타입을 다른 모듈에서도 사용하기 쉽도록 해당 모듈에서 다시 export 하는 상황이 있다고 가정해보자.
    • 이렇게 내보내진 타입이 결국에는 해당 모듈에서만 사용된다면, 이 타입 정보 자체가 필요하지 않으므로 제거되는 것이 좋을 것이다.
    • 하지만 앞서 말한 상황처럼 파일 단위로 코드를 변환하는 경우, 컴파일러는 타입 정보가 제공되는 이전 모듈을 고려하지 않고 단일 파일 단위로 코드를 해석하게 된다.
    • 따라서, 타입 정보가 re-export 되었는지 여부와 관계 없이, 해당 타입이 실제로 사용될지 여부를 알 수 없기 때문에 컴파일러는 타입 정보를 포함한 정보를 생략하지 못한다.
  • 이러한 이유로 TS의 transpileModule API, Babel은 reExportedType이 단순히 타입으로만 사용된 경우에도 제대로 동작하지 못하게 된다.

  • 이는 타입 체크 과정의 안정성을 저해하고, 실제 빌드 이후 결과물의 용량을 불필요하게 늘릴 수 있다.

  • 관련 에러 기록_1

  • 관련 에러 기록_2

🧐 Edge-Case 2

  • TS import elision은 타입으로 사용되는 import만 포함하는 import 선언문을 삭제해버린다.
  • React보다는 전역으로 등록되어야 하는 모듈이 오직 타입을 목적으로만 import 되는 상황이 발생하는 Angular.js 등의 프레임워크에서 자주 발생하는 에러 상황이라고 한다.
// 오직 타입만 선언하였으므로 import elision에 의해 삭제된다.
import { SomeTypeFoo, SomeOtherTypeBar } from "./module-with-side-effects";

// 따라서 이 선언문이 반드시 함께 사용되어야 한다.
import "./module-with-side-effects";
  • 두 번째 선언문이 존재하지 않을 경우,

    • webpack은 변환된 JS코드를 실행할 때에 존재하지 않는 것에 대한 import의 결과로 에러를 발생시킨다.

    • 만약 부수효과를 포함하고 있는 모듈에서 타입을 import 했다면 이 import 구절은 삭제되어 부수효과도 실행되지 않는 문제가 생긴다.

    • 관련 에러 기록_1

    • 관련 에러 기록_2

⚠️ 더 이상 사용되지 않는 해결책

import type 도입 전 해결 방법

  • import 선언문이 명시적으로 type-only로 지정되어 있지 않을 경우 방출 시 컴파일러가 import 선언 삭제를 멈추도록 하였다.

  • 이를 통해 부수 효과를 위해 imports를 보존하고 싶었던 사용자들이 필요한 import 선언문만 보존할 수 있었다.

  • 또한 단일 파일 단위로 코드를 변환할 때에 re-export가 타입 정보만을 내보내는 경우 해당 구문을 삭제하도록 식별자를 명시할 수 있는 기능을 제공하였다.

    • export { T } from './mod'를 사용하여 타입 T를 re-export 할 수 있었다.
    • 방출 시 특별한 JS 코드를 생성하지 않고도 TS 컴파일러에게 해당 export 구문이 타입 정보만 내보낸다는 힌트를 제공하여 타입 생략 여부를 결정할 수 있게 하였다.
  • 하지만 --isolatedModules 플래그의 도입으로 이 방식은 사용되지 않기 시작했다.

isolatedModules

  • re-exporting(타입 정보를 다른 모듈에서 가져오거나 내보냄)이 오류로 처리된다.
  • 단일 파일 단위에서 코드를 변환하더라도 모듈 간 충돌을 사전에 감지할 수 있게 한다.
  • 모듈 간 의존성을 완전히 배제한 상태에서 코드를 변환할 수 있게 한다.
  • 이전 방식은 re-exporting을 허용하되 타입만을 내보내는 것을 컴파일 오류로 처리했는데, 이는 다양한 모듈 시스템을 지원하고 있는 TS에서 복잡성과 모호함이 증가할 수 있는 문제가 있었다.
  • isolatedModules 플래그를 도입하면 타입의 re-exporting 자체를 에러로 처리하므로 단일 파일 변환 시 모듈 간 의존성을 엄격하게 제한할 수 있게 되었다.

단순히 isolatedModules만 사용할 경우의 문제점

  • 만약 isolatedModules만을 사용하면서 re-exporting을 하려면 아래와 같은 방식을 사용해야 했다.
    import { sampleType } from "./a";
    export type sampleType = sampleType;
    • 하지만 TypeScript 3.7 버전 이후로는 로컬로 선언된 타입 이름과 import한 타입 이름이 충돌할 경우를 막기 위해 이러한 문법을 허용하지 않게 되었다.
    • import 구문을 통해 가져온 타입과 동일한 이름을 가진 타입을 현재 모듈에서 재정의하려 하면, 이름 충돌로 컴파일 오류를 발생시켰다.

💊 import type으로 해결하기

🌟 1. type-only imports and exports

  • 이 방식을 통해 모듈에서 타입 정보만을 가져오므로 불필요한 값이 메모리에 로딩되는 문제를 해결하였다.

    • import type을 사용할 경우 이러한 import는 항상 컴파일 과정에서 완전히 제거되어 런타임에는 남아있지 않게 된다.

    • export type 또한 타입의 맥락에서만 사용되는 export 구문이므로, 변환 과정에서 삭제된다.

  • 명시적으로 타입만을 가져온다고 보장되므로 JS 변환 시 삭제된다.

  • Babel 등의 도구가 isolatedModules 옵션을 통해 코드를 추정하기 더 용이해진다.

  • 위의 복잡한 이유에 덧붙여, 명시적으로 타입 import를 나타낼 수 있으므로 개발자의 입장에서 가독성이 좋아지는 효과도 있다.

🚩 2. importsNotUsedAsValues

  • 런타임 시 사용되지 않는 import에서 발생하는 작업을 제어하기 위해 TypeScript 3.8에서 추가된 기능이다.
  • JS로 변환되었을 때 절대 사용되지 않을 import 구문들을 import type을 통해 명시적으로 작성할 수 있으므로 작동하는 플래그이다.

remove

  • 사용하지 않는 import를 제거하는 동작으로, 기본 값으로 설정되어 있다.
  • 기존 동작을 변경하지 않는다.

preserve

  • 사용되지 않는 값들을 모두 보존하는 동작이다.
  • 이를 통해 imports와 부수 효과들이 보존될 수 있다.

error

  • 모든 imports를 preserve처럼 보존하지만 import 값이 타입으로만 사용될 경우 오류를 발생시킨다.
  • 예기치못한 import를 방지하되 부수효과에 사용될 import를 명시적으로 만들고 싶을 때 유용하다.

🔎 References

profile
An investment in knowledge pays the best interest🙃

0개의 댓글

관련 채용 정보