코드 퍼스트로 스키마 변경이 일어났을 때 확인하는 과정을 자동화해보자

Sanghoon Han
creatrip
Published in
10 min readJun 19, 2024

시작하며

안녕하세요 크리에이트립(Creatrip)에서 프론트엔드 개발을 하는 한상훈입니다.

크리에이트립에서는 GraphQL 프로토콜을 사용하여 서버와 통신하며, 스키마를 정의하는 데 있어 코드 퍼스트 전략을 활용하고 있어요.

코드 퍼스트란 먼저 서버 측에서 객체와 그 객체의 데이터 구조를 정의한 후, 이러한 정의를 바탕으로 GraphQL 스키마가 자동으로 생성되는 방식이에요.

코드 퍼스트 전략은 서버 개발 생산성을 높이는 데 도움을 주지만,
변경된 스키마로 인해, 프론트엔드 개발 환경 빌드가 종종 실패하는 문제가 있었어요.
서버 개발로 인해 스키마 변경이 발생하기 전 프론트엔드 빌드가 실패하는지 확인하는 과정을 자동화하여 해결한 경험을 공유해 드리려고 해요.

문제점

코드 퍼스트로 인해 스키마 변경은 서버 개발자 그리고 프론트엔드 개발자 모두에게 적지 않은 피로감을 주고 있었어요.

서버 개발자의 경우 변경 사항을 개발 환경에 반영하기 전에 프론트엔드 개발자에게 변경된 스키마 파일로 인해 빌드가 실패하는지 확인이 필요했어요.

프론트엔드 개발자의 경우 갑작스럽게 변경된 스키마로 인해 빌드가 실패하는 경우 서버 개발 담당자를 찾아 변경된 이유를 확인 후에 대응해야 했어요.

위 불편함은 팀 내에서 모두 공유되고 있었고 개선이 필요하다 느꼈어요.

위 과정을 자동화할 수 없을까 고민하였으며 불현듯 아이디어가 떠올랐어요.

💡 만약 스키마 변경이 생긴다면 그 변경을 일으키는 PR이 반영되기 전 변경된 스키마가 프론트엔드 빌드가 실패하는지 확인하는 과정을 github action을 통해 자동화하면 되겠구나!

자동화 액션 구현

먼저 떠오른 생각을 먼저 도표로 정리해 보았어요.

도표로 정리하니 어떻게 액션을 구현해야 하는지 알 수 있었어요.
이제 단계별로 구현한 내용을 공유해 드릴게요!

1. 변경된 스키마 확인

서버 개발 PR에서 변경된 스키마 파일이 없을 경우엔 해당 액션을 건너뛸 거에요 github action의 paths 기능을 활용하면 쉽게 구현할 수 있어요.

on:
pull_request:
paths: 변경된 스키마 파일들의 경로

paths에 어떤 경로의 파일이 변경되었을 때 action을 작동시킬지 정의할 수 있어요.

2. 스키마 병합

실제 프론트엔드 빌드를 확인하기 위한 스키마를 만들기 위해선 변경된 스키마뿐만 아니라 서버 리포지토리에있는 모든 스키마를 병합하여 하나의 스키마로 만들어야 해요.

스키마를 병합하는 데 graphql tools의 mergeSchema를 사용하면 여러 경로에 있는 스키마를 하나의 스키마로 쉽게 병합할 수 있어요.

graphql은 역사가 깊고 지원하는 라이브러리들이 많아 대부분의 기능은 이미 구현되어 있을 가능성이 커요!

하나의 스키마로 병합하는 것은 yml 파일 대신 script 파일을 작성하여 해당 스크립트 파일을 액션에서 돌리는 방식으로 진행할 거예요.

먼저 스키마의 경로를 활용해 스키마들을 불러올 거예요.

import { readFileSync, writeFileSync } from 'fs';

const schemaPaths = [/**스키마 경로들*/]
const schemaSDLs = schemaPaths.map((schemaPath) => readFileSync(schemaPath, 'utf8'));

그다음 merge schema를 사용하기 위해 불러온 schemaSDL를 schema 객체 형태로 변환할 거예요.

graphql 라이브러리에 내장된 buildSchema를 활용하면 쉽게 object 객체 형태로 변환할 수 있어요.

import { buildSchema } from 'graphql';

const schemas = schemaSDLs.map((sdl) => buildSchema(sdl));

마지막으로 @graphql-tools/schema 라이브러리에 내장된 mergeSchemas를 활용해 스키마를 병합할 거예요.

import { mergeSchemas } from '@graphql-tools/schema';

const mergedSchema = mergeSchemas({ schemas });

스키마 병합하는 스크립트 최종 코드에요.

import core from '@actions/core'
import { buildSchema } from 'graphql';
import { readFileSync, writeFileSync } from 'fs';
import { mergeSchemas } from '@graphql-tools/schema';
import { printSchemaWithDirectives } from '@graphql-tools/utils';

async function main() {
//스키마 경로를 받아옴
const schemaPaths: string[] = JSON.parse(process.env.SCHEMA_PATHS);
//스키마 파일 불러옴
const schemaSDLs = schemaPaths.map((schemaPath) => readFileSync(schemaPath, 'utf8'));
//스키마 object 형식으로 변환
const schemas = schemaSDLs.map((sdl) => buildSchema(sdl));
//mergeSchemas활용
const mergedSchema = mergeSchemas({ schemas });
//최종결과 schema에 저장
core.setOutput('schema', printSchemaWithDirectives(mergedSchema));
}

main();
on:
pull_request:
branches:
- develop
paths:
- 'apps/**/schema.gql'

jobs:
merge-schemas:
runs-on: ubuntu-latest
steps:
- name: 백엔드 리포지토리 체크아웃
uses: actions/checkout@v3

- name: 모든 서버 GraphQL Schema 병합
id: mergeSchemas
run: pnpm ts-node scripts/merge-schema/index.ts
env:
SCHEMA_PATHS: '["스키마 경로a", "스키마 경로b", "스키마 경로c"]'
outputs:
schema: ${{ steps.mergeSchemas.outputs.schema }}

생성된 스키마는 merge-schemas의 outputs 따로 저장해둘 거예요.

3. 병합된 스키마를 활용해 프론트엔드 코드젠 및 타입 확인 액션 제작

이제 병합된 스키마를 활용해 프론트엔드 코드젠 후 타입 체크하는 액션을 제작만 하면 되요.
현재 백엔드 리포지토리와 프론트엔드 리포지토리가 분리되어 있어 프론트엔드 리포지토리에 접근해야 해요.

actions/checkout을 활용하면 쉽게 접근할 수 있어요.

steps:
- name: 프론트엔드 리포지토리 체크아웃
uses: actions/checkout@v3
with:
repository: 프론트엔드 리포지토리 명
ref: 브랜치 이름
#시크릿 토큰
token: 토큰값

이제 이전 액션에서 생성한 스키마를 불러올 거예요.
이전 액션에서 불러온 merge-schemas.outputs에 schema가 저장되어 있기에
해당 스키마 파일을 불러와요.

- name: 저장된 서버 스키마로 local schema 생성
run: |
cat <<EOT > 스키마경로
${{ needs.merge-schemas.outputs.schema }}
EOT

스키마 파일들의 줄 바꿈을 유지한 상태로 파일을 저장해야 하기에 linux의 heredoc 형태를 활용할 거예요.

heredoc이란 here document의 줄임말로 텍스트 블록을 구분자(delimiter)로 감싸서, 줄 바꿈과 공백을 포함한 텍스트를 쉽게 작성하고 처리할 수 있어요.

이제 불러온 스키마를 활용해 코드젠 후 타입 체크를 실행할 거예요.

- name: 프론트 로컬 스키마 코드젠
run: |
pnpm codegen

- name: 타입 체크
run: |
pnpm run type-check

최종 액션 코드에요

check-frontend-type-broken:
runs-on: ubuntu-latest
needs: merge-schemas
steps:
- name: 프론트엔드 리포지토리 체크아웃
uses: actions/checkout@v3
with:
repository: 프론트엔드 리포지토리 이름
ref: 브랜치 이름


#중략(프론트엔드 코드 의존성 설치 및 초기 셋팅)

- name: 저장된 서버 스키마로 local schema 생성
run: |
cat <<EOT >> schema.graphql
${{ needs.merge-schemas.outputs.schema }}
EOT

- name: 프론트 로컬 스키마 코드젠
run: |
pnpm codegen

- name: 타입 체크
run: |
pnpm run type-check

이렇게 스키마 변경을 일으키는 서버 PR이 반영되기 전 변경된 스키마가 프론트엔드 빌드가 실패하는지 확인하는 액션이 개발되었어요.

마무리하며

서버 개발 PR에서 스키마 변경이 일어나면, 프론트엔드 빌드가 실패하는지 확인하는 과정을 자동화해 보았는데요.

이에 따라 더 이상 서버 개발자분들은 프론트엔드 개발자분들에게 스키마 변경으로 인해 프론트엔드 빌드가 실패하는지 일일이 확인할 필요가 없게 되었어요.

프론트엔드 개발자분들은 갑작스러운 스키마 변경으로 인해 빌드가 되지 않아 개발이 지연되는 상황이 발생하지 않게 되었어요.

그리고 스키마 대응도 어떤 서버 개발 PR에서 스키마가 깨지는지 쉽게 확인이 가능하여 대응에도 어려움이 없어졌어요.

이번 개발은 제 첫 서버 리포지토리에 반영된 개발이었으며, 개선이 필요한 곳에 어디서든 개발을 통해 도움을 줄 수 있다는 것을 배웠어요!

동료 개발자분들에게 도움을 준 것 역시 정말 뿌듯했어요.

짧은 글 읽어주셔서 감사합니다 :)

References

--

--