안녕하세요. 오늘은 제가 최근에 기여한 PR #51735을 통해 Airflow UI의 성능과 구조를 개선한 경험을 공유해 보려고 합니다.
해당 PR은 Airflow UI의 i18n(국제화) 번역 파일 처리 방식을 변경하여 초기 JavaScript 번들 크기를 줄이고, 향후 확장성을 확보하는 데 중점을 두고 진행했습니다.
기존 아키텍처의 문제점
여러분이 Airflow UI에 접속하는 순간을 상상해 보세요. DAG 목록을 보고, 로그를 확인하는 등 다양한 작업을 할 것입니다. 그런데 만약 여러분이 한국어로 UI를 보고 있는데, 브라우저는 사실 폴란드어, 대만어, 독일어 등 모든 언어의 번역 파일을 다운로드하고 있었다면 어떨까요?
이것이 바로 제 PR 이전의 Airflow가 작동하던 방식이었습니다. 모든 번역 파일이 airflow-core/src/airflow/ui/src/i18n
경로에 저장되어 있었고, UI가 빌드될 때 모든 번역 파일을 단일 JavaScript 번들에 포함하여 빌드하는 구조였습니다.
이 방식은 아래와 같은 잠재적 문제를 내포하고 있습니다:
- 성능 저하: 사용자가 특정 언어만 사용함에도 불구하고, 브라우저는 모든 언어의 번역 데이터를 다운로드해야 하므로 초기 로딩 시간이 불필요하게 증가한다.
- 확장성 한계: 지원 언어가 추가될수록 선형적으로 번들 크기가 증가하여, 장기적으로 성능 유지보수에 부담으로 작용한다.
✓ 3627 modules transformed.
dist/.vite/manifest.json 0.45 kB │ gzip: 0.21 kB
dist/index.html 0.50 kB │ gzip: 0.32 kB
dist/assets/FailedLogs-BGPSeqnE.js 1.70 kB │ gzip: 0.90 kB
dist/assets/index-DQ137obh.js 4,359.84 kB │ gzip: 1,301.55 kB
물론, 번역 파일이 몇 개 없던 시절에는 이것이 치명적인 문제는 아니었습니다.
하지만 Airflow i18n 프로젝트에 전 세계 기여자들의 참여가 늘어나면서 상황은 달라졌습니다.
언어 파일이 계속 추가될수록 UI 초기 로딩 속도는 점점 느려질 것이 뻔했습니다.
“지금은 괜찮을지 몰라도, 미래에는 분명 문제가 된다.”
우리는 이 문제를 해결해야만 했습니다.
문제 해결을 위한 두 가지 핵심 전략
이 문제를 해결하기 위해 두 가지 접근이 필요했습니다.
- 번역 파일 분리: UI 소스 코드(
src
)에 묶여 있던 번역 파일들을 웹 서버가 직접 제공할 수 있는 정적 영역으로 옮긴다. - 동적 로딩 도입:
i18next-http-backend
라이브러리를 도입하여, 사용자의 언어 설정에 따라 필요한 번역 파일만 HTTP 요청을 통해 동적으로 로드하도록 구현한다.
그럼 먼저 src
에 있는 정적 번역 파일들을 이동시켜야 했습니다.
파일을 어디로 옮길지에 대한 논의는 활발했습니다. 백엔드에서 직접 서빙할지,
아니면 완전히 별도의 패키지로 분리할지 등 여러 의견이 오갔습니다.
저희는 "우선 문제를 해결하는 것"에 집중하기로 했고, 가장 명확하고 빠른 방법인 ui/public
으로 파일을 이전하기로 결정했습니다.
그리고 필요할 때만 번역 파일을 서버에서 불러오는 라이브러리인 i18next-http-backend
를 도입하여 아래와 같이 동적 로딩을 구현하기로 결정했습니다.
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
void i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
backend: {
loadPath: "/static/i18n/locales/{{lng}}/{{ns}}.json",
},
defaultNS: "common",
detection: {
caches: ["localStorage"],
order: ["localStorage", "navigator", "htmlTag"],
},
fallbackLng: defaultLanguage,
interpolation: {
escapeValue: false,
},
ns: namespaces,
react: {
useSuspense: false,
},
resources,
supportedLngs: supportedLanguages.map((lang) => lang.code),
});
구현 과정의 주요 이슈 및 해결
“그냥 파일만 옮기고 라이브러리 추가하면 되는 거 아닌가?” 라고 생각하신다면, 저도 처음에는 그랬습니다.
하지만 Airflow와 같은 대규모 프로젝트에서의 작업은 결코 간단하지 않았습니다.
구현 과정에서 두 가지 주요 기술적 이슈가 발생했습니다.
- pre-commit 훅과 충돌:
pre-commit
훅이 제가 건드리지도 않은openapi-gen
관련 파일을 계속해서 수정했습니다. A로 수정하면 B로 바뀌고, 다시 커밋하면 A로 돌아오는 무한 루프에 갇혀 커밋 자체가 불가능했습니다. (잘못된 것을 알지만 커밋에 추가할 수 밖에 없을 때의 비참함이란..)
- ESLint 파싱 오류: 새로 옮긴 JSON 번역 파일들에 대해 엄격한 TypeScriptRule이 적용되면서 수많은 파싱 오류를 쏟아냈습니다.
원인은 과도하게 넓게 설정된 ESLint의 검사 범위와 JSON 파일을 제대로 해석하지 못하는 기본 설정 때문이었습니다. 아이러니하게도, 기존 번역 파일들은 src
폴더 깊숙한 곳에 숨어 있어 이 검사를 교묘하게 피해가고 있었던 것이죠.
저는 두 단계에 걸쳐 이 문제를 해결했습니다.
-
TypeScript 규칙 범위 축소:
.ts
,.tsx
,.js
,.jsx
같은 실제 코드에만 규칙이 적용되도록 파일 범위를 명확히 지정했습니다.export const typescriptRules = /** @type {const} @satisfies {FlatConfig.Config} */ ({ files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], languageOptions: { parser: typescriptParser, parserOptions: {
-
JSON 파서 도입: 안정적이고 널리 사용되는
jsonc-eslint-parser
를 추가하여 ESLint가 JSON 파일을 올바르게 이해하고 검사할 수 있도록 만들었습니다.import jsoncParser from "jsonc-eslint-parser"; export const i18nRules = { files: ["public/i18n/locales/**/*.json"], languageOptions: { parser: jsoncParser, parserOptions: { extraFileExtensions: [".json"], }, },
물론 ESLint에도 플러그인으로 json을 파싱할 수 있는 플러그인이 생겼습니다. 하지만 jsonc-eslint-parsor는 오래되었고 안정적이며 여전히 많은 곳에서 사용되고 있습니다. 따라서 jsonc-eslint-parsor를 사용해주었습니다.
부가적으로, 번역 파일들과 함께 섞여 있던 번역 기여용 유틸리티 스크립트check_translations_completeness.py
를 개발 스크립트 전용 디렉토리인 dev/i18n
으로 옮겨 프로젝트 구조를 더욱 깔끔하게 정리했습니다.
이를 통해 기존에는 airflow-core/src/airflow/ui/src/i18n
여기 까지 들어가서 스크립트를 실행해야했는데
지금은 uv run dev/i18n/check_translations_completeness.py
이렇게 훨씬 더 간결하게 실행시킬 수 있습니다.
변경 결과 및 향후 개선 가능성
그래서 무엇이 얼마나 바뀌었을까요?
✓ 3589 modules transformed.
dist/index.html 0.50 kB │ gzip: 0.32 kB
dist/.vite/manifest.json 0.69 kB │ gzip: 0.25 kB
dist/assets/FailedLogs-BKux9o7Z.js 1.70 kB │ gzip: 0.90 kB
dist/assets/browser-ponyfill-BvFGHPkG.js 10.26 kB │ gzip: 3.50 kB
dist/assets/index-C6hLGF4h.js 4,268.63 kB │ gzip: 1,269.05 kB
정량적으로 메인 번들 사이즈가 약 32KB 줄었고, 변환되는 모듈 수도 약간 감소했습니다.
작업 당시보다 번역 파일이 많아진 지금은 조금 더 효과를 보고 있지 않을까 싶습니다.
그래도 데이터를 다루는 시선으로 보면 KB
단위의 변화가 작아 보이고 의아할 수 있습니다.
하지만 이 변화는 단지 몇 킬로바이트를 절약하는 것 만을 위한건 아닙니다.
중요한 것은 "구조의 개선"과 "미래를 위한 확장성"입니다.
이제 Airflow는 미래에 수십, 수백 개의 언어가 추가되더라도 UI 로딩 속도에 영향을 주지 않는 구조를 갖게 되었습니다. 또한, 번역 파일을 관리하고 제공하는 방식이 훨씬 유연해졌습니다.
예를 들면 JSON이 아니어도 되게 되었습니다. JSON이 아니라 yaml로 변경도 가능하고, 아니면 번역 파일 자체를 이제 ui/public
이 아니라 완전히 백엔드에서 서빙하게 할 수도 있고 완전히 ui에서 분리할 수 있습니다.
결론
PR #51735는 i18n 번역 파일을 동적으로 로딩하는 방식으로 Airflow UI의 구조를 개선한 내용입니다.
이를 통해 정량적인 번들 사이즈 감소 효과와 더불어, 향후 다국어 지원 확장에 유연하게 대응할 수 있는 기술적 기반을 마련했다는 점에서 의의가 있는 PR이었습니다.
그래서 저는 앞으로 다국어 작업에 대해서 어떤 개선이 일어날지 기대됩니다.
이렇게 Airflow 한국 사용자 모임 Airflow 기여 그룹의 숙제이자, 제 PR에 대한 짧은 소개였습니다.
앞으로 Airflow가 또 어떻게 발전해 나갈지 함께 지켜봐 주시면 좋겠습니다