OpenAPI Generator로 API의 안전한 Model과 정형화된 구현코드 자동생성하기 를 프로젝트에 적용한 후기를 정리한 글이다. 실제로 적용하면서 발생한 사례와 어떤 관점으로 반영되었는지 정리하고자 한다.
Vue 3, Vuex, Pinia, JavaScript, TypeScript 등
장황함
을 없애기 위해 노력하였다. 하지만 모든 코드가 composable하게 개발되지는 않았다(초창기 파일들은 하나의 파일에 로직이 다 들어있다).props drilling
을 그대로 적용한 컴포넌트도 있고, store를 사용하여 해결한다거나 provider 패턴을 사용하여 해결한 코드들이 존재한다.openapi-generator를 사용하여 생성된 api 함수를 변경/교체하면서 전체적인 서비스 레이어를 점검하게 되었는데, 이 때 느낀 점은 다음과 같다.
참고로 Vue 3의 defineProps의 타입스크립트 공식 지원하기 전에 플러그인을 설치하여 우회하여 interface를 import할 수 있다.
vite-plugin-vue-type-imports 사용하면 다음과 같이 컴포넌트와 컴포저블 사이에 interface를 공유할 수 있다.// types.ts export interface User { username: string password: string avatar?: string }
// component.vue <script setup lang="ts"> import type { User } from '~/types' defineProps<User>() </script> <template>...</template>
이 전에 작성한 글에서 openapi-generator를 통해 생성된 코드를 일괄 교체하면서 해결하고 싶었던 부채 5가지를 정리하였다.
BE의 API의 OAS와 FE의 service layer의 API 구조가 1:1 관계로 변경되었다. 프로젝트에서 적용된 컨트롤러 파일 개수는 약 75개 정도였고, 함수는 약 300개 였다.
Swagger UI 문서의 OAS의 Model과 FE의 Type이 동일하게 가져가고 싶은 부분은 약 80%정도 만족한다. 우선 이 이슈는 API-first Development 방식이 전제되어야 하며, OAS가 정확하지 않다면 자동생성된 Model에도 사이드 이펙트를 끼칠 수 있다. 다음과 같은 시행착오를 겪었다.
required: true
되어있는지 확인해보자. required가 빠져있다면 생성되는 interface의 속성들이 모두 optional처리가 된다. 이 Model 파일을 프로젝트에 적용하니 optional chaining
과 assertion
처리를 하게되었다. 진짜 필요한 속성인지 아니면 옵셔널한 속성인지에 대해 정의가 잘 되어있다면 더 좋은 코드가 되었을 텐데 많이 아쉬웠다.API 함수 코드의 퀄리티는 당연 보장되었다. 서비스 레이어의 함수의 기준이 없어 다양한 방식으로 개발되어있던 부분을 하나의 패턴과 동일한 퀄리티로 리펙토링할 수 있어서 좋았다.
API 구조은 generator로 생성할 때의 속성을 정해놓아서 바로 적용할 수 있어 동일한 폴더, 파일명의 규칙을 가질 수 있었다.
접두어 규칙을 변경
하여, (구) 함수와 (신) 함수를 한 눈에 구분할 수 있도록 하였다.generator 프로젝트를 별개로 생성할 수도 있고, 기존 프로젝트 내에 설치할 수도 있는데 이 부분은 나중에 바쁜 일이 끝나고나서 자동화에 대해 고민해봐야겠다.
컴포넌트의 종류로는 다음과 같이 정의할 수 있다.
- Container 컴포넌트 : 하위에 container, presentational 컴포넌트를 가질 수 있으며 어떠한 역할을 할 지에 대해 책임지는 컴포넌트이다. 주로 store와 연결하여 state를 가지며 props로 하위 컴포넌트에 연결한다.
- Presentational 컴포넌트 : UI 렌더링과 이벤트의 역할을 하며 재사용을 위해 잘게 나눠야 하는 컴포넌트이다.
이러한 역할을 하는 컴포넌트들의 사이에 store가 존재하며 API 호출이나 computed 로직이 존재해야한다. 하지만 현재 프로젝트에서는 그런 이상적인 구조가 모두 적용되지 않았다. UI와 데이터(도메인) 간의 결합성이 높은 부분은 API 코드를 적용하면서 신경쓰는 부분이 너무 많았다. 관여된 코드도 많아서 특정 부분만을 교체하는 것이 아닌 많은 범위의 코드를 교체하게 되었다.
[NHN FORWARD 22] 괴물 같이 변한 Dooray! 웹앱 정리하기 세션에 많은 내용들이 집약되어있다. 처음에는 이 세션을 보고 복잡한 서비스에서의 컴포넌트와 데이터, 유지보수성에 대해 이야기로만 생각하였으나 직접 API 서비스 레이어를 리펙토링하면서 왜 API 로직과 UI 컴포넌트간에 높은 응집도와 낮은 결합도를 가지는 구조로 개발되어야 하는지
에 대해 몸소 깨닫게 되었다.
import { axiosInstance, PromiseAxiosResponse } from '@/worker/commands/config/apiInstance';
{{#withSeparateModelsAndApi}}
import {
{{#imports}}
{{classname}},
{{/imports}}
} from '../model';
{{/withSeparateModelsAndApi}}
{{#operations}}
{{#operation}}
{{^isDeprecated}}
const {{nickname}}Axios = ({
{{#allParams}}
{{paramName}},
{{/allParams}}
}: {
{{#allParams}}
{{paramName}}{{^required}}?{{/required}}: {{=<% %>=}}<%&dataType%><%={{ }}=%>;
{{/allParams}}
}): PromiseAxiosResponse<{{{returnType}}}{{^returnType}}void{{/returnType}}> => {
const localVarPath = `{{{path}}}`{{#pathParams}}
.replace(`{${"{{baseName}}"}}`, encodeURIComponent(String({{paramName}}))){{/pathParams}};
return axiosInstance.{{#lambda.lowercase}}{{httpMethod}}{{/lambda.lowercase}}(
localVarPath,
{
params: {
{{#allParams}}
{{paramName}},
{{/allParams}}
}
})
};
{{/isDeprecated}}
{{/operation}}
export {
{{#operation}}
{{^isDeprecated}}
{{nickname}}Axios,
{{/isDeprecated}}
{{/operation}}
};
{{/operations}}
API 구조은 generator로 생성할 때의 속성을 정해놓아서 바로 적용할 수 있어 동일한 폴더, 파일명의 규칙을 가질 수 있었다. 라고 하셨는데, b/e controller 기준으로 api 폴더가 나눠졌다는 말씀 이신가요? 아니면 api와 model 폴더에 파일명이 규칙을 가진다는 말씀을 하신건가요? 폴더로 분리하는 옵션을 찾고 있는데 정보 공유 가능하시다면 댓글 부탁드립니다 ㅠ