사내 프로젝트가 막 시작하였을 당시 새로 도입된 기술들도 많았고, 정해진 일정과 아직 정리되지 않은 규칙들이 많았다. 구현할 기능에 대한 스펙도 설익은 상태에서 기획과 API가 계속 바뀌던 시기가 있었다. 이 시기에 막 Swagger UI도 도입되었고 BE와 FE의 커뮤니케이션이 피어나기 시작했다.
시간이 흘러 프로젝트가 하나의 제품이 되고 스펙이 정해지고 세일즈되기 시작했다. 프로젝트 초기에 있었던 API 구현에 대한 문제를 해결하고 싶었고, 어떻게 하면 Type-Safe
하게 API를 구현할 수 있을지에 대해 다시 찾아보기 시작했다.
FEConf 2020에서 [B2] OpenAPI Specification으로 타입-세이프하게 API 개발하기: 희망편 VS 절망편 세션과 TypeScript로 Open API JSON Schema Test하기 블로그 글을 인상 깊게 보았고, 현재 상태의 기술 부채를 해결하고 싶었다.
Swagger UI를 사용하면 FE 개발자나 기획자들도 누구나 구현없이 API 리소스를 볼 수 있고, 간단한 입력 값과 클릭으로 데이터를 출력할 수 있도록 도와준다. Swagger는 OAS로 자동 생성되며, API 개발자와 FE 개발자 간의 커뮤니케이션을 쉽게 할 수 있도록 해준다. Swagger UI 도입 후에는 어떤 데이터를 보여줄 것인지, 어떤 값들이 필요한 타입인지 등에 대한 부분을 명확하게 볼 수 있었고, UI 페이지에서 결과를 바로 볼 수 있어 FE 개발자가 테스트하기 편한 환경이 되었다.
API 통신은 Axios 라이브러리를 사용하여 구현하고 있으며, baseURL과 interceptor 등의 옵션마다 axios instance를 만들어 사용하고 있었다.
제품 개발 초기부터 현재까지 API의 버전, 생성 규칙, 코딩 컨벤션, 응답 데이터 타입 등이 계속 달라졌고, FE 프로젝트의 API 관련 폴더와 파일 구조 및 코드 퀄리티도 많이 달라졌다.
가장 큰 문제점은 Swagger UI의 형태가 FE의 API 구조와 달랐다는 점이다. 그 이유로는 여러 가지가 있는데,
controller
와 method
의 관계가 점진적으로 변경되는 사이에 FE 프로젝트에는 이전 방식의 구조가 유지가 되었다.openapi-generator 라이브러리와 openapi-typescript-codegen 라이브러리를 후보군으로 삼았다. openapi-generator의 경우 github star가 14.6K, openapi-typescript-codegen의 경우 github star가 1.3K로 차이가 있었다. 그리고 openapi-generator의 공식문서가 더 상세하였으며, 지원하는 언어도 많았고, FE CONF 2020의 [B2] OpenAPI Specification으로 타입-세이프하게 API 개발하기: 희망편 VS 절망편
세션에서 사용했기 때문에 openapi-generator
를 선택하였다.
OpenAPI Spec 2, 3버전 모두 지원하고 있으며, 공식문서가 정말 잘 제공되어있었다. 사용법과 속성들도 다양해서 생각한 것보다 더 많은 기능들이 존재하였다. openapi-generator 기능들을 손쉽게 사용하기 위해 openapi-generator-cli
를 사용하였다.
Front-end에서 OAS generator를 어떻게 쓰면 좋을까? 블로그 글에서 다양한 사용법들을 볼 수 있었고, 많은 도움을 받았다.
Swagger UI의 헤더 우측에는 definition을 선택하는 셀렉트박스가 존재하고, 선택한 definition의 json/yaml을 볼 수 있는 link가 상단에 존재한다. 이 링크를 클릭하면 텍스트를 볼 수 있는데, 이를 활용할 것이다.
Swagger UI의 Demo인 OAS3 버전의 Petstore 문서를 활용하여 생성을 진행할 것이다.
# install the latest version of "openapi-generator-cli"
npm install @openapitools/openapi-generator-cli -g
openapi-generator 문서에서 CLI 설치 페이지를 참고해보자. npm 방식으로 Windows 환경에서 설치하였다.
openapi-generator-cli version
version을 확인해본다. 이 때 명령어에서 이 시스템에서 스크립트를 실행할 수 없으므로...
와 같은 에러가 발생하면 command를 관리자 권한으로 실행해서 사용한다.
npm init
package.json 파일을 생성한다. scripts 명령어를 쉽게 사용하기 위함이다.
generators list에서 genearator name을 typescript-axios로 사용하였다.
{
"name": "openapi-generator-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate": "openapi-generator-cli generate -g typescript-axios -i ./input/pet-store.json",
},
"author": "",
"license": "ISC"
}
다음과 같이 package.json 파일을 구성하고 npm run generate를 실행해보자!
root 폴더로 여러 파일들이 생성되는 모습을 볼 수 있다. 하지만 generate를 통해 결과 파일들이 나오지 않았다. generate 스크립트를 제대로 실행하기 위해 몇 가지 설정이 필요하다.
script 옵션들
- -g : generator를 설정하는 옵션이고
typescript-axios
를 선택하였다.- -i : input을 의미하며, yaml/json 파일의 위치를 지정한다
- -o : output을 의미하며, 코드를 생성할 위치를 지정한다.
- -c : generator 설정파일의 위치를 의미한다.
- -t : 커스텀 template 설정파일의 위치를 의미한다.
openapi-generator-cli generate -g typescript-axios -i [yaml/json의 URL] -o [output하는 PATH] -c ./config.yaml
typescript-axios의 generator 설정 옵션은 openapi generator 문서 해당 페이지에서 참고하였다.
우선 한 번 generate 명령어를 실행해보았다. 위 이미지에 openapitools.json 파일이 존재하는데 이처럼 설정을 위해 json 파일을 하나 추가해보겠다.
root 경로에 openapi.json 파일을 생성하였으며 아래와 같이 추가해준다.
{
"spaces": 2,
"modelPackage": "src/model",
"apiPackage": "src/api",
"supportsES6": true,
"withNodeImports": false,
"useSingleRequestParameter": true,
"enumNameSuffix": "",
"withSeparateModelsAndApi": true
}
openapi-generator-cli generate -g typescript-axios -i ./input/pet-store.json -c ./openapi.json
다음과 같은 명령어를 실행하였다.
이 때 -o [output path]를 명령어에 추가하지 않는 이유는 기본 아웃풋 폴더는 src이며, 설정파일인 openapi.json에 modelPackage, apiPackage path를 추가하여 설정하였기 때문이다. 만일 output path가 있다면 그 폴더 이후에 modelPackage, apiPackage path가 이어서 설정된다.
root에 src 폴더가 생성되고, modelPackage는 model 폴더 안에, apiPackage는 api 폴더 안에 생성되었다.
템플릿없이 기본적으로 제공하는 generate를 사용하면 api, model 폴더가 생성된다. 이 때, model 폴더의 경우 interface가 어느정도 이쁘게 나오는 것을 볼 수 있다. 하지만, api 폴더 안에 생성된 ts 파일의 경우 기본적으로 제공되는 코드가 나온다.
OpenAPI Specification으로 타입-세이프하게 API 개발하기 유투브 영상의 코드이다. 위처럼 코드가 생산되는데, 이 코드는 실제로 다루기에 너무 번거롭고 상용구들이 많아 템플릿을 이용하여 hygen처럼 정형화된 코드 템플릿을 구성하기로 하였다.
위에 첨부된 generator 이미지를 보면 generator default templating engine은 mustache 언어를 사용한다고 되어있다. generator를 실행하면 기본적으로 내장되어있는 템플릿으로 파일이 생성된다.
기본 설정된 template이 아니라 custom template을 사용하는 방법은 retrieving templates 내용을 확인해본다.
// package.json
{
"name": "openapi-generator-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate": "openapi-generator-cli generate -g typescript-axios -i ./input/pet-store.json -o ./generate -c ./config.yaml",
"template": "openapi-generator-cli author template -g typescript-axios -o ./mustaches"
},
"author": "",
"license": "ISC"
}
우리는 template을 typescript-axios를 사용하고 있으므로 package.json에 다음과 같은 스크립트를 추가한다.
npm run template
스크립트를 실행하면, mustaches 폴더에 template 파일들이 생성된다.
// package.json
{
"name": "openapi-generator-cli",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"generate:default": "openapi-generator-cli generate -g typescript-axios -i ./input/pet-store.json -o ./generate -c ./config.yaml",
"generate": "openapi-generator-cli generate -g typescript-axios -i ./input/pet-store.json -o ./generate -t ./mustaches -c ./openapi.json",
"template": "openapi-generator-cli author template -g typescript-axios -o ./mustaches"
},
"author": "",
"license": "ISC"
}
내장된 template을 사용하는 명령어를 npm run generate:default로 custom template을 사용하는 경우는 npm run generate로 변경해보았다.
template의 내용을 변경하려면 mustache.js 깃허브와 mustache 메뉴얼을 참고해보자.
메뉴얼 문서를 보면 mustache K-V 매핑, if 문, for 문, camelCase/PascalCase 등 case를 변경하는 문법 외에는 거의 기능이 존재하지 않는다. 정말 기본적인 언어이므로 js의 내장함수와 같은 고급(?) 기술을 사용할 수 없다. (아니면 못찾는 거일수도...)
template을 수정하는 방식은 Front-end에서 OAS generator를 어떻게 쓰면 좋을까? 블로그 중 Custom Template 예시 단원을 많이 참고하여 수정하였다.
modelGeneric.mustache 파일의 내용을
{{#vars}}
{{#isEnum}}
export enum {{enumName}} {
{{#allowableValues}}
{{#enumVars}}
{{{name}}} = {{{value}}}{{^-last}},{{/-last}}
{{/enumVars}}
{{/allowableValues}}
}
{{/isEnum}}
{{/vars}}
아래와 같이 변경하였다.
{{#vars}}
{{#isEnum}}
export type {{enumName}} =
{{#allowableValues}}
{{#enumVars}}
{{{value}}} {{^-last}}|{{/-last}}
{{/enumVars}}
{{/allowableValues}}
{{/isEnum}}
{{/vars}}
API 서버에서 정의한 snake case의 변수명을 camel case로 변경하는 방식이다. API 서버에서 camel case로 이미 변수명을 지었다면 변경할 필요가 없다.
{{baseName}}
-> {{#lambda.camelcase}}{{baseName}}{{/lambda.camelcase}}
apiInner.mustache
파일의 내용이 실제로 api 폴더 안에 생성되는 파일의 코드 형태이다. 이 파일을 mustache문법을 활용하여 변경해보자.
generate가 정상적으로 되지 않을 때
[main] WARN o.o.c.config.CodegenConfigurator - There were issues with the specification, but validation has been explicitly disabled.
위처럼 WARN이 발생하고 Errors가 나타나면서 생성이 안되는 경우가 존재한다.
"generate": "openapi-generator-cli generate -g typescript-axios -i ./input/pet-store.json -t ./mustaches -c ./openapi.json --skip-validate-spec",
--skip-validate-spec
를 script 마지막에 추가해보자.
pet-store의 OpenAPI의 json파일로 generate하면 다음과 같은 api 파일이 생산된다.
좌측의 apiInner.mustache 파일의 내용을 실제 프로젝트에서 사용되는 API 코드의 형태로 변경해야한다. 이 때 가장 문제점은 API 함수명을 어떻게 만드는가?이다.
{{classname}}
, {{nickname}}
, {{operationId}}
같은 변수를 사용하여 API 함수명 명명 규칙을 세웠다. 이 방법은 팀이나 조직마다 다를 수 있으니 단순 참고용으로 사용하면 좋을 것 같다.
이미 BE에서 사용되는 메소드명을 변경하기에는 사이드이펙트가 클 것으로 예상되어 {{operationId}} 속성을 추가하고, API Path로 조합해서 유니크한 id를 생성하여 이를 Swagger로 보내는 방식을 사용하여 도입하였다. 하지만 정답은 없기 때문에 이 스펙은 각자의 취향에 맡긴다.
apiInner.mustache의 내용을 변경한 결과 pet-api.ts 파일이 위 이미지처럼 나온다. 코드를 어느정도 에러없이 편하게 보고싶고 약간의 코드나 타입의 도움을 받기 위해서 프로젝트에 typescript와 axios를 설치하면 된다.
100% 자동화가 이뤄질 수 없는 것은 사실이다. openapi-generator-cli를 단독으로 프로젝트를 만들어 실행할 수 있고 기존 프로젝트에 추가적으로 설치하여 제너레이팅할 수 있다.
{{#operations}}
{{#operation}}
{{^isDeprecated}}
const {{nickname}}Axios = ({
{{#allParams}}
{{paramName}},
{{/allParams}}
}: {
{{#allParams}}
{{paramName}}{{^required}}?{{/required}}: {{=<% %>=}}<%&dataType%><%={{ }}=%>;
{{/allParams}}
}): PromiseResBody<{{{returnType}}}{{^returnType}}void{{/returnType}}> => apiInstance.{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}(
'{{path}}',
{
params: {
{{#allParams}}
{{paramName}},
{{/allParams}}
}
}
);
{{/isDeprecated}}
{{/operation}}
export {
{{#operation}}
{{^isDeprecated}}
{{nickname}}Axios,
{{/isDeprecated}}
{{/operation}}
};
{{/operations}}
위 코드는 apiInner.mustache
파일의 일부 내용이다. custom template을 만들어서 이 템플릿에 맞는 API 파일이 생성되도록 구현하였다. 하지만 mustache 문법상 구현하고 싶은 문법이 100% 모두 구현할 수 없었다(언어의 한계점?). {{=<% %>=}}<%&dataType%><%={{ }}=%>
이 코드가 인상적인데 type을 나열해주기 위해서는 위와 같이 쓸 수 밖에 없다.
API 전체의 기술 부채와 전체적으로 갈아엎을 때는 위와 같은 방식으로 진행하기에 좋겠지만, OAS의 json파일의 내용으로 코드를 생성하다보니 원하는 함수만 코드 및 타입 생성을 하고 싶은 경우에 어떻게 진행해야할 지에 대한 생각을 하지 못했다. API 서버가 Swagger UI의 많은 definition을 갖고있는 경우 여러 json파일이 생겨나는데, 이 부분을 한 번에 제너레이팅하는 방법도 찾아야할 것 같다.
path에 파라미터가 들어가는 경우에는 mustache의 한계점(?)으로 인해 완전한 코드 생성이 되지 않았다.
const localVarPath = `{{{path}}}`{{#pathParams}}
.replace(`{${"{{baseName}}"}}`, encodeURIComponent(String({{paramName}}))){{/pathParams}};
문법을 따로 만들어서 apiInstance에 넣어주면 되지만, 매 번 들어가야하는 상용구가 되버린다. 또한, api 함수의 파라미터는 하나의 객체로 구성되어있는데, 특정 파라미터만 프리미티브 타입으로 넣는 규칙이 있는 경우는(특정 파라미터를 따로 넘기는 경우) 일부 코드를 수정해줘야한다.
그리고 파라미터가 없는 경우에는 완벽한 코드 생성이 되지 않아, 이 부분은 프로젝트에 옮긴 이후에 eslint auto fix를 사용하여 어느정도 코드 포메팅이 되지 않을까 생각한다.
처음에 아무 문제없이 generate하다가 특정 JSON 파일에서 문제가 발생하였다. 이 문제를 해결하기 위해 openapi-generate-cli의 버그인 줄 알고 github > issue를 찾아보고 API의 로직 문제라고 생각했다. 하지만 단순한 JSON의 parsing 문제였다. 너무 어렵게 생각해서 정상적인 결과를 도출하기 위해 시간을 많이 썼다.
generator 적용 전에 구글에서 json validator와 json beatifier 모두 실행해서 테스트를 해보자.
{{HttpMethod}} 변수를 사용하여 코드를 자동 생성하였지만 delete만 _delete로 생성되었다. 이 부분은 github에서 문제를 해결할 수 있었다. generate의 script에 --reserved-words-mappings delete=delete
코드 옵션을 추가한다.
"generate": "openapi-generator-cli generate --reserved-words-mappings delete=delete -g typescript-axios -i ./input/pet-store.json -o ./generate -t ./mustaches -c ./openapi.json --skip-validate-spec",
API 코드를 모두 갈아엎을 각오로 리펙토링과 자동화를 도입하는 경우 외에도 다른 방식으로 사용할 수 있을지에 대해 좀 더 조사가 필요하다. 확실히 100% 자동화는 아니지만 거의 동일한 코드 퀄리티와 도메인 단위로 API 구조가 구성되는 부분에 대해서는 만족한다.
기회가 된다면 Swagger가 아닌 Postman을 가지고 자동 생성해보고 싶은 생각도 존재한다. 또한 생성된 코드를 가지고 테스팅까지 하면 좋을 것 같다는 생각이 든다.
모든 기술의 도입에는 Trade-off가 존재한다. 나와 비슷한 생각을 하시는 개발자들이 발퀄인 이 글을 보고 어떠한 과정과 결과를 얻을 수 있는지 간접 체험을 하면서 openapi-generator를 도입하는 것이 좋을 지에 대해 고민하고 도움이 되면 정말 좋을 것 같다.
FE는 기획, 디자인, API의 영역이 모두 걸쳐있는 분야이다. FE 개발자라서 이렇게 재미있고 매력적인 도전과 기술을 사용할 수 있지 않나?라는 생각이 든다.
https://github.com/kdeun1/openapi-generate-project