콘텐츠로 이동

Contract 작성법

본 글은 ts-rest 공식문서 가이드 를 토대로 작성된 글입니다.

자세한 설명은 원본을 참고해주세요

Contract 에 대한 사전 지식

Contract 를 작성하는 이유

  • 백엔드 서비스가 계약한 API 스펙대로 동작하는지 확인해야 한다.
  • 서비스가 요청 및 응답 데이터를 올바르게 처리하는가: XML, JSON, Query params 등
  • 데이터 포맷이 완성되었는가: 최대길이, 데이터타입, 요구되는 필드 등
  • HTTP status 가 올바르게 사용되었는가
  • 백엔드가 리소스/메서드마다 요청에 대해 응답하는가
  • 집현전 프로젝트는 Contract 를 작성하는 툴로 ts-rest 를 사용한다.

ts-rest 특징

  • ts-restAPI 에 대한 계약을 정의하는 방법 을 제공한다.
  • 종단 간 type-safe 보장
  • RPC 와 같은 클라이언트 인터페이스 제공
  • 작은 번들 크기(2.2kb!)
  • 테스트에 용이하다
  • 추가적인 코드 생성 없음 >많은 프레임워크나 라이브러리에서는 특정 기능이나 구조를 위해 자동으로 코드를 생성하기도 합니다.

    자동으로 생성된 코드는 때로는 예기치 않은 방식으로 동작할 수 있습니다. 또한 기본 코드와 생성된 코드 사이에서 불일치가 발생할 수 있고 생성된 코드가 업데이트되거나 변경될 때 발생하는 부작용을 추적하기 어려울 수 있습니다.

  • 런타임 유형 검사를 위한 Zod 지원

  • OpenAPI 통합 가능

ts-rest 사용 전 준비

zod 를 설치해야 한다. tsconfig.json 파일에 strict 를 허용해야한다. Zod 를 사용하기 위해서 필요

    {
      "compilerOptions": {
        ...
        "strict": true
      }
    }
  • zod 사용법도 익혀두어야 한다.

집현전 프로젝트에서 contract 추가

  • ~/backend/contract 디렉토리에서 작업이 이루어진다
  • 전체적인 작업 프로세스는 프로젝트 개요 참조
  • ~/backend/contract/src 내부에 router 마다 디렉토리를 생성해서 작성하면 된다.
  • reviews contract 를 예시로 들어 설명하겠다.

reviews contract 설명

reviews/index.ts
import { initContract } from '@ts-rest/core';
import { z } from 'zod';
import { bookInfoIdSchema, bookInfoNotFoundSchema } from '../shared';
import {
    contentSchema,
    mutationDescription,
    reviewIdPathSchema,
    reviewNotFoundSchema,
} from './schema';

export * from './schema';

//  contract 를 생성할 때, router 함수를 사용하여 api 를 생성
const c = initContract();

export const reviewsContract = c.router(
    {
        post: {
            method: 'POST',
            path: '/',
            //  z.object 는 zod 라이브러리의 문법이다. zod 사용법 참고
            query: z.object({ bookInfoId: bookInfoIdSchema.openapi({ description: '도서 ID' }) }),
            description: '책 리뷰를 작성합니다.',
            body: contentSchema,
            responses: {
                201: z.literal('리뷰가 작성되었습니다.'),
                // 아래외 같이 Schema 로 분리할 수 있다. schema.ts 에 어떤 타입인지 선언되어 있음
                404: bookInfoNotFoundSchema,
            },
        },
        //  다른 api 들의 계약서를 선언해준다
        ...

    },
    //  pathPrefix 를 통해 모든 경로에 공통적으로 /reviews 를 붙여줍니다.
    //  여러 api 마다 공통적으로 /reviews/~~~ 를 사용하니까 중복해서 선언하는 것을 방지하기 위함.
    { pathPrefix: '/reviews' },
);

index.ts 설명

모든 contract 들을 모아서 하나의 contract 로 만드는 파일이다. contracts/src 경로에 존재한다.

index.ts
import { initContract } from '@ts-rest/core';
import { reviewsContract } from './reviews';
import { historiesContract } from './histories';
import { usersContract } from './users';
import { likesContract } from './likes';
import { stockContract } from './stock';

export * from './reviews';
export * from './shared';

const c = initContract();

//  다른 contract 를 모아서 하나의 contract 로 만들기.
export const contract = c.router(
  {
    // likes: likesContract,
    reviews: reviewsContract,
    histories: historiesContract,

    stock: stockContract,
    // TODO(@scarf005): 유저 서비스 작성
//     users: usersContract,
  },
  {
    // 모든 경로는 /api/v2 로 들어오기 때문에 묶어서 관리한다.
    pathPrefix: '/api/v2',
    strictStatusCodes: true,
  },
);