June.

10월 안에는 꼭...

Portfolio

About

AJV 및 GitHub Action을 사용하여 JSON 파일에서 일관된 메타 정보 보장하기

2023/05/14
개발

7 min read

AJV 및 GitHub Action을 사용하여 JSON 파일에서 일관된 메타 정보 보장하기

Locales:

en

ko

TL;DR

이 글은

  • Source of Truth (진실의 원천)을 두고 메타 데이터를 쉽게 관리하고 싶은 분
  • 스크립트를 원하는 시점에 실행 및 검증을 자동화하고 싶은 분
  • JSON 데이터의 검증하는 방법을 알고 싶은 분

들이 읽으면 유용합니다.

또한 이 글을 읽으면

  • AJV를 이용해서 JSON 파일 검증하는 방법에 대해서 알게 됩니다.
  • Github Action을 이용해서 검증 스크립트 자동화하는 방법에 대해서 알게 됩니다.

들어가기

우선 메타 데이터가 무엇일까요? "어떤 목적을 가진 데이터"를 메타 데이터라고 합니다. 우리가 자주 사용하는 메타 데이터의 대표적인 예시는 package.json입니다. 프로젝트의 이름, 버전, 설명, 작성자, 의존성 라이브러리 등등 프로젝트에 필수적인 정보라는 목적을 가진 데이터들이 들어있습니다.

제가 맡고 있는 프로젝트인 Seed Design 레파지토리에서 문서 프로젝트가 들어가 있는 docs 폴더가 있습니다. 해당 프로젝트는 디자인시스템의 문서 프로젝트입니다. Avatar 컴포넌트를 예시로 들어보겠습니다.

만약 Avatar에 대한 style 정보, usage 정보, overview 정보 등을 관리하기 위해 따로 mdx 문서를 만들어서 관리하는데 각 문서에서 coverImage를 사용하고 있다고 해보겠습니다. 그렇다면 overview.mdx, style.mdx, usage.mdx 모두 아래와 같이 사용하고 있겠죠.

1{/* coverImage를 직접 부르고 있음 */} 2![coverImage](./coverImage.png)
1# AS-IS 2📦avatar 3 ┣ 📜coverImage.png 4 ┣ 📜overview.mdx # coverImage 사용 중 5 ┣ 📜style.mdx # coverImage 사용 중 6 ┗ 📜usage.mdx # coverImage 사용 중

근데 만약에 coverImage의 스펙이 변경되어서 확장자가 png에서 jpeg로 변경이 됐다고 생각해 보겠습니다. 그럼 우리는 세 개의 파일을 옮겨가며 coverImage의 확장자를 고쳐주어야 합니다. 근데 이런 일이 정말 많아지면 하루 종일 데이터만 고치다가 끝날 수도 있겠네요.

그래서 우리는 메타 데이터 파일을 따로 둡니다. 아래와 같이 말이죠.

1# TO-BE 2📦avatar 3 ┣ 📜coverImage.png 4 ┣ 📜component-meta.json # 메타 데이터 파일 5 ┣ 📜overview.mdx 6 ┣ 📜style.mdx 7 ┗ 📜usage.mdx

그리고 해당 JSON 파일에는 이런 식으로 적혀있을 겁니다.

1{ 2 "coverImage": "./coverImage.jpeg" 3}

overview.mdx, style.mdx, usage.mdx 세 개의 파일에서 coverImage를 직접 import 해서 사용하는 것이 아니라 세 개의 파일에서 메타 데이터 JSON 파일을 바라보게 하고 해당 데이터에 따라서 정보를 보여주게 합니다. 메타 데이터 JSON 파일을 Source of Truth (진실의 원천)으로 두는 것입니다.

이렇게 하면 무엇이 좋을까요?

  • 변경에 유리합니다.
  • 소통하기가 쉬워지고 생산성이 높아집니다.
  • 데이터가 일관되게 설명되어 더 쉽게 해석하고 관리할 수 있도록 합니다.
  • 데이터 정확성, 완전성 및 관련성이 올라가기 때문에 품질이 올라가고 전반적인 퀄리티가 좋아집니다.

하지만 모든 것에 장점이 있다면 단점도 있습니다.

  • 메타 데이터도 사람이 생성하는 데이터이기 때문에 관리를 해줄 사람이 필요합니다.
  • 항상 최신 데이터를 유지해야 합니다.
  • 어떤 데이터가 들어갈지 팀원들과 잘 상의해서 넣어야 합니다. (일치 비용)

단점들 대부분이 관리 비용입니다. 이 글에서는 관리 비용을 최대한 낮추기 위한 작업을 해볼 겁니다.

만약 메타 데이터 JSON 파일에 잘못된 값이 들어가면 어떻게 할까요? 디자인시스템은 수십 개의 컴포넌트로 이루어져 있습니다. 이 모든 컴포넌트의 메타 데이터를 수동으로 일일이 확인한다는 건 불가능합니다.

메타 데이터 JSON 파일이 수정될 때 AJV(JSON 스키마 Validator)를 이용하여 메타 데이터 검증하고 해당 검증하는 로직을 자동화하여서 머지가 되기 전에 자동으로 확인하는 것까지 해보겠습니다.

AJV로 커스텀 JSON 스키마 작성과 검증 스크립트 작성

AJVJSON schema validator입니다. JSON에 대한 데이터 형식을 선언적으로 적어두고 JSON 파일이 해당 데이터 형식에 부합하는지 검증할 수 있습니다.

AJV의 사용 예시를 간단히 살펴보겠습니다.

1const Ajv = require("ajv"); 2 3// JSON schema 선언하기 4const schema = { 5 type: "object", 6 properties: { 7 name: { type: "string" }, 8 age: { type: "number" }, 9 }, 10 required: ["name", "age"], 11}; 12 13// schema를 사용하여 유효성 validator 인스턴스를 만듭니다. 14const ajv = new Ajv(); 15const validate = ajv.compile(schema); 16 17// schema에 대해 일부 JSON 데이터의 유효성을 검사합니다. 18const data = { 19 name: "Alice", 20 age: 30, 21}; 22const isValid = validate(data); 23 24if (isValid) { 25 console.log("Data is valid!"); 26} else { 27 console.log("Data is invalid:"); 28 console.log(validate.errors); 29}

이게 끝입니다. 그럼 우리가 해야 할 일은 다음과 같습니다.

  • JSON schema 선언하기
  • 유효성을 검사하는 스크립트 만들기

JSON schema 선언하기

저는 schema를 선언하기 위한 파일을 따로 작성했습니다. 파일이 조금 길지만 전부 읽지 않아도 됩니다. 아래 코드에서 중요한 것은 여러 가지 패턴을 전부 검증할 수 있다는 것입니다. 정규표현식을 통해서 특정 문자를 검사할 수도, 필수적인지 옵션으로 들어갈 수 있는지도 검증할 수 있습니다. 그리고 정해진 프로퍼티만 들어갈 수 있는지 혹은 추가적인 프로퍼티가 들어가도 되는지도 정할 수 있습니다.

공식문서를 참고하세요

1const statusSchema = { 2 type: "string", 3 pattern: "^(todo|in-progress|done)$", 4}; 5 6const stringSchema = { 7 type: "string", 8}; 9 10const storybookSchema = { 11 type: "object", 12 properties: { 13 path: stringSchema, 14 height: { type: ["string"] }, 15 }, 16 required: ["path"], 17 additionalProperties: false, 18}; 19 20const mdxSchema = { 21 type: "string", 22 pattern: "^.*.mdx$", 23}; 24 25const jsonSchema = { 26 type: "string", 27 pattern: "^.*.json$", 28}; 29 30const pngSchema = { 31 type: "string", 32 pattern: "^.*.png$", 33}; 34 35const platformSchema = { 36 type: "object", 37 properties: { 38 ios: { 39 type: "object", 40 properties: { 41 status: statusSchema, 42 alias: { type: "string" }, 43 path: { type: "string" }, 44 }, 45 required: ["status", "alias", "path"], 46 additionalProperties: false, 47 }, 48 android: { 49 type: "object", 50 properties: { 51 status: statusSchema, 52 path: { type: "string" }, 53 }, 54 required: ["status", "path"], 55 additionalProperties: false, 56 }, 57 react: { 58 type: "object", 59 properties: { 60 status: statusSchema, 61 path: { type: "string" }, 62 }, 63 required: ["status", "path"], 64 additionalProperties: false, 65 }, 66 figma: { 67 type: "object", 68 properties: { 69 status: statusSchema, 70 path: { type: "string" }, 71 }, 72 required: ["status", "path"], 73 additionalProperties: false, 74 }, 75 docs: { 76 type: "object", 77 properties: { 78 overview: { 79 type: "object", 80 properties: { 81 status: statusSchema, 82 storybook: storybookSchema, 83 mdx: mdxSchema, 84 }, 85 additionalProperties: false, 86 }, 87 usage: { 88 type: "object", 89 properties: { 90 status: statusSchema, 91 mdx: mdxSchema, 92 }, 93 additionalProperties: false, 94 }, 95 style: { 96 type: "object", 97 properties: { 98 status: statusSchema, 99 mdx: mdxSchema, 100 additionalProperties: false, 101 }, 102 }, 103 }, 104 required: ["usage", "style", "overview"], 105 additionalProperties: true, 106 }, 107 }, 108 required: ["ios", "android", "react", "figma", "docs"], 109 additionalProperties: false, 110}; 111 112export const componentMetaSchema = { 113 type: "object", 114 properties: { 115 name: stringSchema, 116 description: stringSchema, 117 thumbnail: pngSchema, 118 primitive: jsonSchema, 119 group: stringSchema, 120 platform: platformSchema, 121 }, 122 required: ["name", "description", "thumbnail", "platform"], 123}; 124 125export const primitiveMetaSchema = { 126 type: "object", 127 properties: { 128 name: stringSchema, 129 description: stringSchema, 130 thumbnail: pngSchema, 131 primitivie: mdxSchema, 132 }, 133 required: ["name", "description", "thumbnail", "primitive"], 134};

유효성을 검사하는 스크립트 만들기

그리고 해당 Schema를 통해서 검증 해줄 스크립트가 필요합니다. 제 요구사항은 다음과 같았습니다.

1📦content 2 ┣ 📂component 3 ┃ ┣ 📂action-sheet 4 ┃ ┃ ┣ 📜component-meta.json 5 ┃ ┣ 📂actionable-callout 6 ┃ ┃ ┣ 📜component-meta.json 7 ┃ ┣ 📂alert-dialog 8 ┃ ┃ ┣ 📜component-meta.json 9 ┃ ┃ ... 10 ┣ 📂primitive 11 ┃ ┣ 📂avatar 12 ┃ ┃ ┣ 📜primitive-meta.json 13 ┃ ┣ 📂button 14 ┃ ┃ ┣ 📜primitive-meta.json 15 ┃ ┣ 📂checkbox 16 ┃ ┃ ┣ 📜primitive-meta.json 17 ┃ ┣ ...
  • 루트에서 ./content/component 폴더에 있는 모든 폴더 안의 component-meta.json 파일만 읽어서 component schema 검증
  • 루트에서 ./content/primitive 폴더에 있는 모든 폴더 안의 primitive-meta.json 파일만 읽어서 primitive schema 검증
  • 검증에 실패하면 실패한 파일명과 어떤 이유에서 실패했는지 출력하기

코드로 표현하면 다음과 같습니다.

1import Ajv from "ajv"; 2import { prettify } from "awesome-ajv-errors"; 3import fs from "node:fs/promises"; 4import path from "node:path"; 5 6import { 7 componentMetaSchema, 8 primitiveMetaSchema, 9} from "./meta-data-schemas.mjs"; 10 11const ajv = new Ajv(); 12 13// ---------실행 부분 시작--------- // 14 15console.log("Validating meta.json files..."); 16 17validateJsonInDir({ 18 dir: path.resolve("./content/component"), 19 validate: ajv.compile(componentMetaSchema), 20 type: "component", 21}); 22validateJsonInDir({ 23 dir: path.resolve("./content/primitive"), 24 validate: ajv.compile(primitiveMetaSchema), 25 type: "primitive", 26}); 27 28console.log("Finished validating meta.json files"); 29 30// ---------구현 부분 시작--------- // 31 32async function validateJsonInDir({ dir, validate, type }) { 33 try { 34 // NOTE: dir에 해당하는 폴더의 파일과 폴더들을 읽습니다. 35 const filesOrFolders = await fs.readdir(dir); 36 37 for (const fileOrFolder of filesOrFolders) { 38 const filePath = path.join(dir, fileOrFolder); 39 40 const stats = await fs.stat(filePath); 41 42 // NOTE: 폴더만 찾습니다. 43 if (!stats.isDirectory()) { 44 continue; 45 } 46 47 const subfiles = await fs.readdir(filePath); 48 49 // NOTE: 각 컴포넌트에 대한 폴더에서 json 파일만 찾습니다. 50 for (const subfile of subfiles) { 51 if (path.extname(subfile) !== ".json") { 52 continue; 53 } 54 55 const data = await fs.readFile(path.join(filePath, subfile), "utf8"); 56 57 const json = JSON.parse(data); 58 const isValid = validate(json); 59 const fileName = `${json.name.replaceAll(" ", "-").toLowerCase()}.json`; 60 61 if (!isValid) { 62 // NOTE: 검증에 통과하지 못했을 때 실행됩니다. 63 console.log(`${type}/${fileName} is invalid`); 64 console.error(prettify(validate, { data: json })); 65 66 process.exit(1); 67 } else { 68 // NOTE: 통과했을 땐 굳이 출력하지 않아도 됩니다. 69 // console.log(`${type}/${fileName} is valid`); 70 } 71 } 72 } 73 } catch (err) { 74 console.error(err); 75 } 76}

이렇게 스크립트를 작성했으면 package.json에 스크립트에 명령어를 작성해줍니다.

1{ 2 "scripts": { 3 "validate:meta-data": "node scripts/validate-meta-data.mjs" 4 } 5}

한 번 실행해 볼까요?

CLI에서 실행해보기
CLI에서 실행해보기

잘 되는 것 같네요!

만약 데이터 형식이 맞지 않으면 어떻게 될까요? 저는 컴포넌트의 메타 데이터 JSON 파일에 descriptionstring 형식으로 들어가도록 했습니다.

1export const componentMetaSchema = { 2 type: "object", 3 properties: { 4 name: stringSchema, 5 description: stringSchema, 6 thumbnail: pngSchema, 7 primitive: jsonSchema, 8 group: stringSchema, 9 platform: platformSchema, 10 }, 11 required: ["name", "description", "thumbnail", "platform"], 12};

그리고 componentaction-sheet 컴포넌트에서 description을 잠시 빼보겠습니다. 그러곤 다시 스크립트를 실행한다면?

1{ 2 "name": "Action Sheet", 3 "description": "OS 시스템 액션시트를 대체하는 커스텀 액션시트입니다.", 4 ... 5}
스크립트 다시 실행
스크립트 다시 실행
  • 어느 컴포넌트에서 실패했는지
  • 어느 프로퍼티가 잘못됐는지

잘 확인하는 것 같네요!

오류 메시지를 예쁘게 보이는 것은 awesome-ajv-errors 라이브러리를 참고해 주세요.

자동 검증을 위해 AJV를 GitHub Actions와 통합

요기까지 오셨다면 AJV를 통해서 원하는 검증 스크립트를 만드는 것까지 된 것 같습니다. 근데 일일이 메타 데이터가 변경될 때마다 수동으로 스크립트를 실행시켜 확인할 수 있지만 사람들은 너무 바쁩니다. PR을 올리고 해당 Pull Request가 메타 데이터를 바꿨다면 자동으로 스크립트를 실행하는 Action이 있으면 좋을 것 같습니다.

Github Action은 위와 같은 요구사항을 만족하기에 완벽한 도구입니다. 원하는 폴더에서 변경사항이 있을 때만 실행하기, 원하는 브랜치에서, 원하는 동작에서만(on push, on pull request, ...etc) 등등 원하는 동작들을 다 할 수 있습니다.

바로 Action을 작성해 봅시다.

1on: 2 push: # push 될 때마다 3 branches: # 모든 브랜치를 검사 4 - '**' 5 paths: # 원하는 경로의 json 파일들이 변경됐을 때만 action이 실행됨 6 - 'docs/content/component/**/component-meta.json' 7 - 'docs/content/primitive/**/primitive-meta.json' 8 9name: Validate Seed Docs meta data files 10 11jobs: 12 build: 13 name: Validate meta data files 14 runs-on: ubuntu-latest 15 16 steps: 17 - uses: actions/checkout@v3 18 19 - uses: actions/setup-node@v3 20 with: 21 node-version: 18.12.1 22 cache: yarn 23 24 - name: Install Dependencies 25 run: yarn install --immutable 26 27 # 검증 스크립트 실행 28 - name: Validate meta data files 29 working-directory: ./docs 30 run: | 31 yarn validate:meta-data 32 33 - name: Report success 34 run: echo "Script ran successfully!"

그리고 PR을 만들고 push를 해봅시다.

PR에서 성공했을 때의 모습
PR에서 성공했을 때의 모습
액션 탭에서 성공했을 때의 모습
액션 탭에서 성공했을 때의 모습

잘 되는 것 같네요!

만약 실패한다면 어떻게 보일까요?

PR에서 실패했을 때의 모습
PR에서 실패했을 때의 모습
액션 탭에서 실패했을 때의 모습
액션 탭에서 실패했을 때의 모습

아까 작성한 스크립트에서 실패하면 프로세스를 나가는 코드가 있었습니다. 해당 코드가 없으면 스크립트가 실패해도 Action이 성공한 것처럼 보이기 때문에 꼭 넣어주어야 합니다.

1if (!isValid) { 2 // NOTE: 검증에 통과하지 못했을 때 실행됩니다. 3 console.log(`${type}/${fileName} is invalid`); 4 console.error(prettify(validate, { data: json })); 5 process.exit(1); // 실패한다면 Github Action의 프로세스를 끝내야 합니다. 6} else { 7 // console.log(`${type}/${fileName} is valid`); 8}

결론

AJV와 GitHub Actions를 함께 사용하면 프로젝트의 JSON 파일 전체에서 일관된 메타 정보를 보장하는 데 도움이 될 수 있습니다. 스키마에 대해 데이터의 유효성을 검사하면 조기에 오류를 발견하고 후속 문제를 방지할 수 있습니다. GitHub Actions로 자동화된 유효성 검사를 설정하면 워크플로우를 더욱 간소화하고 사람이 실수할 가능성을 줄일 수 있습니다. 이 포스팅이 이러한 도구를 함께 사용하여 데이터의 품질과 일관성을 개선하는 방법에 도움이 되었다면 좋겠습니다.

관련 포스트가 4개 있어요.

테크밋업 발표에서 하지 못한 Icona의 남은 과제들에 대한 이야기에 대해서 적어보았다.

개발
icon
about-icona-remaining-tasks cover image
2024/10/26
NEW POST

아이콘 피그마 배포 시스템, Icona의 남은 과제들 (부제. 테크밋업 발표에서 하지 못한 얘기들)

ESM, CJS, TS를 모두 지원하는 라이브러리를 만들어보고, CLI를 간단하게 만들어보겠습니다.

개발
npm
deploy-simple-npm-library cover image
2024/03/19 (updated)

간단한 NPM 라이브러리 배포해보기 2탄 (CLI 간단한 라이브러리 만들기 & 모든 환경 지원, ESM, CJS, TS)

Figma Plugin을 이용하여 에셋을 전시하는 방법에 대해 알아봅니다.

개발
에셋타운
asset-town-figma-plugin cover image
2024/03/17

에셋 관리 시스템을 만들어보자 (에셋타운 4편 - Figma Plugin을 이용한 에셋 전시)

에셋타운에서 Cloudflare Workers와 R2를 이용하여 웹훅을 처리하고 CDN 캐싱을 제공하는 방법에 대해 설명합니다.

개발
에셋타운
asset-town-provide-cdn-using-webhook-and-cloudflare cover image
2024/03/01

에셋 관리 시스템을 만들어보자 (에셋타운 3편 - Cloudflare Workers, R2를 이용한 웹훅 처리와 CDN 캐싱)

profile

정현수.

Currently Managed

Currently not managed

© 2024. junghyeonsu all rights reserved.