[typescript] openapi codegen에서 optional 삭제하기

마리 Mari·2024년 1월 10일
post-thumbnail

openapi-typescript-codegen과 첫 만남

이전 회사에서 일할 때, 내가 담당하던 프로젝트의 서버 언어가 node에서 java spring으로 변경되고, 통신 방법도 graphQl에서 REST APIs로 변경된 적이 있었다. 이때 클라이언트에서는 apollo-clientapollo:codegen을 사용하고 있어서, 이 둘을 대체할 수 있으면서 REST API에 적합한 라이브러리가 필요했다. 몇가지 라이브러리를 찾아보았고 apollo-client@tanstack/react-query로, apollo:codegenopenapi-typescript-codegen으로 대체하였다.

사실 openapi-typescript-codegen보다 swagger-codegen이나 openapi-generator가 더 사용자도 많고 버전도 더 높은데, 왜 이걸 골랐는지 스스로 조금 의문이긴 하다. 그 때 당시 검색도 지금보다 못했고, 오직 typescript에 적합한 것을 찾겠다는 생각 때문에 openapi-typescript-codegen을 고른 것 같다. openapi-swagger를 보긴 했는데, 서버용 라이브러리라고 생각하고 넘겼던 것 같다.
(openapi-typescript-codegen의 옵션을 커스텀하면서 자료가 없어서 너무 힘들었던 기억이 있는데, 왜 자료가 없었던건지 이제야 이해가 된다..)


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 등) 그래도 !를 조금이라도 없애보고자 + 더 발전시켜서 완벽하게 수정하는 코드를 완성할 수 있지 않을까 하는 기대감에 코드를 작성하기 시작했다.


optional을 삭제하는 node 코드 작성하기

key?: value;형태를 찾아 key: value;로 수정하는 코드를 작성할 것이다. 사실 node 코드를 많이 작성해보지 않아서 많이 헤맸다. 삽질한게 아까워서 블로그에 기록해본다 ^_ㅠ

0. 변수 선언

우선 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. 대상 파일 읽어오기

// 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. 파일 한 줄씩 읽고 replace 하기

// 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. original 파일 삭제 후 duplicated file의 이름 변경

사실 한 줄씩 읽어오기와 원래 파일에 덮어쓰기를 동시에 하는 방법을 몰라서, 번거롭지만 수정파일을 따로 만들고 이름은 변경하는 방식으로 작업했다...

// 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


참고자료

profile
우리 블로그 정상영업합니다.

0개의 댓글