BigQuery에 이벤트가 안 들어와서 원인을 매번 손으로 짚어보던 일이 있었다. 클라이언트 선언과 DE팀 스키마가 어긋나도 아무도 알려주지 않기 때문이다. 이걸 PR 단계에서 자동으로 잡도록 만든 과정을 정리했다.
기존 문제점
이벤트 하나를 BigQuery까지 보내려면 클라이언트와 데이터 엔지니어링 양쪽이 각각 선언을 해야 한다. 문제는 이 둘이 어긋나도 아무 에러가 나지 않는다는 점이다.
상황
우리 서비스는 Firebase SDK로 이벤트를 로깅하고, 이 이벤트가 BigQuery 테이블까지 흘러간다. 코드로 살펴보면 다음과 같다.
firebaseTagging('sample_page_view', {
id: props.placeId,
type: props.placeType,
status: props.isAvailable ? 'TRUE' : 'FALSE',
traffic_source: trafficSource,
})
장소 상세페이지에 진입했을 때 적재하는 sample_page_view 이벤트이며, 그 장소의 아이디, 타입, 상태도 함께 남긴다.
우리 클라이언트 개발자(앱/웹)는 단순히 ‘로깅’만 하면 끝이지만 그 이전과 이후의 흐름은 생각보다 복잡하다.
이벤트 적재의 흐름
이벤트 설계 담당자 입장에서의 이벤트 적재 흐름은 다음과 같다.
- 기획서/분석 니즈 기반으로 필요한 이벤트를 파악한다.
- 앱 플로우를 따라가며 기존 이벤트 현황을 확인한다.
- 추가 정의 필요한 이벤트를 노션에 작성한다.
- 관리자 컨펌을 받는다.
- 협업 채널에 스레드를 생성해, 기획 담당 파트에 요청한다.
- 개발팀과 논의하며 명세를 확정한다.
- Firebase 이벤트 명세 스프레드시트에 기입한다.
- 배포 2일 전까지 Data Engineering 팀에 작업을 요청한다.
- 배포 후 BigQuery 정상 적재를 확인한다.
여기서 이벤트의 ‘로깅 구현’과 ‘적재’를 구분할 필요가 있다.
- 이벤트 로깅(Firebase) 구현: 클라이언트 개발 팀 (6번)
- Firebase → BigQuery raw(events_*) 적재: 자동 (Firebase export)
- raw → 오리지널 마트 적재: DE팀이 구축한 파이프라인 (8번 이후)
즉 이벤트를 BigQuery에 적재하기 위해서는 두 번의 작업이 필요한 것이다. 클라이언트 팀은 이벤트를 interface에 정의해서 각 로깅이 필요한 곳에서 로깅을 해야 한다. 그리고 DE(데이터 엔지니어링)팀은 클라이언트 팀이 로깅하여 적재된 raw 이벤트를 오리지널 마트에 적재하기 위한 스키마 JSON을 선언해두어야 한다.
DE팀이 작성한 스키마 JSON의 내용은 다음과 같다.
// data-catalog/rules/my-project.json
{
// ..
{
"event_name": "service_event",
"event_label": "sample_page_view",
"event_params_to_columns": [],
"legacy": []
},
}
// data-catalog/schemas/my-project/services/service.json
{
"event_params": {
"type": "STRING",
"id": "STRING",
"traffic_source": "STRING",
"status": "STRING",
// ..
}
}
여기서 문제점이 하나 있다. raw가 중간에서 아무거나 받아주기 때문에, 양쪽 선언이 어긋나도 에러가 발생하지 않는다. 마트에 컬럼이 비거나 이벤트가 누락될 뿐이다.
지금까지는 BigQuery에서 이벤트가 비면 스키마 JSON 쪽인지 클라이언트 로깅 쪽인지 매번 수동으로 짚어봐야 했다. 이걸 자동화하기로 했다.
이벤트 검증 자동화 흐름
- main 브랜치에 PR을 올린다.
- 해당 레포지토리의 이벤트 명세가 정의된 인터페이스 파일을 조회한다.
- yaml 추출 패키지를 통해 ts → yaml을 변환한다.
- 생성된 yaml과 DE팀의 카탈로그 레포지토리에 정의된 이벤트 스키마를 비교/검증한다.
- 검증 결과 로그를 생성한다.
- 명세 불일치 발생 시 Slack 채널로 알림을 전송한다.
GitHub Actions
- main 브랜치에 푸시하거나, PR을 올렸을 때 실행한다. (흐름 1단계)
on:
push:
branches: [main]
paths:
- 'src/modules/firebaseEvent/types/FirebaseEvent.types.ts'
- '.github/workflows/event-schema.yml'
pull_request:
paths:
- 'src/modules/firebaseEvent/types/FirebaseEvent.types.ts'
- '.github/workflows/event-schema.yml'
workflow_dispatch:
- 현재 레포지토리로 체크아웃하고, Node.js를 설치해 세팅을 준비한다. (흐름 2단계)
jobs:
generate-and-validate:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
- generate-event-schema Composite Action을 사용해 ts → yaml로 변환한다. (흐름 3단계)
- name: Generate event schema
uses: my-org/event-schema-tools/.github/actions/generate-event-schema@main
with:
file: src/modules/firebaseEvent/types/FirebaseEvent.types.ts
interface: IFFirebaseEventMap
입력으로 검증이 필요한 interface가 담긴 ts 파일과 interface명을 함께 전달한다.
- data-catalog 레포지토리의 client-event-validate action을 이용해 3번에서 생성한 yaml 파일과 일치하는지를 검증한다. (흐름 4~6단계)
- name: Validate against data catalog
uses: my-org/data-catalog/.github/actions/client-event-validate@main
with:
domain: my-service
project: my-project
client: front-end
client-root: ${{ github.workspace }}/build/event_yamls
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
# 알림 채널 분기 — main push만 운영 채널, 그 외는 테스트 채널.
env: ${{ github.event_name == 'push' && github.ref_name == 'main' && 'prod' || 'test' }}
타 레포지토리 접근 방식
현재 레포지토리의 워크플로우에서 같은 조직에 있는 타 레포지토리에 접근하기 위해서는 자격증명이 필요하다. 이 자격증명으로는 내 개인 PAT(Personal Access Token)을 레포 secret에 넣어서 시도해볼 수 있다. 내가 그 레포지토리에 접근 권한이 있기 때문에 가능한 경우이다. 하지만 만약 내가 퇴사를 하거나, 권한이 회수될 경우 이 자동화는 끊기게 된다. 이러한 자동화는 개인에 종속되면 안 된다고 판단해, 워크플로우가 다른 레포지토리에 접근할 다른 방법을 찾아봤다.
- 프론트팀 공용 조직 토큰 활용: 프론트 레포들이 공용으로 쓰는 토큰에 repo 권한이 있다면 그대로 활용이 가능하다.
- Composite Action 방식: 대상 레포지토리에 Composite Action을 두고 조직 내 접근을 허용하면,
uses:만으로 접근이 가능하다. 즉, GitHub Actions를 통한 접근만 열어둔 것이다. - GitHub App: 봇 신원을 만들어서, 워크플로우가 실행될 때마다 그 신원으로 임시 토큰을 발급받아 쓰는 방식. 개인 종속은 확실히 풀리지만 App 등록과 권한 관리를 따로 해야 한다.
- 패키지 발행: GitHub Packages에 발행하면 체크아웃 없이 npm install로 해결할 수 있다. 대신, private 패키지를 설치하기 위해서는 사내 레포에 접근할 수 있는 토큰이 또 필요하다.
이 중에서 기존에 사내에서 채택해온 방식이자 다른 팀이 채택하기에도 용이하다고 판단해 2번 Composite Action 방식을 선택했다. Composite Action 방식은 간단하다. 해당 레포지토리에 Actions를 하나 추가하고 설정에서 권한 하나만 열어주면 된다. uses:로 부르면 그 레포지토리 내용이 github.action_path에 딸려오기 때문에, 따로 체크아웃하지 않아도 카탈로그 파일을 읽을 수 있다.
권한 접근이 필요한 레포지토리에서는 아래와 같이 composite action을 만들어주면 된다.
# .github/actions/generate-event-schema/action.yml
name: 'Generate Event Schema YAML'
inputs:
source-dir:
required: true
output-path:
default: 'events.yaml'
outputs:
yaml-path:
value: ${{ steps.gen.outputs.path }}
runs:
using: 'composite'
steps:
- run: npm ci --omit=dev
shell: bash
working-directory: ${{ github.action_path }}
- id: gen
run: node ${{ github.action_path }}/cli.js --src "${{ inputs.source-dir }}" --out "${{ inputs.output-path }}"
shell: bash
그리고 설정이 하나 필요하다: Settings → Actions → General → Access → "Accessible from repositories in the my-org organization"
호출하는 쪽에서는 아래와 같이 위 레포지토리 composite action에 접근할 수 있다.
- uses: my-org/event-schema-tools/.github/actions/generate-event-schema@main
with:
source-dir: ./src/events
다만 @main으로 걸어두면 상대 레포가 바뀌는 순간 우리 CI가 같이 흔들린다. 태그를 끊어서 쓰는 게 안전하다.
또 하나의 문제점
또 하나의 문제가 있다. 인터페이스에 정의만 해두고 실제로 그 이벤트를 로깅하지 않아도 위 플로우는 그대로 통과한다는 것이다.
이건 간단히 해결됐다. 위 GitHub Actions를 시작하기 전에 코드베이스를 읽어, 선언만 하고 쓰지 않는 이벤트를 체크하면 됐다. 이는 타 레포의 yaml파일 등에 의존이 없기 때문에 현재 레포지토리에 작성을 했지만, 추후 다른 팀도 위 방법을 채택을 하며 확장하게 된다면 이 검증 코드 또한 타 레포에서 접근할 수 있도록 열어두는 것도 좋은 방법이라는 생각이 들었다.
결과
상용 배포 이후 첫 슬랙 메시지가 전달이 되었다! 현재는 누락된 이벤트가 없어 효과가 크게 체감이 되지는 않지만, 앞으로 이번 자동화의 효과는 차차 두고 봐야할 듯하다.
맺으며
이번 작업을 진행하며 어려웠던 건 구현이 아니었다. 구현은 기존에 자주 해오던 GitHub Actions와 Slack API를 이용한 작업이었기에 크게 어렵지 않았다. 그런데 소통이 가장 큰 어려움이자 병목이었다. 데이터 엔지니어링 팀에 이벤트 적재 관련해서 논의를 해야 하고, typescript → yaml 라이브러리를 만든 타 파트 팀원과 소통을 해야 했다.
기존에는 내가 작업을 맡게 된 팀원들과 소통을 했다면, 이번에는 내가 좀 더 주도적으로 ‘이건 누구에게 묻는 것이 가장 적절할까?’에 대해서 고민하고 직접 묻고, 슬랙 히스토리와 노션 문서를 찾아가며 ‘기존에 이를 해결한 선례가 있을까?’를 찾으면서 주도성을 느낀 시간들이었다. 그리고 예상보다 빠르게 일을 마무리할 수 있어서 참 다행이었다.
나는 자동화를 참 좋아한다. 이번 건 비교적 단순했고 이미 갖춰진 게 많았지만 앞으로는 더 복잡하고, 더 많은 병목을 제거해주는 자동화를 만들어 기여하고 싶다.
이번 자동화는 단순히 우리 팀, 우리 프로젝트뿐 아니라 사내 전체적으로 도입해서 다른 팀원들의 불편함을 해결할 수 있으면 좋겠다.