본문 바로가기

Backend/Typescript

Typescript 외부 모듈 타입 선언

- 문제 상황

 

업무간에 외부 모듈을 사용하는 경우는 너무도 많습니다.

당장 생각나는 것들 나열만 해도 express, lodash, nest, ioredis, mysql, dayjs 등 굉장히 많죠.

Javascript 에서 일반적인 CommonJS 스타일로 모듈을 사용한다면 코드는 아래와 같을 겁니다.

 

const _ = require('lodash');

const test = [1, 2, 3];

_.each(test, item => {
  console.info(item);
});

// 1
// 2
// 3

 

하지만 이를 Typescript 에서 ES6 syntax 로 바꿔 사용하면, 문제점이 있습니다.

 

import _ from 'lodash';

const test = [1, 2, 3];

_.each(test, item => {
  console.info(item);
});

// TSError: ⨯ Unable to compile TypeScript:
// error TS7016: Could not find a declaration file for module 'lodash'

 

사용하는 lodash 모듈의 타입 정의를 찾을 수 없어 Typescript 의 컴파일이 실패하는 문제입니다.

Try `npm i --save-dev @types/lodash` if it exists or add a new declaration (.d.ts) file containing `declare module 'lodash';` 

대체로 이런 문제가 발생할 땐 위와 같은 문구가 나오는데, @types/lodash 라는 모듈이 무조건 있으니 시도하라는 안내는 아니므로 npm 에 실제로 해당 모듈이 있는지 확인한 후에 추가해주면 대부분 해결됩니다. 혹은 일부 모듈의 경우엔 모듈 자체적으로 타입 정의를 갖고 있어서 별도로 @types 를 설치할 필요가 없는 케이스도 있습니다. (ex. dayjs)

 

하지만 문제는 모든 외부 모듈이 이러한 타입 정의를 갖고있지 않다는 것 입니다.

저의 경우엔 최근에 배치 프로그램을 새로 작성하면서 AWS Redshift 에 적재되어 있는 데이터들을 사용해야 하다보니, npm 에 올라온 aws-redshift 라는 모듈을 사용해야 하는 케이스가 있었습니다. 그리고 이 모듈은 별도의 타입 정의를 갖고 있지 않아 Typescript 에서 바로 사용할 수가 없었습니다.

 

import Redshift from 'aws-redshift';

...

// TSError: ⨯ Unable to compile TypeScript:
// error TS7016: Could not find a declaration file for module 'aws-redshift'.

 

이런 경우 이 모듈 사용을 포기해야 하는 것이 아니라, 따로 프로젝트 내에 타입을 정의해주면 사용이 가능합니다.

실제로 위의 Try~ 라고 나온 부분 내용을 보면 @types 를 설치하거나, declaration 을 추가하라는 부분이 있죠.

따라서 이번 포스팅에선 이 aws-redshift 모듈을 대상으로 타입을 정의해 사용해보려 합니다. (사실 모듈 자체는 @aws-sdk 를 사용할 수 없는 다른 문제가 있어 대안을 찾다보니 aws-redshift 가 대상이 된 것이고, 타입 정의가 없는 다른 외부 모듈들에도 충분히 응용 가능한 내용입니다)

 

- 타입 정의 및 설정

 

먼저 프로젝트 내부에 types 라는 디렉토리를 만듭니다. 저의 경우 src 하위에 types 디렉토리를 만들었습니다.

이후 types 하위에 aws-redshift 디렉토리를 생성하고 그 하위에 index.d.ts 파일을 생성합니다. 

 

declare module 'aws-redshift';

 

index.d.ts 에 들어갈 내용은 위의 한 줄이 전부입니다. 상당히 간단하죠.

그리고 tsconfig.json 을 약간 수정해줍니다.

 

{
  "compilerOptions": {
    ...,
    "typeRoots": ["./node_modules/@types", "./src/types"]
  },
  ...
}

 

위와 같이 typeRoots 부분을 수정해 주었습니다. 기본적으로 npm install @types/lodash --save-dev 와 같이 설치한 외부 모듈의 타입 정의 파일은 node_modules/@types 에 위치하게 됩니다. 그리고 Typescript 에선 typeRoots 설정을 넣지 않아도, node_modules/@types 는 컴파일에 포함됩니다. (링크)

 

하지만 src 하위에 types/aws-redshift/index.d.ts 와 같은 타입 정의 파일을 별도로 만들었으므로, 이 위치에 있는 파일들도 컴파일 대상에 포함되도록 위와 같이 추가해주어야 합니다. 당장 이것만으로 src 에 위치한 *.ts 에서 aws-redshift 모듈을 import syntax 로 사용할 수 있게 되었습니다.

 

import Redshift from 'aws-redshift';

console.info(Redshift);

// [Function: Redshift] {
//   query: [Function],
//   model: { create: [Function: create], import: [Function] },
//   import: [Function]
// }

 

- 디테일한 타입 정의

 

위 정도의 타입 정의만 추가해도 코드 작성에 큰 문제는 없을 것 입니다. 실제 모듈의 사용법은 npm, github 혹은 별도의 doc 을 통해 제공될테니 말이죠. 하지만 매번 찾아보는 것은 너무도 귀찮은 일이고 IDE 의 도움을 받을 수 없다는 부분은 생산성을 꽤 저하시키는 일입니다.

따라서 aws-redshift 에 맞게 타입 정의를 좀 더 디테일하게 해보도록 하겠습니다.

 

우선 npm 페이지를 참고하면, aws-redshift 모듈이 export default 하는 객체는 생성자입니다.

생성자를 통해 객체를 만들어서 connection 을 맺을 수 있고 그 객체를 통해 query, parameterizedQuery, close 등의 메서드를 사용할 수 있습니다. 해당 모듈에 대한 더 상세한 내용은 이 포스팅 주제와 별 상관없으니 넘어가도록 하겠습니다.

그럼 우리는 위에서 사용했던 declare 구문 안에 이 스펙에 맞는 생성자를 정의해서 넣어주면 됩니다.

 

type ClientOption = {
  user: string;
  database: string;
  password: string;
  port: number;
  host: string;
  max: number;
  connectionTimeoutMillis: number;
  idleTimeoutMillis: number;
};

declare module 'aws-redshift' {
  class Redshift {
    constructor(option: ClientOption);

    query(queryString: string, option?: any): Promise<any>;

    parameterizedQuery(
      queryString: string,
      params: Array<any>,
      option?: any
    ): Promise<any>;

    close(): void;
  }

  export = Redshift;
}

 

새로 작성한 index.d.ts 는 위와 같습니다. ClientOption 은 별 중요한 내용은 아니고, declare 구문 안에 Redshift 라는 클래스를 만들어서 export 해준게 전부입니다. 객체를 생성할 수 있도록 Redshift 라는 클래스를 정의해서 export 하고, 3개의 메서드를 가지도록 코드를 추가했습니다. 이렇게 작업한 이유는 위에서 얘기한 것 처럼 aws-redshift 라는 모듈에 맞춘 것 입니다.

 

이렇게 타입 정의를 추가해줬을 때 IDE 에서 어떻게 변하는지 보도록 하겠습니다.

 

aws-redshift constructor error

 

기존 타입 정의에 declare module 'aws-redshift' 한줄만 넣었을 때와 달리 객체를 생성하려고 할 경우, 생성하기 위한 'option' 이라는 인자가 들어오지 않았다고 IDE 에서 알려줍니다. 위의 타입 정의에서 constructor(option: ClientOption) 이 들어가있기 때문입니다.

 

Redshift query method

 

client 라는 Redshift 객체를 생성해서 어떤 메서드가 제공되는지도 IDE 의 도움을 받을 수 있습니다. 이 또한 별도로 작성한 타입 정의 덕분입니다. 옆의 메서드 설명에서도 index.d.ts 에 추가한 것 처럼 queryString 과 option 두 인자를 받아 Promise<any> 를 반환한다고 알려주고 있습니다.

 

query method error

 

마찬가지로 query 메서드를 아무런 인자 없이 사용할 경우, IDE 에서는 인자가 필요하다고 알려줍니다.

 

- 다른 디렉토리에서 사용

 

위의 경우는 어디까지나 src 에서 작업하는 파일의 경우였습니다.

설정마다 다를 수 있지만 저는 tsconfig.json 에 include 설정이 src/**/* 로 되어있어서 src 하위의 파일들만 설정의 영향을 받습니다.

따라서 같은 설정이라면 테스트 코드를 위한 test 디렉토리가 src 와 같은 레벨에 위치해있다고 한다면, aws-redshift 모듈에 대해 별도로 타입 정의한 index.d.ts 의 도움을 받지 못합니다.

 

{
  "compilerOptions": {
    ...,
    "typeRoots": ["./node_modules/@types", "./src/types"]
  },
  "include": [
    "src/**/*",
    "test/**/*"
  ],
  ...
}

 

이럴 경우 위처럼 include 설정에 test/**/* 를 넣어주면 테스트 코드 작성 시에도 별도로 작성한 타입 정의 파일의 도움을 받을 수 있습니다. 물론 이것은 하나의 예시일 뿐, 다른 디렉토리에도 응용이 가능하며 만약 테스트 코드가 별도 디렉토리로 빠져있지 않다면 별로 필요한 내용은 아닙니다. 또한 이렇게 include 설정을 건드릴 경우 빌드 결과물의 구조가 달라질 수 있기 때문에 그 점은 유의해야 합니다.

 

여기까지 aws-redshift 를 예시로 타입 정의가 없는 외부 모듈에 대해 직접 타입 정의를 프로젝트에 추가하는 방법을 알아봤습니다.

미리 외부 모듈의 사용법을 알고 작업하면 되므로 개발자의 성향에 따라 이는 별로 유용하다고 생각되지 않을 수 있습니다.

하지만 분명 혼자 작업할 때도 이런 타입 정의에 의한 suggest 가 없다면 사용법이 적힌 doc 을 여러번 열어야 할 것이며, 다른 팀원들과 함께 작업한다면 이런 부분은 생산성에 더 큰 영향을 줍니다.

 

이번 포스팅에서 저는 어디까지나 딱 필요한 부분만 타입 정의를 추가해 사용하는 예시를 보였지만 외부 모듈이 제공하는 모든 함수나 클래스, 메서드 등을 전부 정의해 @types/aws-redshift 와 같은 모듈을 npm 에 올려 오픈소스에 기여할 수도 있습니다.

 

다음 포스팅에선 동일한 주제는 아니지만 이 예시를 그대로 가져가서 좀 더 여러가지 타입 정의를 해보도록 하겠습니다.

'Backend > Typescript' 카테고리의 다른 글

Typescript Generic 활용  (0) 2021.12.18