회사에서 새로운 프로젝트를 진행하던 중,
TypeScript를 쓰는데 response type을 알 수 없어 TypeScript를 쓰는 의미가 없는 코드가 생기거나
항상 직접 api 요청을 해보고 response type을 클라이언트에서 직접 일일이 작성해 주는 게
너무 불편하고 오래 걸려서 어떻게 좋은 방법이 있지 않을까 하는 고민이 있었다.
이런 이야기를 내 친구인 용준이랑 하다가 openapi-typescript라는 모듈을 써 보는 게 어떻겠냐고 해서 알아봤다.
openapi-typescript는 swagger schema 문서를 읽고 문서의 내용을 TypeScript interface로 바꿔 주는 것 같았다.
마침 우리 개발팀은 api를 swagger로 관리하기 때문에 사용할 수 있었다.
그래서 백엔드 개발자분에게 이러한 게 있어서 schema 문서를 잘 작성해 주시면 업무 퍼포먼스가 더 올라갈 것 같다고 전달하니 흔쾌히 작성해 주셨다.
이제 한번 사용해 보자.
npx openapi-typescript ${schema docs url} --output schema.ts사실 별거 없다. 프로젝트에서 위 코드를 사용해 주기만 하면 된다.
schema 문서의 주소와 저장할 파일의 경로만 입력해 주면 끝이다. 당연히 뒤에 저장할 파일 명은 수정이 가능하다.
schema가 어떻게 생성되는지 실제 사용 코드로 짧게 예시를 올려보겠다.
export interface path {
'/api/v1/groups/{groupId}': {
/** groupId에 해당하는 그룹의 정보를 가져옵니다. */
get: operations['GroupController_getGroup'];
/** groupId에 해당하는 그룹을 삭제합니다. */
delete: operations['GroupController_removeGroup'];
/** groupId에 해당하는 그룹의 정보를 수정합니다. */
patch: operations['GroupController_updateGroup'];
};
}
export interface components {
schemas: {
Group$mZFLkyvTelC5g8XnyQrpOw: {
id: number;
/** Format: date-time */
createdAt: string;
/** Format: date-time */
updatedAt: string;
/** @description 그룹의 도메인, 없으면 기본값으로 그룹의 id가 사용된다 */
domain: string | null;
/** @description 그룹 이름 */
name: string;
/** @description 그룹을 생성한 유저 id */
creatorId: number;
};
};
}
export interface operations {
/** groupId에 해당하는 그룹의 정보를 가져옵니다. */
GroupController_getGroup: {
parameters: {
path: {
groupId: number;
};
};
responses: {
200: {
content: {
'application/json': components['schemas']['Group$mZFLkyvTelC5g8XnyQrpOw'];
};
};
/** 토큰이 없거나 잘못되었을 때 */
401: unknown;
/** 토큰은 정상적이나 리소스에 접근할 권한이 부족하면 forbidden 대신 not found가 반환됨 */
404: unknown;
};
};
}path, comonents, operations가 이런 식으로 이쁘게 정리되어 나온다. 백엔드 개발자가 schema 문서에 주석을 이쁘게 잘 달아놓으면 그것까지 잘 가져오는 모습이다.
schema.ts가 이렇게 생성이 잘 되었으면 type이 필요한 곳에서 가져다 쓰면 된다.
import { components } from './schema.ts';
type APIResponse = components['schemas']['APIResponse'];이런 식으로 schema.ts를 import 해준 후 components["schemas"]["response name"]; 이렇게 사용하면 된다. components의 마지막 배열 key 값은 swagger schema에 백엔드 개발자분이 짠 이름으로 되어 있을 것이다.
그리고 api response뿐만 아니라 api request 시에 필요한 데이터도 받아 올 수 있다.
import { operations } from './schema.ts';
type getUsersById = operations['getUsersById'];이렇게 operations에서 요청할 api를 key 값으로 넣고 가져오면 해당 api 요청 시에 필요한 data의 type을 가져올 수 있다.
실제 사용하고 있는 코드를 예시로 짧게 올려보겠다.
import { authenticateRequest } from '@/api/authenticateRequest';
import type { components, operations } from '@/types/model';
type GetGroupInfoPath = operations['GroupController_getGroup']['parameters']['path'];
interface GetGroupInfoProps {
path: GetGroupInfoPath;
}
type GetGroupInfoResponse = components['schemas']['Group$mZFLkyvTelC5g8XnyQrpOw'];
export async function getGroupInfo({
path,
}: GetGroupInfoProps): Promise<GetGroupInfoResponse | false> {
const response = await authenticateRequest<GetGroupInfoResponse>({
method: 'get',
url: `/api/v1/groups/${path.groupId}`,
useGroupId: false,
});
return response;
}
export type { GetGroupInfoProps, GetGroupInfoResponse };그룹 정보를 요청하는 api 요청 코드이다. 이렇게 api request data와 response data를 가져와 api 요청 코드를 작성 할 수 있다. authenticateRequest는 프로젝트에 맞춰 axois 관련 코드를 감싸는 내가 따로 작성한 함수이다.
서버에서 schema가 바뀌면 처음 schema를 가져올 때 사용한 코드를 다시 쓰면 된다. 새롭게 바뀐 schema가 덮어씌워질 것이다.
더 자세한 내용은 밑에 github 링크를 남겨 두었으니 보고 사용하면 될 것 같다.