- 문제 상황
업무간에 외부 모듈을 사용하는 경우는 너무도 많습니다.
당장 생각나는 것들 나열만 해도 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 에서 어떻게 변하는지 보도록 하겠습니다.
기존 타입 정의에 declare module 'aws-redshift' 한줄만 넣었을 때와 달리 객체를 생성하려고 할 경우, 생성하기 위한 'option' 이라는 인자가 들어오지 않았다고 IDE 에서 알려줍니다. 위의 타입 정의에서 constructor(option: ClientOption) 이 들어가있기 때문입니다.
client 라는 Redshift 객체를 생성해서 어떤 메서드가 제공되는지도 IDE 의 도움을 받을 수 있습니다. 이 또한 별도로 작성한 타입 정의 덕분입니다. 옆의 메서드 설명에서도 index.d.ts 에 추가한 것 처럼 queryString 과 option 두 인자를 받아 Promise<any> 를 반환한다고 알려주고 있습니다.
마찬가지로 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 |
---|