사내·고객사 대상 업무 통합 웹 플랫폼
| 지표 | 결과 |
|---|---|
| 초기 로딩 | 초기 번들 2.5MB → 0.76MB(−70%), Lighthouse 성능 75 → 95, LCP 7.6초 → 1.4초 (데스크톱) |
| 목록 화면 | 29개 페이지 중 24개가 공용 테이블 위젯 1개를 사용 |
| 사용 규모 | 평일 일 평균 약 270명 · 월 약 570명 (사내 구성원 · 고객사) |
| 확장성 | 2025.01 이후 신규 페이지 요청마다 아키텍처 변경 없이 29개까지 확장 |
| 권한 | 라우트 · 메뉴 · 버튼 3계층이 룰셋 1개 공유 |
배경과 역할
레거시는 백엔드 없이 브라우저가 BaaS를 직접 조회하는 바닐라 JS 사이트 8개였다. 화면마다 테이블·폼·권한 로직이 따로 있었고, 집계를 클라이언트에서 처리해 주요 대시보드 로딩이 10초를 넘겼다. 2025년 초 이 도구들을 단일 플랫폼으로 통합하는 프로젝트가 시작됐고(전체 6명 — FE 2 · BE 3(팀장 포함) · 기획·디자인 1), FE 아키텍처와 공용 체계 설계를 맡았다.
FE 2명의 분담은 아래와 같다.
| 영역 | 담당 |
|---|---|
| FSD 레이어 구조 · 기술 선정 · 라우팅 | 본인 |
| 공용 테이블 위젯 · 공통 훅 | 본인 |
| 권한 시스템(룰셋 · 평가 함수 · 3계층 적용) | 본인 |
| 서버 상태 계층(TanStack Query · Axios 클라이언트 · API 공용 훅) | 본인 |
| 동적 폼 — Zod 검증 스키마 런타임 생성 | 본인 |
| 동적 폼 — 메타데이터 기반 UI 렌더러 | 팀원 |
| 집계 API · 서버 사이드 페이징 | BE (FE에서 응답 구조를 정의해 요청) |
무엇을 만들었나 — 데모
세 화면 모두 목데이터 기반 데모 페이지로 촬영했다. 실제 업무 데이터는 포함하지 않는다.
공용 데이터 테이블 위젯
29개 페이지 중 24개의 목록 화면이 사용하는 표준 테이블. 페이지가 TanStack Table 인스턴스를 만들어 주입하고, 위젯은 렌더링과 상호작용만 책임진다.
| 정렬 | 검색 · 페이지네이션 |
|---|---|
![]() | ![]() |
| 컬럼 가시성 제어 | 행 선택 · 일괄 액션 |
|---|---|
![]() | ![]() |
드래그 앤 드롭 행 재정렬 — onDragEnd prop 하나만 넘기면 활성화된다.

대량 결과 입력 위저드
항목 수만큼 행이 생겨 최대 1,700행(입력 칸 약 5천 개)까지 늘어나는 입력 화면. 단계별 검증 → 필터 기반 일괄 편집 → 제출 전 정합성 가드 순으로 흐른다. 행이 많을 때 스크롤 끊김과 입력 지연이 심해 행 가상화를 적용했고, 값은 폼 상태 배열 하나로 관리해 화면 밖 행도 제출 시 전체 검증한다.

현황 메일 발송
필터 조건에 따라 메일 본문이 어떻게 구성될지 즉시 보여주는 발송 화면.

어떻게 설계했나 — 하이라이트
1. 집계 로직의 위치를 바꿨다
레거시는 별도 백엔드 없이 브라우저가 BaaS 컬렉션을 직접 조회하고 조인·집계를 전부 프론트엔드에서 수행했다. 통합 플랫폼에서는 이 구조가 가장 크게 드러나던 테스트 진행 리포트 화면을 서버 집계로 전환했고, 클라이언트는 쿼리 캐시를 갖는다.

같은 기능, 집계 로직의 위치만 다르다. 강조된 상자가 브라우저에서 API 서버로 이동하면서 요청 수가 ×N에서 ×1로 줄었다.
역할 분담: 서버 집계 API는 BE가 구현했다. 나는 리포트 화면에 필요한 응답 구조(집계 단위 · 페이징 · 필터 파라미터)를 먼저 정의해 BE에 전달했고, 그 위에 TanStack Query 캐시 · 무효화 전략과 Axios 기반 API 공용 훅을 설계·구현했다.
2. 29개 페이지에서도 의존성이 얽히지 않는 구조
FSD 기반 4개 레이어의 단방향 의존 구조를 설계했다. 공유가 필요한 코드는 아래 레이어로 내려보내는 것이 유일한 경로다.

규칙은 문서로만 두지 않았다. 팀이 쓰는 Claude 코드 리뷰 스킬에 레이어 의존 방향과 상태 분리 규칙을 명시해, PR 리뷰 단계에서 점검한다.
3. 상태를 출처 기준으로 세 갈래로 분리
"전역 상태 하나에 다 넣는" 구조 대신 서버 상태(TanStack Query) · 클라이언트 상태(Zustand) · URL 상태를 각각의 SSOT(single source of truth)로 뒀다. 서버 데이터를 전역 스토어에 복사하는 패턴은 리뷰 규칙으로 금지했다.

4. 룰은 한 곳, 소비는 세 곳 — 선언적 권한 시스템
역할 × 조직 조합의 접근 제어를 페이지마다 if문으로 흩뿌리는 대신, 중앙 룰셋과 단일 평가 함수로 통일했다. 라우트 가드 · 메뉴 노출 필터 · 버튼 게이팅 세 계층이 같은 규칙을 공유한다.

"이 페이지는 누가 보나"의 답이 항상 룰셋 한 파일에 있다.
이 룰셋은 FE에만 있어서 화면을 거치지 않는 API 호출까지 막지는 못했다. 그래서 2026년부터 권한 정의와 최종 검사를 BE로 옮기는 전환을 진행 중이다(FE 담당, 권한 DB · API는 BE 구현).
5. 필드 추가에 배포가 필요 없는 동적 폼
서버가 내려주는 필드 메타데이터만으로 폼 · 테이블 컬럼 · Zod 검증 스키마가 런타임 생성된다. 현재 9개 페이지가 이 구조를 쓰고, 필드가 거의 바뀌지 않는 폼은 FE에서 직접 정의한다.

분담: 메타데이터 → Zod 스키마 변환 계층은 본인, 메타데이터 → UI 렌더러는 팀원이 맡았다.
6. 제어 역전으로 만든 표준 테이블
위젯이 데이터를 가져오지 않는다. 페이지의 컨트롤러 훅이 테이블 인스턴스를 만들어 주입하고, 위젯은 렌더링만 책임진다. 페이지는 "무엇을", 위젯은 "어떻게"를 담당한다.

- 위젯은 데이터의 출처를 모르므로 서버사이드 페이징이든 클라이언트 정렬이든 같은 위젯이 소화한다
- 200행 이상에서 가상 스크롤이 자동 활성화된다 — 소량 데이터에서는 오버헤드를 피한다
더 깊이 보기
| 문서 | 내용 |
|---|---|
| 아키텍처 딥다이브 | FSD 레이어 · 상태 분리 · 권한 · 동적 필드 전체 설계 |
| 결과 조회·입력 플로우 | 가장 분기가 많은 기능의 화면 통합, 접근 제어 이중화, 제출 검증 파이프라인 |
설계 결정 요약
[Architecture] 왜 FSD인가 문제 — 29개 페이지 규모에서 페이지끼리 서로를 참조하기 시작하면 되돌릴 수 없다. 선택 — 4개 레이어 단방향 의존을 강제하고, 공유 코드는 반드시 아래 레이어로 내려보내는 규칙. 근거 — 규칙이 단순해야 팀원이 지킬 수 있고, 실제로 이후 신규 페이지들이 구조 변경 없이 얹혔다.
[State] 왜 Zustand인가 문제 — 상태관리 라이브러리 선정. 선택 — 서버 상태를 TanStack Query로 분리하는 결정을 먼저 했더니 남는 전역 상태가 사용자 정보·세션·다이얼로그 정도로 작았고, 그 크기에 Redux의 보일러플레이트는 과한 비용이었다. 근거 — 문제의 크기에 도구를 맞췄다.
[Widget] 왜 headless 조합인가 문제 — 완성형 그리드 라이브러리를 쓸 것인가, 직접 조합할 것인가. 선택 — 목록 화면이 플랫폼의 중심이라 테이블이 곧 디자인 시스템이었고, 렌더링을 100% 자체 디자인 시스템으로 가져가기 위해 headless를 택했다. 근거 — 초기 비용은 위젯 1개 개발로 지불하고, 이후 목록 페이지를 추가할 때는 컨트롤러 훅만 쓰면 된다.
[Permission] 왜 중앙 룰셋인가 문제 — 버튼만 숨기면 URL 직접 접근이 뚫리고, 라우트만 막으면 "보이는데 못 들어가는" 불일치가 생긴다. 선택 — UI 노출 판정과 라우트 가드가 같은 평가 함수를 공유하는 이중 방어. 근거 — 안내(UX)와 차단(방어)을 분리하되 규칙은 한 곳에 두어 불일치를 없앤다.



