엔지니어링
2025년 12월부터 2026년 2월까지 약 3개월간 래블업에서 프론트엔드 인턴으로 일하면서, Backend.AI WebUI의 50개 이상 컴포넌트에 '이야기(Story)'를 붙이는 작업을 했습니다. 구체적으로는 스토리를 하나씩 쓰기 전에 가이드라인과 자동화 파이프라인을 먼저 만들었고, 그 기반 위에서 AI와 함께 스토리를 작성했습니다. 이 작업을 한 줄로 요약하면, 기반을 먼저 세우는 것, 그리고 AI를 도구로 쓸 때 사람의 역할이 무엇인지를 이해하는 것이었다는 생각이 듭니다. 이 블로그 글에서는 이런 깨달음을 얻기까지의 과정에 해당하는 Storybook 인프라 구축부터 ui.backend.ai 배포까지를 공유합니다.
스토리북이란?
Storybook은 UI 컴포넌트를 앱과 독립된 환경에서 렌더링하고 문서화할 수 있는 도구입니다. 이름 그대로 컴포넌트의 "이야기(Story)"를 쓰는 도구인데, 여기서 이야기란 컴포넌트의 특정 상태를 시각적으로 보여주는 하나의 장면을 의미합니다. 예를 들어, 버튼 컴포넌트라면 '기본 상태', '비활성화 상태', '로딩 중 상태' 각각이 하나의 스토리가 됩니다.
코드를 읽지 않고도 "이 컴포넌트가 어떻게 생겼고, 어떤 옵션이 있는지" 브라우저에서 바로 확인할 수 있습니다. props를 실시간으로 바꿔가며 다양한 상태를 즉시 탐색할 수도 있습니다.
왜 필요했나?
Backend.AI WebUI에는 BAI라는 접두어가 붙은 자체 UI 컴포넌트가 50개 이상 있습니다. BAIButton, BAICard, BAIModal처럼 팀 안에서 공통으로 쓰는 컴포넌트들인데, 대부분은 사용법을 정리한 문서가 없었습니다. 새로 합류한 팀원이 "BAICard를 어떻게 쓰지?"라는 질문에 답을 얻으려면, 소스 코드를 직접 열어 읽거나 앱을 띄워서 해당 컴포넌트가 쓰이는 화면을 찾아다녀야 했습니다.
Storybook이 아예 없었던 것은 아닙니다. react/와 packages/backend.ai-ui/ 두 곳에 각각 인스턴스가 있었지만, react/ 쪽은 스토리가 1개뿐이었고 나머지도 50개가 넘는 컴포넌트에 비하면 문서화 비율이 낮았습니다.
그리고 스토리를 많이 만드는 것만으로는 충분하지 않았습니다. 50개가 넘는 컴포넌트에 대해 일관된 품질의 스토리를 유지하려면 자동화가 필수였습니다. 사람이 수작업으로 하나씩 만들면 작성 방식이 제각각이 되고, 컴포넌트가 수정될 때 스토리 업데이트를 빼먹기 쉽습니다. 가이드라인으로 기준을 잡고, AI 도구로 생성을 돕고, CI로 누락을 자동 검사하는 구조가 필요했던 이유입니다.
어떻게?
작업은 크게 4단계로 진행했습니다.
인프라 구축 → 자동화 도구 개발 → 컴포넌트 스토리 작성 → 배포 & 안정화
인프라 구축: Storybook이 "제대로" 동작하려면
Storybook을 설치하고 npm run storybook을 실행하면 바로 쓸 수 있을 것 같지만, Backend.AI WebUI처럼 복잡한 앱에서는 그렇지 않았습니다. 컴포넌트가 의존하는 글로벌 설정들이 Storybook 환경에는 없기 때문입니다.
가장 먼저 마주한 문제는 다국어 지원(i18n)이었습니다(#4946). Backend.AI WebUI는 21개 언어를 지원하는 다국어 앱이라 컴포넌트 내부에서 useTranslation() 훅으로 번역 텍스트를 가져옵니다. 그런데 Storybook에 i18n 설정이 없으니 "생성" 버튼이 general.Create라는 키 문자열로 그대로 출력되었습니다. i18next를 Storybook preview에 통합하고, 화면 위쪽 도구 모음에 언어 선택 드롭다운을 추가해서 21개 언어를 자유롭게 전환하며 확인할 수 있게 만들었습니다.
그 다음은 Ant Design ConfigProvider 통합이었습니다(#5020). Backend.AI WebUI는 Ant Design을 기반으로 하되, ConfigProvider로 상당한 커스터마이징을 하고 있었습니다. 이는 Backend.AI 만의 스타일을 입히고, 고객에 따라 원하는 테마를 적용할 수 있게 하는 설정인데, Storybook에 이게 빠져 있어서 컴포넌트가 실제 앱과 다른 모습으로 렌더링되었습니다. 이것도 decorator로 통합하고, light/dark 모드 전환도 함께 지원하도록 구성했습니다.
테마 전환도 필요했습니다(#5088). Backend.AI WebUI는 고객사별로 다른 테마를 적용할 수 있어서, 하나의 컴포넌트가 Ant Design 기본 테마와 Backend.AI 커스텀 테마에서 어떻게 보이는지를 둘 다 확인할 수 있어야 했습니다. 화면 위쪽 도구 모음에 테마를 골라 바꿀 수 있는 드롭다운을 추가했습니다. 아래 스크린샷에서 "Default (Ant Design)"이라고 표시된 부분이 이 드롭다운인데, 기본 테마와 커스텀 테마를 한 클릭으로 오갈 수 있어서 같은 컴포넌트가 테마별로 어떻게 달라지는지 바로 비교할 수 있습니다.

이 세 가지 설정을 하나의 Global Provider decorator로 묶어 모든 스토리에 자동 적용되게 만들었습니다. 어떤 컴포넌트의 스토리를 열어도 다국어·디자인 설정·테마가 동일하게 적용됩니다.
기본 Storybook은 흰 바탕에 Storybook 로고가 나오는데, 이 상태로는 사내 도구로서의 정체성이 약하다고 생각했습니다. 사이드바 상단에 Backend.AI UI 로고를 배치하고, 브랜드 컬러인 오렌지(#FF7A00)를 기본 색상으로 적용했습니다(#5040). 파비콘도 교체하고, 프로젝트 개요를 담은 Introduction 페이지도 만들었습니다.

인프라 쪽에서 가장 큰 규모의 작업은 Storybook v10 업그레이드와 인스턴스 통합이었습니다. 기존에는 모노레포 안에 Storybook 인스턴스가 2개 있었는데, react/ 쪽은 스토리가 딱 1개뿐이고 Storybook 관련 패키지만 13개가 깔려 있었습니다. v9에서 v10으로 메이저 업그레이드를 하면서 이 기회에 두 인스턴스를 하나로 통합하기로 했습니다.
그런데 v10 업그레이드는 예상보다 복잡했습니다. Storybook 10은 tsconfig.json의 moduleResolution이 bundler여야 한다는 요구사항이 있었고, eslint-plugin-storybook 10.x는 ESLint 9의 flat config만 지원했습니다. Storybook 업그레이드 하나가 TypeScript 설정 변경과 ESLint 마이그레이션을 연쇄적으로 끌고 온 셈입니다. 시행착오가 적지 않았지만, 이 작업 이후 모든 스토리가 backend.ai-ui 패키지 하나에 모이게 되어 관리가 훨씬 수월해졌습니다.
자동화 도구 개발: PR부터 배포까지 자동으로
인프라가 잡힌 후 바로 스토리를 작성하지 않았습니다. 50개 이상의 컴포넌트에 대한 스토리를 수작업으로 하나씩 만드는 것은 비효율적이고, 사람마다 작성 방식이 달라질 수밖에 없기 때문입니다. 먼저 자동화 도구와 기준을 만들고, 그 위에서 스토리를 작성하는 전략을 택했습니다. 자동화 도구 개발은 PR(Pull Request)을 적극적으로 활용하는 것에서 시작되었습니다. 즉, 코드를 수정한 뒤에 검토 요청하면, 팀원들이 코드를 검토하고 문제가 없으면, 반영(merge)하는 식으로 진행되었습니다.
최종적으로 기획한 파이프라인은 다음과 같습니다.

1단계: PR 생성 — 개발자가 컴포넌트 파일(예: BAIButton.tsx)을 수정하고 PR을 만듭니다. 여기까지는 평소와 동일한 개발 흐름입니다.
2단계: 자동 감지 — PR이 생성되면 자동 검사 시스템(GitHub Actions)이 트리거됩니다. 변경된 파일 목록에서 BAI 컴포넌트 파일만 필터링하고, 어떤 컴포넌트가 수정되었는지 감지합니다.
3단계: 분석 — GitHub Copilot CLI를 활용한 분석 단계입니다. 변경된 컴포넌트에 대응하는 스토리 파일이 존재하는지 확인하고, Props 인터페이스를 파싱해서 스토리의 argTypes 커버리지를 분석합니다. 새로 추가된 prop이 스토리에 반영되어 있는지, 타입이 변경된 prop은 없는지를 자동으로 검사합니다.
4단계: 리포트 — 분석 결과가 PR 댓글로 자동 작성됩니다. 어떤 컴포넌트의 스토리를 새로 만들어야 하는지(Create), 어떤 스토리를 업데이트해야 하는지(Update)를 테이블로 보여줍니다. 댓글 하단에는 @claude /bui-component.stories.tsx 커맨드가 안내되어, 바로 다음 단계로 넘어갈 수 있습니다.

5단계: AI가 스토리 생성 — PR에서 @claude를 멘션하면, Claude가 가이드라인을 참고해서 스토리를 자동 생성하고 직접 커밋합니다. prop 타입에 따라 적절한 Storybook 컨트롤을 매핑하는데, boolean이면 체크박스, 유니온 타입이면 셀렉트 드롭다운, string이면 텍스트 입력 같은 식입니다.

6단계: 배포 — 리뷰를 거쳐 merge되면 AWS Amplify가 자동으로 빌드하고, ui.backend.ai에 배포됩니다.
이 파이프라인의 핵심은, 코드 리뷰어가 "스토리 업데이트했어?"라고 물어볼 필요 없이 시스템이 감지하고, AI가 생성하고, 자동으로 배포되는 구조라는 것입니다. 개발자는 컴포넌트만 수정하면 나머지는 파이프라인이 처리합니다.
이 파이프라인을 구성하기 위해 만든 도구들을 하나씩 소개하겠습니다.
AI에게 기준을 먼저 가르치기: 1,000줄짜리 가이드라인
파이프라인의 모든 자동화가 참조하는 기준 문서입니다(#4958). CSF(Component Story Format) 3 포맷을 기본으로 정하고, Meta 설정 방법, 6가지 스토리 정의 패턴, 안티 패턴, 체크리스트까지 포함한 1,000줄이 넘는 종합 가이드라인을 만들었습니다.
왜 이렇게까지 상세한 문서가 필요했을까요? AI에게 "스토리를 만들어 줘"라고 말하는 것만으로는 쓸 만한 결과가 나오지 않기 때문입니다. 맥락 없이 시키면, Storybook 7 문법과 8 문법을 섞어 쓰거나, argTypes를 누락하거나, 우리 프로젝트의 Relay Fragment 패턴을 전혀 모른 채 코드를 만들어 냅니다.
어떤 도구 분야에서든 마찬가지이겠지만, AI가 쓸 만한 결과를 내려면 사람이 먼저 "좋은 결과"의 기준을 정의해야 합니다. 가장 좋은 예시를 모으고, AI가 왜 틀렸는지를 분석하고, 거기서부터 자유도를 점점 넓혀가는 반복이 필요합니다. 1,000줄이라는 분량은 과시가 아니라, 그 반복의 결과물입니다.
이 가이드라인을 .github/instructions/storybook.instructions.md에 둔 이유가 있습니다. GitHub Copilot의 Custom Instructions 기능이 특정 파일 패턴에 매칭되는 instruction 파일을 자동으로 참조하기 때문입니다. *.stories.tsx 파일에 적용되도록 설정해두면, 팀원이 스토리를 편집할 때 Copilot이 우리 컨벤션을 자동으로 참조해서 코드를 제안합니다.
AI 기반 스토리 자동 생성
파이프라인 5단계에서 사용되는 도구입니다(#4959). Claude Code에서 /manage-bui-component-story라는 슬래시 커맨드를 만들었는데, 컴포넌트 파일 경로를 인자로 주면 Claude가 가이드라인과 props 인터페이스를 분석해서 CSF 3 포맷의 스토리 파일을 자동 생성합니다.
처음부터 잘 작동한 것은 아닙니다. 초기에 Claude가 만든 스토리는 props의 타입을 잘못 매핑하거나, 불필요한 데코레이터를 추가하는 경우가 많았습니다. 그때마다 "왜 틀렸는지"를 분석해서 가이드라인에 반영하는 반복을 거쳤습니다. 안티 패턴 섹션이 대표적인데, Claude가 실제로 범한 실수들을 하나씩 모아서 "이렇게 하지 마라"는 목록으로 축적한 것입니다. 이 과정을 건너뛰면 자동화 도구를 만들어도 결국 사람이 매번 결과물을 처음부터 고쳐야 합니다.
나중에 두 차례 확장해서, 여러 컴포넌트를 한 번에 처리하는 배치 모드와, 기존 스토리를 현재 props와 비교해서 업데이트하는 UPDATE 모드도 추가했습니다.
GitHub Actions: CI로 빈틈 잡기
파이프라인 2~4단계를 담당하는 부분입니다. 두 가지 검사를 순차적으로 수행합니다.
첫 번째는 스토리 존재 여부 검사입니다. PR이 ready_for_review로 전환되면, 변경된 컴포넌트 파일에 대응하는 스토리 파일이 있는지 확인합니다. 없으면 PR 댓글로 누락 목록을 올리고, 스토리가 추가되면 자동으로 댓글을 삭제합니다.
두 번째는 커버리지 체커입니다. 스토리 파일이 있어도 props의 절반만 다루고 있을 수 있으니까, 컴포넌트의 Props 인터페이스와 스토리의 argTypes를 비교해서 차이를 리포트합니다. 추가된 prop은 +, 제거된 prop은 -, 타입이 변경된 prop은 ~로 표시하고, 누락된 prop에 대해서는 추천 argType 스니펫까지 제공합니다.
컴포넌트 스토리 작성: 유형별로 다른 접근
자동화 도구가 갖춰진 후, 본격적으로 스토리를 작성했습니다. 여기서 모든 컴포넌트를 동일한 방식으로 다룰 수 없다는 점이 중요했습니다. 데이터 의존성에 따라 크게 3가지 유형으로 나뉘고, 유형에 따라 난이도와 접근 방식이 완전히 달랐습니다.
기본 컴포넌트 — BAIButton, BAIModal, BAICard 같은 props만 넘기면 동작하는 컴포넌트들입니다(#5005, #5007). argTypes를 정의하면 Storybook의 Controls 패널에서 직접 props를 조작하며 탐색할 수 있습니다. "기본"이라고 해서 단순한 건 아닙니다. BAIButton은 비동기 액션 핸들링과 더블클릭 방지 기능이 있어서 클릭 후 로딩→완료 흐름을 스토리로 보여줘야 했고, BAIResourceNumberWithIcon은 NVIDIA GPU, AMD GPU, TPU 등 14종 리소스 타입별 아이콘을 전부 표현해야 했습니다.
유틸리티 컴포넌트 — BAIIntervalView, BAIUncontrolledInput 같은 내부 상태나 타이머 로직을 가진 컴포넌트들입니다. 단순히 props를 넘기는 것만으로는 동작을 보여줄 수 없어서, render 함수 안에서 React hooks를 조합해야 합니다.
여기서 실제로 삽질한 얘기를 하나 하자면, BAIIntervalView는 지정된 간격마다 children을 다시 렌더링하는 컴포넌트인데, 스토리에서 "현재 몇 번째 갱신인지"를 보여주려고 useState로 카운터를 관리했더니 브라우저가 완전히 멈춰버렸습니다. 인터벌이 발생할 때마다 state를 갱신하고, state 갱신이 리렌더를 일으키고, 리렌더가 인터벌을 재등록하는 무한 루프에 빠진 겁니다. useRef로 바꿔서 리렌더를 트리거하지 않도록 수정해서 해결했는데, 처음에는 뭐가 문제인지 파악하는 데만 한참 걸렸습니다.
Relay Fragment 컴포넌트 — 가장 까다로운 유형이었습니다(#5024, #5011). Backend.AI WebUI는 데이터 페칭에 GraphQL(Relay)을 사용합니다. Relay의 Fragment 패턴은 각 컴포넌트가 자신이 필요한 데이터를 GraphQL fragment로 선언하고, 부모로부터 해당 데이터를 받는 구조입니다. 문제는, Storybook에서는 실제 GraphQL API 서버가 없다는 것입니다. 데이터를 줄 부모도, 서버도 없으니 컴포넌트가 렌더링 자체를 할 수 없습니다.
이걸 해결하기 위해 RelayResolver + MockPayloadGenerator 패턴을 적용했습니다. Storybook 전용 GraphQL 쿼리를 정의하고, 가짜 Relay 환경에서 mock 데이터로 응답하게 만드는 구조입니다.
처음에는 태그나 표시 컴포넌트처럼 단순한 것부터 시작했는데, 모달/버튼 컴포넌트에서 가장 어려운 문제를 만났습니다. 모달은 열릴 때 쿼리로 데이터를 가져오고, 확인 버튼을 누르면 뮤테이션으로 데이터를 변경합니다. 그런데 기존 RelayResolver는 GraphQL 오퍼레이션을 하나만 처리할 수 있었습니다. 재귀 리졸버 패턴을 적용해서 여러 오퍼레이션을 무제한으로 처리할 수 있게 만들었고, 이 패턴은 이후 다른 뮤테이션 포함 컴포넌트의 스토리에도 재사용했습니다.
이 과정에서 뜻밖의 성과도 있었습니다. 셀렉트/쿼리 컴포넌트들의 스토리를 작성하다가 불필요한 passthrough 래퍼 컴포넌트를 발견해서 제거하기도 했고, 테이블 컴포넌트에서는 Relay의 Global ID를 btoa()로 생성해야 한다는 걸 알게 되기도 했습니다. Claude가 가이드라인을 바탕으로 만든 스토리에서 이런 문제가 드러나기도 했는데, 구조화된 기준 안에서 작업하다 보니 사람이 놓쳤던 것이 수면 위로 올라온 것입니다. 스토리를 작성하는 과정 자체가 코드를 깊이 이해하고 개선하는 기회가 되었습니다.
배포 & 안정화: 로컬에서 되는데 배포에서 안 되는 이유
작성한 스토리들은 ui.backend.ai에 배포되어 팀 전체가 브라우저에서 바로 확인할 수 있게 되었습니다. 그런데 세 가지 컴포넌트에서 문제가 터지는 문제가 발생했습니다. BAIFetchKeyButton은 dayjs의 relativeTime 플러그인이 없어서 상대 시간 텍스트가 깨졌고, BAIAdminResourceGroupSelect는 mock resolver의 GraphQL 타입명이 실제 스키마와 달라서 드롭다운 항목이 하나로 합쳐졌고, BAILink는 테마 폰트가 적용되지 않았습니다. 세 가지 문제 모두, Storybook이 앱의 런타임 환경을 공유하지 않는다는 같은 근본 원인에서 비롯되었습니다.
스토리가 50개를 넘어가면서 사이드바 정리도 필요해졌습니다. Components/Button/BAIButton처럼 3단계 계층이었던 것을 Button/BAIButton처럼 2단계로 단순화하고, BAI 아이콘 전체를 한 화면에서 검색할 수 있는 Icons overview 스토리도 추가했습니다.
정리
3개월간의 작업을 정리하면 이렇습니다.
- 인프라: i18n, ConfigProvider, 브랜딩, 테마 툴바, v10 업그레이드, 인스턴스 통합
- 자동화: 1,000줄 가이드라인, Claude 슬래시 커맨드, 스토리 존재 검사 CI, 커버리지 체커 CI
- 스토리: 기본 컴포넌트 15개, 유틸리티 7개, Relay Fragment 27개
- 배포 & 안정화: ui.backend.ai 배포, 사이드바 구조 정리, 기존 스토리 문서화 보강
총 22개 PR 중 17개가 merge되었고, 4개가 리뷰 진행 중입니다.
돌이켜보면 가장 잘한 판단은 인프라와 자동화를 먼저 만든 것입니다. 가이드라인 없이 스토리를 작성했다면 사람마다 포맷이 달랐을 것이고, CI 검사 없이는 props 누락을 사람이 일일이 확인해야 했을 것입니다. 앞에서 만든 10개의 인프라/자동화 PR이 뒤의 12개 스토리/배포 PR의 품질과 속도를 결정했다고 생각합니다.
두 가지 남는 것
이 작업을 하기 전까지 BAI 컴포넌트는 개발자가 직접 눈으로 확인할 수 있는 화면이 없었습니다. 컴포넌트가 어떻게 생겼는지 알려면 소스 코드를 읽거나, 앱을 직접 띄워서 해당 컴포넌트가 쓰이는 페이지까지 찾아가야 했습니다. 이제는 ui.backend.ai에 접속하면 모든 컴포넌트를 브라우저에서 바로 탐색할 수 있습니다. 하지만 단순히 "볼 수 있게 되었다"는 것보다 더 중요한 교훈이 두 가지 있습니다.
첫째, 기반이 먼저다. BAI 컴포넌트는 여러 화면에서 공통으로 쓰이는 부품입니다. 기준 없이 각자 가져다 쓰면 같은 버튼인데 화면마다 다르게 동작하는 불일치가 쌓입니다. Storybook은 이 문제에 대한 답입니다. 각 컴포넌트가 어떤 props를 지원하고 어떤 상태가 가능한지를 시각적으로 정의해두면, 그것이 곧 컴포넌트의 기준이 됩니다. 스토리를 먼저 많이 만들려는 유혹이 있었지만, 가이드라인과 자동화를 먼저 갖추고 나서야 50개 이상의 스토리를 일관된 품질로 만들 수 있었습니다.
둘째, AI와 일할 때 사람의 역할이 바뀐다. "스토리를 만들어 줘"라고 명령하는 것은 쉽습니다. 하지만 AI가 쓸 만한 결과를 내려면, 먼저 사람이 좋은 결과의 기준을 정의해야 합니다. 1,000줄 가이드라인은 한 번에 쓴 것이 아니라, AI의 실패를 관찰하고 원인을 분석해서 반복적으로 갱신한 결과물입니다. 사람이 스토리를 보고 컴포넌트를 일관되게 사용하듯, AI도 .github/instructions/storybook.instructions.md라는 가이드라인을 보고 일관된 스토리를 생성합니다. 사람을 위한 기준(Storybook)과 AI를 위한 기준(가이드라인)이 같은 곳에서 출발한다는 것이 이 파이프라인이 실제로 돌아갈 수 있었던 이유입니다. 코드를 작성하는 일이 줄어드는 대신, 기준을 정의하고 갱신하는 일이 늘어났습니다. AI를 도구로 쓴다는 것은 결국 그런 일입니다.
이 인턴십에서 가장 의미 있었던 건 컴포넌트에 대해 "모두가 같은 것을 보고 이야기할 수 있는 기반"을 만든 것이었다고 생각합니다. 그리고 그 "모두"에는 이제 AI도 포함되는 것이죠.
이 글에서 언급된 PR들은 모두 lablup/backend.ai-webui 레포지토리에서 확인할 수 있습니다.
