Typescript

스크립트 파일과 모듈 파일 이해하기

비숑주인 2025. 5. 15. 16:44

TypeScript에서 스크립트 파일과 모듈 파일은 구분 기준에 따라 동작 방식이 달라진다. 이는 타입 정의의 범위와 import/export 문법 사용에 영향을 주기 때문에 명확하게 이해하는 것이 중요하다.

$ 함수를 import하지 않았는데도 사용할 수 있는 이유

TypeScript에서 test.ts 파일 내에서 $ 함수를 import하지 않았음에도 사용할 수 있는 경우가 있다. 이는 TypeScript가 misc.d.ts 파일을 스크립트 파일로 인식했기 때문이다.

TypeScript에서는 최상위 스코프에 import나 export 문이 존재하지 않으면 스크립트 파일, 존재하면 모듈 파일로 판단한다.

 

스크립트 파일 vs 모듈 파일 예시

스크립트 파일

declare namespace Example {
  const test: string;
}

 

위 코드에는 import나 export가 없기 때문에 스크립트 파일이다.

 

모듈 파일

declare namespace Example {
  const test: string;
}
export {}

 

이 경우에는 최상위 스코프에 export가 존재하므로 모듈 파일이다.

네임스페이스 내부의 export는?

declare namespace Example {
  export const test: string;
}

 

이 예시는 export가 네임스페이스 내부에 있으므로 최상위 스코프에는 없다고 판단되어 스크립트 파일이다.

결론적으로 test.ts와 misc.d.ts는 모두 스크립트 파일이기 때문에, test.ts는 misc.d.ts에 선언된 $ 타입을 자유롭게 사용할 수 있다. 이처럼 스크립트 파일의 타입 정의는 다른 파일에서 자유롭게 참조 가능하다.

모듈 파일에서도 스크립트 파일의 타입 사용 가능

// test2.ts
$('h1');
export {}

 

export {} 문이 있으므로 test2.ts는 모듈 파일이다. 그럼에도 $를 import하지 않고도 사용할 수 있다. 이는 misc.d.ts의 타입 선언이 스크립트 파일이기 때문이다.

물론 아래처럼 명시적으로 import할 수도 있다.

import $ from 'jquery';
$('h1');
export {}

 

그러나 이 경우 $는 다른 타입을 가리킨다.

import 여부에 따른 $ 타입 차이

  • import한 경우: node_modules/@types/jquery/index.d.ts에서 export = jQuery로 정의된 타입을 사용한다.
  • import하지 않은 경우: misc.d.ts의 스크립트 파일에 선언된 $ 타입을 사용한다.
// import한 경우
declare const jQuery: JQueryStatic;

// import하지 않은 경우
declare const $: JQueryStatic;

 

모듈 파일의 인터페이스 병합 여부

TypeScript에서는 인터페이스나 네임스페이스는 이름이 같으면 병합되는 특성이 있다. 그러나 모듈 파일에서는 병합되지 않는다. 이를 실습으로 확인해보자.

 

module1.ts

export interface Test {
  name: string;
}

export default function() {
  console.log('default export');
}

 

module2.ts

export interface Test {
  name2: string;
}

 

module3.ts

import * as module1 from './module1';
import * as module2 from './module2';

const ex1: module1.Test = {
  name: 'hi',
  name2: 'error' // 오류 발생
};

const ex2: module2.Test = {
  name: 'error', // 오류 발생
  name2: 'hi'
};

module1.default();

 

module1.ts와 module2.ts 모두 Test라는 이름의 인터페이스를 export하고 있지만, 모듈 파일이므로 병합되지 않는다. 따라서 서로 다른 인터페이스로 간주되고, 존재하지 않는 속성에 접근하면 에러가 발생한다.

import * as 네임스페이스 from 모듈

위 예시에서 사용된 import * as module1 from './module1' 문법은 해당 모듈이 export한 모든 항목을 module1이라는 네임스페이스의 멤버로 가져오는 것이다.

module1.Test         // export한 인터페이스
module1.default()    // default export한 함수

 

이처럼 모듈 파일은 자동으로 네임스페이스처럼 사용할 수 있기 때문에, 보통 모듈 파일 내부에 별도로 네임스페이스를 만들 필요는 없다.

Type-Only Imports/Exports

TypeScript는 타입 전용 import/export 문법을 지원한다. 다음 예시로 살펴보자.

 

module4.ts

interface Name {
  first: string;
  last: string;
}

interface Age {
  korean: number;
  american: number;
}

export type { Age };
export default Name;

 

module5.ts

import type Name from './module4';
import type { Age } from './module4';

const name: Name = {
  first: 'zero',
  last: 'cho'
};

const age: Age = {
  korean: 30,
  american: 28
};
  • export type: export하는 대상이 값이 아니라 타입임을 명시
  • import type: import하는 대상이 타입임을 명시

이러한 Type-Only import/export는 일반적으로는 필요하지 않지만, TypeScript가 아닌 외부 도구가 코드를 분석할 때 타입인지 값을 구분하지 못할 수 있기 때문에 사용하는 것이다. 주의할 점은 import type Name, { Age }처럼 default import와 named import를 한 줄에 동시에 사용할 수 없다는 점이다. 따라서 module5.ts처럼 따로따로 import해야 한다.