카테고리 없음

내가 중요하게 생각하는 tsconfig, ESLint - isolatedModule를 깊게 파봤다.

최져니 2025. 4. 10. 17:22

나는 tsconfig.json은 기본 세팅대로 가져가는 편이고, 그중 가장 중요하게 생각하는 속성들은 총 4가지를 뽑는다.

꼽아보니 대부분 실수로 누락하는 걸 방지해 주는 속성들이다. 4가지 중에서 isolatedModules에 대해 자세히 알아보려고한다.

네 가지 속성을 간단히 소개하자면 아래와 같다.

  • noImplicitReturns: 모든 경로에서 함수에서 return을 하는지를 확인해 주는 옵션. 실수를 컴파일 타임에 잡을 수 있다.
  • noImplicitAny: 컴파일러가 특정 변수의 타입을 any로 추론하게 되면 에러를 일으키도록 하는 옵션이다. 요즘은 @typescript-eslint/no-explicit-any로 해결.
  • strictNullChecks: null, undefined, any타입 외의 모든 타입에서 null과 undefined 타입을 배제하는 옵션.
  • 💡 isolatedModules: 프로젝트 내에 모든 각각의 소스코드 파일을 모듈로 만들기를 강제한다.

이는 각 파일이 자체적으로 완전한 변환 단위가 되어야 하며, 타입스크립트 컴파일러가 다른 파일의 타입 정보에 대한 암시적인 의존 없이 타입 에러 없이 해당 파일을 독립적으로 분석하고 이해할 수 있어야 한다는 의미이다.

 

왜냐면, 기본적으로 트랜스파일러가 변환은 b.ts(아래 예시) 파일만 보고 변환을 시도한다. 여기서 Babel은 message가 어떤 타입인지 알지 못한다. a.js 파일의 내용을 보지 않고 b.js만 변환하기 때문이다. 따라서 message.toUpperCase()가 런타임에 에러를 발생시킬 가능성을 배제할 수 없다. (간단한 예시로는 웬만하면 에러가 나진 않는다)

// a.ts
export const message = 'hello';

// b.ts
import { message } from './a';
console.log(message.toUpperCase());

 

즉 트랜스파일러는 타입 정보를 알지 못하고, 단순히 구문을 변환한다. isolatedModules: true는 이러한 트랜스파일러가 각 파일을 안전하게 JavaScript로 변환할 수 있도록 타입스크립트 코드 작성 방식에 제약을 가하는 것이다.

 

  • 명시적인 타입 사용 의도: import type 자체가 "이 심볼은 타입으로만 사용할 거야"라는 명확한 의도를 전달한다. 컴파일러는 이 정보를 바탕으로 해당 심볼이 값의 컨텍스트에서 나타나지 않도록 검사한다.
  • 암묵적 추론의 감소: 값으로 사용되지 않기 때문에, 컴파일러는 해당 심볼이 어떤 구체적인 타입을 가지는지 런타임 동작을 기반으로 추론할 필요가 없다. 타입스크립트의 타입 검사는 주로 컴파일 시점에 이루어지며, import type은 이 컴파일 시점의 타입 정보 관리에 초점을 맞춘다.
  • 트랜스파일러와의 협업: 트랜스파일러가 해당 구문을 안전하게 처리 (제거 또는 무시)할 수 있도록 명시적인 정보를 제공한다. 그뿐만 아니라 import/export를 하게 되면 런타임 과정에서 js에서는 type을 사용할 필요가 없기 때문에 컴파일 결과에서 제외된다. 따라서, 심볼에 대해서는 값으로서의 타입을 추론하는 과정을 생략하고 번들링 과정에서 코드 길이가 더 짧아져서 번들 사이즈가 줄어든다.

 

 

 

isolatedModules 속성을 활성화시키고 타입으로만 사용되는 심볼을 일반적인 export 구문으로 re-export 하려고 할 때 에러를 발생시킬까?

 

isolatedModules 속성 활성화 시 에러 메세지

 

 

먼저 이를 해결하는 문법은 아래와 같다.

export type { SomeThing };

 

이유는 트랜스파일러(Babel 등)가 런타임에 존재하지 않는 타입 정보를 export 하려고 시도하는 상황을 방지하기 위함이다.

// a.ts
export interface User {
  name: string;
}

// b.ts
import { User } from './a';
export { User }; // 에러 발생
  1. User는 interface로 정의된 타입이다. 타입스크립트의 타입 정보는 컴파일 시에 타입 검사를 위해 사용될 뿐, 런타임 JavaScript 환경에는 존재하지 않는다.
  2. Babel 같은 트랜스파일러는 b.ts 파일을 독립적으로 JavaScript로 변환하려고 시도한다. Babel은 타입 정보를 알지 못하므로, export { User }; 구문을 보고 JavaScript의 export 구문으로 그대로 변환한다.
  3. JavaScript로 변환했을 때, 런타임에 'User'라는 값이 존재하지 않는다.
    // b.js
    export { User };
  4. 만약 다른 모듈에서 b.js를 임포트 하여 User를 값으로 사용하려고 하면 런타임에 User가 정의되지 않았다는 오류가 발생할 것이다. 트랜스파일러는 이러한 잠재적인 런타임 오류를 방지할 수 없다.

 

isolatedModules: true 설정은 이러한 트랜스파일러의 한계를 고려하여, 타입 정보를 값 정보와 명확하게 분리하도록 강제한다. export type을 명시적으로 사용하도록 함으로써, 트랜스파일러가 타입 관련 코드를 안전하게 처리 (남겨두거나 제거)할 수 있도록 돕는 것이다.

// b.ts
import type { User } from './a';
export type { User }; // 올바른 방법

 

export type을 사용하면 트랜스파일러는 User가 타입 정보라는 것을 알고 JavaScript export 구문으로 변환하지 않는다.

대신, 최종 JavaScript 파일에서 이 export type 구문은 제거된다.

 

결론적으로, isolatedModules: true는 트랜스파일러가 런타임에 존재하지 않는 타입 정보를 실수로 export 하여 런타임 오류를 발생시키는 것을 방지하기 위해, 타입 export 시에는 export type을 명시적으로 사용하도록 강제하는 것이다.

 

이는 타입스크립트 코드의 안정성과 트랜스파일러와의 호환성을 높이는 데 역할을 한다.


 

ESLInt 경우에는 아래와 같은 규칙들은 활성화하는 편이다. 차라리 빌드에서 fail 시켜버리는 최소한의 룰이라 생각한다.

  • react/no-unused-vars: React 컴포넌트 내 미사용 변수 발견 시 경고/에러 표시.
  • prefer-const: 재할당 없는 let 변수 발견 시 const 사용 권장 경고.
  • no-var: var 사용 시 let/const 사용 강제 경고/에러.
  • @typescript-eslint/no-unused-vars: TypeScript 미사용 변수/함수/매개변수 발견 시 경고/에러 표시.
  • @typescript-eslint/no-explicit-any: 명시적 any 타입 사용 시 경고/에러 표시.
  • @typescript-eslint/no-unused-expressions: 값이 사용되지 않는 표현식 발견 시 경고/에러 표시.
  • react/display-name: displayName 미정의 React 컴포넌트 발견 시 경고/에러 표시 (ReactDevtool때문에 사용)
  • react-hooks/exhaustive-deps: 불완전한 Hooks 의존성 배열 발견 시 경고/에러 표시.
  • react/no-unescaped-entities: 잘못된 HTML 엔티티 이스케이프 발견 시 경고/에러 표시.
  • @typescript-eslint/no-non-null-asserted-optional-chain: 옵셔널 체인 후 ! 사용 시 경고/에러 표시.