
이전 회사에서 일할 때, 내가 담당하던 프로젝트의 서버 언어가 node에서 java spring으로 변경되고, 통신 방법도 graphQl에서 REST APIs로 변경된 적이 있었다. 이때 클라이언트에서는 apollo-client와 apollo:codegen을 사용하고 있어서, 이 둘을 대체할 수 있으면서 REST API에 적합한 라이브러리가 필요했다. 몇가지 라이브러리를 찾아보았고 apollo-client는 @tanstack/react-query로, apollo:codegen은 openapi-typescript-codegen으로 대체하였다.
사실 openapi-typescript-codegen보다 swagger-codegen이나 openapi-generator가 더 사용자도 많고 버전도 더 높은데, 왜 이걸 골랐는지 스스로 조금 의문이긴 하다. 그 때 당시 검색도 지금보다 못했고, 오직 typescript에 적합한 것을 찾겠다는 생각 때문에 openapi-typescript-codegen을 고른 것 같다. openapi-swagger를 보긴 했는데, 서버용 라이브러리라고 생각하고 넘겼던 것 같다.
(openapi-typescript-codegen의 옵션을 커스텀하면서 자료가 없어서 너무 힘들었던 기억이 있는데, 왜 자료가 없었던건지 이제야 이해가 된다..)
codegen은 정말 편하지만 몇가지 문제점이 있다. 서버에서 api 명세에 required: true를 따로 명시해주지 않으면, 꼭 담겨오는 값이어도 optional로 render된다. 심지어 nulluble이 아닌데도 optional로 생성된다...
예를 들어 openapi 스펙이 다음과 같으면
"TempDto": {
"type": "object",
"properties": {
"id": { "type": "integer", "format": "int64" },
"name": { "type": "string" },
}
},
typescript에서는 다음과 같이 타입이 정의된다.
export type TempDto = {
id?: number;
name?: string;
};

사실 '이거 서버에서 nulluble로 해놓고 거짓말 하는거 아니야?ㅡㅡ'싶어서 서버 코드에 들어가 Model 정의를 살펴보았는데 진짜 nulluble이 아니었다..
자세한 건 [FeConf] OpenAPI Specification으로 타입-세이프하게 API 개발하기: 희망편 VS 절망편에서 강연자분이 아주 잘 설명해주셨다. 영상에서는 다른 라이브러리인 openapi-generator를 이용했지만 문제점(절망편)은 동일했다. 이전까지는 나만 겪는 문제점이라고 생각했는데, 다른 사람들도 비슷한 어려움을 겪는다니 반갑기도 하고 더욱이 이 문제를 해결하고 싶어졌다.
당연히 안된다. code generate할 때 마다 매번 따로 삭제해줄 것인가? 못할 것도 없겠지만, 그래서야 codegen을 사용하는 의미가 없어진다고 생각한다. 또, 당연히 human error로부터 자유로울 수 없을 것이고.
그래서 자동으로 ?를 삭제해주는 스크립트를 짜기로 결심했다 ^^)/
사실 타입 정의에서 ?만 삭제한다고 완전히 해결되는 것은 아니다. 또 어떤 경우에는 정말로 optional이 필요하기 때문에 모두 삭제할 수도 없었다. (update 요청시 requsetBody로 들어가는 Dto 등) 그래도 !를 조금이라도 없애보고자 + 더 발전시켜서 완벽하게 수정하는 코드를 완성할 수 있지 않을까 하는 기대감에 코드를 작성하기 시작했다.
key?: value;형태를 찾아 key: value;로 수정하는 코드를 작성할 것이다. 사실 node 코드를 많이 작성해보지 않아서 많이 헤맸다. 삽질한게 아까워서 블로그에 기록해본다 ^_ㅠ
우선 ResponseDto만 수정하는 것으로 대상을 한정지었다.
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const FOLDER = "src/generated/models";
const targetPatterns = [/RespDto.ts/];
const keyValueRegex = /(\w+)\?: (.+);/g;
// 1. get targeted file
const files = fs.readdirSync(path.resolve(__dirname, FOLDER), "utf-8");
const targetFiles = files.filter(file =>
targetPatterns
.map(regex => regex.test(file))
.some(result => result === true),
);
// 2. read file and replace
for (const file of targetFiles) {
const fileName = path.parse(file).name;
const inputPath = path.resolve(__dirname, FOLDER, file);
const outputPath = path.resolve(__dirname, FOLDER, `${fileName}_dup.ts`);
const readStream = fs.createReadStream(inputPath, { flags: "r" });
const writeStream = fs.createWriteStream(outputPath, { encoding: "utf8" });
const fileReadLine = readline.createInterface({
input: readStream,
output: writeStream,
crlfDelay: Infinity,
});
for await (const line of fileReadLine) {
const { EOL } = require("os");
const transformed = line.replace(keyValueRegex, "$1: $2;");
writeStream.write(`${transformed}${EOL}`);
}
}
사실 한 줄씩 읽어오기와 원래 파일에 덮어쓰기를 동시에 하는 방법을 몰라서, 번거롭지만 수정파일을 따로 만들고 이름은 변경하는 방식으로 작업했다...
// 3. delete origin and rename dup
for (const file of targetFiles) {
const fileName = path.parse(file).name;
const filePath = path.resolve(__dirname, FOLDER, file);
const fileDupPath = path.resolve(__dirname, FOLDER, `${fileName}_dup.ts`);
fs.unlinkSync(filePath);
fs.renameSync(fileDupPath, filePath);
}
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const FOLDER = "src/generated/models";
const targetPatterns = [/RespDto.ts/];
const keyValueRegex = /(\w+)\?: (.+);/g;
const transform = async () => {
// 1. get targeted file
const files = fs.readdirSync(path.resolve(__dirname, FOLDER), "utf-8");
const targetFiles = files.filter(file =>
targetPatterns
.map(regex => regex.test(file))
.some(result => result === true),
);
// 2. read file and replace
for (const file of targetFiles) {
const fileName = path.parse(file).name;
const inputPath = path.resolve(__dirname, FOLDER, file);
const outputPath = path.resolve(__dirname, FOLDER, `${fileName}_dup.ts`);
const readStream = fs.createReadStream(inputPath, { flags: "r" });
const writeStream = fs.createWriteStream(outputPath, { encoding: "utf8" });
const fileReadLine = readline.createInterface({
input: readStream,
output: writeStream,
crlfDelay: Infinity,
});
for await (const line of fileReadLine) {
const { EOL } = require("os");
const transformed = line.replace(keyValueRegex, "$1: $2;");
writeStream.write(`${transformed}${EOL}`);
}
}
// 3. delete origin and rename dup
for (const file of targetFiles) {
const fileName = path.parse(file).name;
const filePath = path.resolve(__dirname, FOLDER, file);
const fileDupPath = path.resolve(__dirname, FOLDER, `${fileName}_dup.ts`);
fs.unlinkSync(filePath);
fs.renameSync(fileDupPath, filePath);
}
};
transform();
사실 변경 후 벌써 몇가지 문제점을 발견해서.. 실효성있는 코드가 될지는 의문이지만, 그래도 위 작업을 하면서 node 코드로 파일 수정하는 방법을 배울 수 있었다. 언젠가 도움이 되는 날이 오겠지? ^.^)b