Skip to Content
ProductivityZod를 이용한 Json Schema 공유 시스템 만들기

Zod를 이용한 Json Schema 공유 시스템 만들기

서비스를 개발하다보면 프론트엔드와 백엔드에서 공통적으로 읽고 써야 하는 데이터 구조가 생기는데요, 여러곳에서 공유하는 데이터가 많아지다보면 문제가 발생하곤 합니다.

  • 한쪽에서 필드 이름이 바뀌었거나 타입이 달라졌는데, 다른 쪽에는 반영되지 않아 런타임 에러 발생

  • 어떤 값이 필수인지 아닌지에 대한 기준이 불명확

그리고 프로젝트가 서버가 Django로 구현되어 있어 프론트엔드에서 타입 작성과 런타임 오류 방지를 위한 명세가 필요했기도 합니다.

위에서 언급한 문제를 방지하기 위해 타입 정의를 명세로 추출하고, 이를 자동으로 관리하는 시스템이 필요합니다.

이 글에서는 Zod로 타입 정의를 작성하고, 이를 JSON Schema로 추출하여 백엔드와 공유하는 구조를 어떻게 구축했는지에 대해서 정리해봤습니다.

JSON Schema

공식문서에서는 JSON Schema를 JSON 문서의 구조, 제약 조건, 데이터 타입을 주석으로 달고 검증하기 위한 선언형 언어로 이를 통해 JSON 데이터에 대한 기대치를 표준화하고 정의할 수 있는 방법을 제공한다고 설명합니다.

JSON Schema 사용 예시

JSON Schema를 사용해서 유저에 대한 데이터와 결제에 관한 데이터를 검증하는 예시입니다.

사용된 코드를 이해할 수 있도록 간단히 구조를 살펴보면

  • type: 데이터의 유형을 정의 object, array, string, number, integer, boolean, null 등의 타입을 사용할 수 있습니다.
  • properties: 객체 내부의 속성들을 정의
  • required: 객체 항목 중 필수인 항목을 지정
  • minimum: 숫자의 값에 최소값을 설정
{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "user": { "type": "object", "properties": { "id": { "type": "string", "format": "uuid" }, "name": { "type": "string", "minLength": 1 }, "email": { "type": "string", "format": "email" } }, "required": ["id", "name", "email"] }, "payment": { "type": "object", "properties": { "method": { "type": "string", "enum": ["credit_card", "paypal", "bank_transfer"] }, "transactionId": { "type": "string" }, "amount": { "type": "number", "minimum": 0 } }, "required": ["method", "transactionId", "amount"] } }, "required": ["user", "payment"] }

데이터의 타입과 필드를 필수로 설정하거나, 값이 N보다 커야하는 등 JSON 데이터를 검증할 수 있습니다.

예제

위 예시에서는 결제 데이터 중 amount 값의 타입을 숫자로 지정해두었는데요, JSON Schema 검증 도구에 amount를 숫자가 아닌 문자 ($100)을 넣으면 숫자가 아닌 문자가 들어왔다고 검증에 실패하게 됩니다.

이는 특정 언어에 종속받지 않는 규격이기 때문에, 백엔드와 프론트엔드에서 사용하는 언어가 다르더라도 사용 가능합니다. 자바는 networknt/json-schema-validator, 파이썬에서는 jsonschema, 타입스크립트에서는 ajv 등의 라이브러리가 있습니다.

Zod

하지만 JSON Schema만 사용하는 방식은 아쉬움이 있었습니다. 프론트엔드에서는 TypeScript를 사용하고 있기 때문에, 데이터 검증과 동시에 타입까지 추론할 수 있다면 더 효율적일 것이라 생각했습니다.

타입스크립트에서 사용하는 런타임 데이터 검증 라이브러리인 zod를 사용하면,

  1. 데이터 검증 시 타입을 추론해주는 기능도 있어 데이터의 타입을 따로 작성하지 않아도 되고
  2. json schema보다 가독성도 좋고 작성하기 간편합니다.
z.object({ user: z.object({ id: z.string().uuid(), name: z.string().min(1), email: z.string().email(), }), payment: z.object({ method: z.enum(["credit_card", "paypal", "bank_transfer"]), transactionId: z.string(), amount: z.number().min(0), }), });

차라리 데이터 구조를 zod로 작성하고 타입스크립트에서는 그대로 zod를, 다른 언어에서는 zod로 json schema를 추출해서 검증하는게 개발자 경험이 더 좋을 것 같았습니다. 아래 라이브러리로 zod로 json schema를 추출할 수 있습니다.

github Action을 사용한 시스템 구성하기

zod로 스키마를 작성하면 예제 데이터가 스키마에 맞게 되어있는지 검증하고 서버에서 사용하기 위한 Json Schema를 생성하는 파이프라인을 구성하려고 합니다.

깃 협업 중 자동으로 진행되어 최신의 명세를 확인할 수 있다면 좋겠다고 생각해서 Github Actions를 선택했습니다.

  • zod 스키마 작성 -> github actions -> 프로젝트에서 zod Schema나 Json Schema를 사용하는 작업 흐름

예제

Github Action

Github Action은 아래 코드처럼 예시 데이터 검증 (jest 테스트) -> Json Schema 파일 생성 -> 깃에 푸시하는 단계로 구성했습니다.

name: JSON Schema Generation and Validation on: push: branches: - main jobs: build: name: JSON Schema Generator runs-on: ubuntu-latest steps: # 1. Check out the repository - uses: actions/checkout@v2 # 2. Install dependencies using Yarn - uses: borales/actions-yarn@v4 with: cmd: install # 3. Run tests - uses: borales/actions-yarn@v4 with: cmd: test # 4. Run the 'yarn generate' command to generate files - uses: borales/actions-yarn@v4 with: cmd: generate # 5. Check for changes, commit, and push - name: Commit and Push changes if any env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" git add . # Check if there are any changes to commit if git diff --staged --quiet; then echo "No changes to commit." else git commit -m "Generated files via GitHub Actions" # Pull the latest changes from the remote before pushing git pull --rebase origin main # Push the changes to the main branch git push origin HEAD:main fi

스키마를 더욱 이해하기 쉽게 예시 데이터를 넣을 수도 있고, 기존 데이터가 스키마와 충돌되지는 않는지 테스트 할 수 있도록 만들었습니다.

zod 스키마를 수정/추가/삭제해서 main 브랜치에 커밋이 push되면 파이프라인을 실행하고, 예시 데이터 검증 후 문제가 없다면 Json Schema를 추출하고 추출한 파일을 커밋합니다.

프로젝트 파일 구조

schema 안에 각 스키마별로 index.ts에 zod 스키마를 작성하고, 하위 examples 폴더에 예시 json을 넣습니다.

│ generate-schemas.ts │ jest.config.js │ validation.test.ts ├─.github │ └─workflows │ main.yml └─schema └─user │ index.ts │ readme.md └─examples user-pay.json user-only.json

예시 데이터 스키마로 검증하기

validation.test.ts는 ts-jest를 이용해서 작성했고, 스키마별로 index.ts 파일을 읽어서 examples 폴더의 json 파일들을 검증합니다.

import * as fs from "fs"; import path from "path"; const schemaFolders = fs.readdirSync("./schema"); schemaFolders.forEach((schemaFolder) => { describe(`${schemaFolder} 스키마 테스트`, () => { const schemaFolderPath = path.join(__dirname, "schema", schemaFolder); const examplesFolderPath = path.join(schemaFolderPath, "examples"); const schemaFilePath = path.join(schemaFolderPath, "index.ts"); it.each(fs.readdirSync(examplesFolderPath))( `${schemaFolder}/%s validation`, async (fileName) => { const validator = (await import(schemaFilePath)).default; const raw = fs.readFileSync( path.join(examplesFolderPath, fileName), "utf-8" ); const json = JSON.parse(raw); const parseResult = validator.safeParse(json); if (parseResult.success === false) { console.error(parseResult.error); } expect(parseResult.success).toEqual(true); } ); }); });

github actions에서는 아래처럼 테스트 결과를 보실 수 있습니다.

예제

Zod 스키마로 Json Schema 생성하기

zod를 json schema로 바꿔주는 generate-schemas.ts는 테스트 코드처럼 각 폴더의 index.ts 에 default 로 export 된 zod 스키마를 가져온 후, zod-to-json-schema 라이브러리로 json schema 를 추출한 후 파일로 저장합니다.

import fs from "fs/promises"; import path from "path"; import { zodToJsonSchema } from "zod-to-json-schema"; const jsonSchemaFolderPath = path.join(__dirname, "jsonSchema"); (async () => { // jsonSchema 폴더가 없으면 생성 try { await fs.mkdir(jsonSchemaFolderPath, { recursive: true }); } catch (error) { console.error(`Error creating directory ${jsonSchemaFolderPath}:`, error); process.exit(1); // 오류 발생 시 종료 } // 기존 파일 삭제 try { const files = await fs.readdir(jsonSchemaFolderPath); for (const file of files) { const filePath = path.join(jsonSchemaFolderPath, file); await fs.unlink(filePath); } } catch (error) { console.error( `Error reading or deleting files in ${jsonSchemaFolderPath}:`, error ); } // 스키마 폴더 읽기 try { const schemaFolders = await fs.readdir("./schema"); // json 스키마 파일 생성 await Promise.all( schemaFolders.map(async (schemaFolder) => { const schemaFolderPath = path.join(__dirname, "schema", schemaFolder); const schemaFilePath = path.join(schemaFolderPath, "index.ts"); const validator = (await import(schemaFilePath)).default; const jsonSchemaFilePath = path.join( jsonSchemaFolderPath, `${schemaFolder}.json` ); await fs.writeFile( jsonSchemaFilePath, JSON.stringify(zodToJsonSchema(validator), null, 4) ); }) ); } catch (error) { console.error("Error generating schemas:", error); process.exit(1); // 오류 발생 시 종료 } })();

이제 메인 브랜치를 업데이트하면 github action으로 자동으로 예시 데이터를 검증하고, jsonSchema 폴더에 json schema 파일을 저장합니다.

깃을 사용해 자연스럽게 형상 관리도 할 수 있고, 타입스크립트를 사용하는 프로젝트에서는 npm으로 .ts 파일만 배포해서 패키징을 해 zod의 이점을 살려서 개발할 수 있습니다.

이런 문제를 줄이기 위해 스키마를 코드로 정의하고 Git 기반으로 관리하면, 위치나 변경 이력을 명확하게 추적할 수 있고 배포와 검증 과정도 자동화되어 협업에서 발생하는 의사소통 비용을 효과적으로 줄일 수 있습니다.

Last updated on