피처링코에서는 콘텐츠 마케팅 캠페인의 성과를 리포트 형태로 제공합니다.
초기에는 기획자가 설계한 UI를 기준으로 정해진 지표만을 시각화하여, 그 결과를 그대로 전달하는 방식으로 리포트 시스템이 운영되었습니다.
하지만 캠페인 규모가 커지고, 기업마다 분석하고자 하는 지표 조합이나 방식이 달라지면서 고정된 구조의 리포트는 반복적인 개발 리소스를 요구하는 문제에 직면하게 되었습니다.
새로운 리포트를 구성하려면 화면을 다시 설계하고 UI를 직접 코딩해야 했으며, 단순히 지표 순서를 바꾸는 요청조차도 코드 수정과 배포 과정을 거쳐야 했습니다.
특히 피처링의 리포트는 UX/UI가 미리 설계되어 있는 상태에서 API 응답만 주입받는 구조였기 때문에, 사용자가 커스텀할 수 없고, 리포트 수정 또한 개발팀의 수작업에 의존할 수밖에 없었습니다. 피처링의 운영 구조만으로는 다양한 고객 요구에 유연하게 대응하기 어려웠고, 결국 고객사 맞춤 리포트를 SI 형태로 개별 개발하는 일이 반복되었습니다.
이러한 구조적 한계를 해결하기 위해, 우리는 DataEffect의 분석 화면을 서버 주도형(Server-Driven UI) 으로 재설계했습니다. 기존처럼 화면을 코드로 설계하는 대신, UI의 전체 구조(Page, Section, Widget)를 서버에서 정의하고 프론트엔드는 이 구조를 해석해 각 위젯을 비동기적으로 호출하고 시각화하는 방식입니다.
이 글에서는 기존의 정적 리포트 시스템을 데이터 중심 대시보드 플랫폼으로 확장하기 위해 도입한 위젯 시스템의 설계 배경, 실제 적용 방식, 확장 가능성까지 기술적으로 풀어보려 합니다.
위젯 기반 구조로 전환하기 위한 설계 기준
고정된 리포트 화면을 위젯 단위로 분해하기 위해서는, 단순히 UI를 조립 가능한 구조로 만드는 것만으로는 부족했습니다.
다양한 고객사의 요구를 수용하면서도, 일관된 방식으로 화면을 구성할 수 있으려면 기획, 데이터, 렌더링 방식까지 포괄하는 설계 기준이 필요했습니다.
따라서 다음과 같은 네 가지 기준을 바탕으로 위젯 시스템의 구조를 설계했습니다.
- 구성의 유연성
기업마다 중요하게 여기는 지표는 서로 다릅니다.
어떤 곳은 인게이지먼트 수치를 우선시하고, 어떤 곳은 광고 효율을 먼저 보고자 합니다.
따라서 리포트 화면은 코드에서 정의된 고정 레이아웃이 아니라, 서버 응답을 기반으로 유동적으로 구성되어야 했습니다. - 지표 간 독립성과 비동기성
각 위젯은 서로 다른 API를 통해 데이터를 가져오며, 지표마다 호출 조건(날짜, 플랫폼, 인플루언서 등)이 상이합니다.
모든 지표를 한 번에 불러오고 렌더링하는 방식은 성능 면에서도 한계가 있었습니다.
그래서 화면을 먼저 그리고, 지표는 각 위젯 단위로 비동기 fetch하는 구조를 기본으로 설계했습니다. - 구조의 일관성과 확장성
리포트에 사용되는 지표는 점점 더 다양해지고 있습니다.
처음에는 scalar, bar_chart 정도였지만, 이후에는 area_chart, table, line_chart 등으로 확장되었습니다.
각기 다른 유형의 데이터를 하나의 공통된 구조로 표현할 수 있어야 유지보수가 가능하다고 판단했고, 모든 위젯은 동일한 스키마 하에서 type, data, response_data를 갖는 구조로 정리되었습니다. - 레이아웃 제어 가능성
시각화 도구에서 지표를 어디에 어떤 크기로 배치할 수 있느냐는 사용자 경험에 매우 큰 영향을 미칩니다.
각 지표들은 단순 나열이 아니라 정보의 우선순위를 표현할 수 있어야 했습니다.
따라서 각 위젯에 position_x, position_y, width, height를 부여하여 레이아웃을 서버에서 정의하고, 프론트는 그 메타 정보를 기반으로 배치할 수 있도록 설계했습니다.
이 네 가지 기준은 이후에 설계된 Page, Section, Widget 구조의 기반이 되었고,
서버가 UI의 구성을 정의하고 프론트는 해석해 표현하는 Server-Driven UI 시스템으로 안정적으로 전환하는 데 중요한 역할을 했습니다.
구조 설계 방식
DataEffect의 리포트 시스템은 Page → Section → Widget으로 구성된 계층형 Server-Driven UI 구조를 기반으로 동작합니다. 이 세 가지 단위는 화면 구성은 물론, 데이터 요청, 상태 관리, UI 렌더링까지 전체 흐름의 중심이 됩니다.
모든 리포트 화면은 이 구조를 기준으로 렌더링되며, 다양한 분석 목적에 따라 기업 맞춤형 확장이 가능하도록 설계되었습니다.
Page: 전체 화면을 대표하는 루트 모델
DataEffect의 리포트 시스템은 화면 전체를 Page라는 단위로 추상화합니다.
하나의 Page는 특정 캠페인에 귀속된 분석 화면을 의미하며, 서버에서 응답된 구조를 기반으로 동적으로 렌더링됩니다.
예를 들어, 아래는 캠페인의 요약 정보를 제공하는 Summary 페이지와 콘텐츠 인사이트를 제공하는 Insight 페이지의 실제 예시입니다.
이 두 페이지는 서로 다른 목적을 가지고 있지만, 공통적으로 같은 Page 모델을 응답값으로 사용합니다. 프론트엔드는 이 Page 구조를 해석하여, 어떤 분석 화면이든 동일한 방식으로 렌더링할 수 있게 됩니다.
type Page = { code?: string; query: Query; section_list: Section[]; type?: PageType; ... };
다양한 UI 요구를 하나의 모델로 추상화
Summary 페이지와 Insight 페이지는 설계 목적과 인터랙션 방식이 크게 다릅니다.
Summary는 사용자가 구성할 수 있는 위젯 보드 형태의 화면으로, 각 위젯의 position_x, position_y, width, height 값을 기반으로 배치됩니다.
반면 Insight는 기획자가 정의한 고정형 UI/UX에 맞춰 구성됩니다.
필터, 영역 차트, 테이블 등은 모두 서버에서 정해진 데이터 구조에 따라 내려오며, 클라이언트는 그 응답을 기반으로 정해진 방식으로 시각화합니다.
이처럼 두 페이지는 UI 구성 방식과 사용자 관여도에서 뚜렷한 차이가 있지만, 서버는 두 경우 모두 동일한 Page 모델로 응답을 내려줍니다.
프론트엔드는 이 공통된 구조를 해석한 뒤, 화면 타입에 따라 분기 렌더링 로직을 적용함으로써, 구조의 일관성과 로직의 재사용성을 동시에 확보할 수 있습니다.
Section: 화면의 구역을 정의하는 중간 단위
Section은 하나의 Page를 구성하는 중간 단위로, 화면을 기능적 또는 시각적으로 분리된 영역으로 나누는 데 사용됩니다.
각 Section은 하나 이상의 Widget으로 구성되며, 동일한 카테고리의 지표들을 묶거나 레이아웃 상 하나의 행/열로 표현될 영역을 정의합니다.
type Section = { code: string; // Section 고유 식별코드 widget_list: Widget[]; // layout position_x: number; position_y: number; width: number; height: number; };
이 구조는 실제로 렌더링/비동기 요청/상태 관리의 단위가 되며, 이후 서술할 Widget 단위의 API 요청도 이 기반 위에서 수행됩니다.
Section의 렌더링 흐름
Section은 화면 내에서 어떻게 배치되는지를 결정하는 레이아웃 단위로 동작합니다.
각 Section은 position_x, position_y, width, height 속성을 통해 12-column 기반의 그리드 레이아웃 시스템에서 자신의 위치와 크기를 정의합니다.
예를 들어, 아래 Summary 화면에서는 다음과 같은 두 개의 Section이 존재합니다:
- Section 1 – Graph 영역
- (0,0), width: 12, height: 5
- Section 2 – Scalar 지표 영역
- (0,5), width: 12, height: 4
참고로 Section 간에는 기본적으로 gap이 적용되어 있습니다.
Section 내부 Widget List 렌더링 방식
각 Section에는 여러 개의 Widget이 포함되어 있으며, 이 widget_list는 Section이 서버로부터 응답될 때 함께 전달됩니다.
따라서 Section이 렌더링될 때, 내부 위젯도 동기적으로 함께 레이아웃 구조를 잡을 수 있습니다.
이러한 구조 덕분에 사용자는 화면이 로드되자마자 위젯이 어느 위치에 어떤 형태로 배치되는지를 빠르게 인지할 수 있습니다.
type Widget = { code?: string; // widget 고유 식별코드 type: WidgetType; index: number; data: WidgetData // layout position_x?: number; position_y?: number; width?: number; height?: number; }; // data_value가 아닌 title, name 정보들을 포함한 타입 type WidgetData = { name: string; title: string; tooltip?: string; unit?: string; formula_text?: string; color?: string; };
Widget: 데이터를 시각화하는 최소 단위 컴포넌트
Widget은 리포트 화면에서 실제로 데이터를 시각화하는 최소 단위입니다. 각 Widget은 하나의 지표 또는 정보를 담당하며, 화면상에서는 카드, 차트, 도표 등 다양한 형태로 표현됩니다.
위젯 단위의 비동기 fetch 구조
각 Widget은 자신의 고유 code와 type에 따라 독립적인 API 요청을 보냅니다.
예를 들어, type: scalar, code: abc123
인 위젯은 이 정보만으로 서버에서 데이터를 fetch하고, 해당 응답값을 기준으로 렌더링됩니다.
앞서 소개한 Section 구조처럼, Widget은 초기 렌더링 시점에 이미 Section 안에 배치되어 있는 상태입니다.
이때 title, position_x/y, width/height 등의 메타 정보는 이미 클라이언트에 존재하므로, 스켈레톤 UI를 먼저 렌더링할 수 있고, 이후 위젯별로 데이터를 비동기 fetch하여 실제 콘텐츠를 표시하게 됩니다.
위젯 type과 response_data 구조
현재 위젯은 크게 3가지 타입으로 분기되어 처리됩니다. 각 타입은 고유한 response_data 구조를 가지고 있으며, 프론트는 이 구조를 기반으로 시각화 컴포넌트를 분기 처리합니다.
타입 |
설명 |
예시 지표 |
---|---|---|
scalar |
수치형 위젯 |
좋아요 수, CPE, 평균 조회수 등 |
chart |
차트형 위젯(시계열 기반) |
일별 조회수, 콘텐츠별 반응 추이 등 |
table |
표 형태의 비교/분석용 위젯 |
콘텐츠 목록, 인플루언서 분석 등 |
Query: 필터 상태의 스냅샷과 Widget 요청
DataEffect의 리포트에서는 사용자가 설정한 필터 조건을 단순히 클라이언트 상태로만 유지하지 않습니다. 모든 필터 조합은 서버 측에서 query_code라는 고유한 식별자로 스냅샷(snapshot) 저장되며, 이 코드를 기반으로 각 Widget의 데이터 요청이 이루어집니다.
이처럼 Widget은 자신에게 주어진 code, type, query_code(optional) 세 가지 값만으로 서버와 통신할 수 있고, 서버는 각 query_code에 저장된 필터 조건을 기준으로 해당 Widget에 맞는 데이터셋을 반환합니다.
이러한 구조는 위젯이 분산되어 렌더링되더라도 필터 상태의 일관성을 유지시킬 수 있고, 복잡한 UX 흐름과 데이터 요청 로직 사이의 경계를 명확히 구분할 수 있었습니다. 또한, query_code는 특정 분석 상태를 고정된 형태로 저장하거나, 다른 사용자와 공유하는 것도 가능하여 보다 안정적이고 재사용할 수 있게 되었습니다.
정형화된 페이지에서의 흐름: Insight
지금까지는 Summary 페이지를 중심으로 Page -> Section -> Widget 구조를 설명했습니다.
하지만 앞서 Page 단락에서 언급했듯, DataEffect는 기획에 맞춘 고정형 UI 페이지에서도 동일한 구조를 적용하고 있습니다.
사용자가 선택한 콘텐츠 필터, 분석 지표, 그래프 타입, 테이블 내 정보 구성 등의 query 정보를 먼저 서버에서 받아오고, 그 query_code를 기반으로 Section 안에 포함된 각 Widget이 자신에게 필요한 데이터를 요청하여 렌더링됩니다.
Summary 페이지와 마찬가지로, Insight 페이지 역시 각 Widget은 scalar, chart, table의 타입으로 나뉘며, 응답받는 데이터 구조도 동일하게 통일되어 있습니다. 프론트엔드는 Widget의 타입에 따라 해당 데이터를 해석하고 시각화하는 역할만 수행합니다.
이처럼 UI 구성 방식이 다르더라도, 모든 리포트를 일관된 데이터 셋안에서 처리할 수 있게 되었습니다.
마치며
다양한 리포트 요구사항을 하나의 구조로 수렴시킨 이번 경험은 단순히 컴포넌트를 재활용한다라는 생각을 넘어, 분석 플랫폼 전반의 일관성과 확장성을 정립하는 중요한 전환점이었습니다.
처음 Widget, Section, query_code라는 핵심 개념을 정리하고 이를 실제 동작하는 구조로 만들기까지는 설계에도, 데이터 구조 설득에도, 구현에도 많은 시간이 소요되었습니다. 하지만 한번 정의한 후로 모든 리포트 페이지를 일관된 형태로 구성할 수 있었고, 새로운 요구사항도 기존의 구조 안에서 유연하게 대응할 수 있었습니다.
요즘 말하는 Full Server-Driven UI처럼, 페이지 전체를 서버가 구성하는 방식은 아니지만, 서버 데이터를 세팅해두는 것만으로 리포트 페이지가 자동으로 구성되고, formula같은 커스텀한 지표까지 포함할 수 있는 유연한 기반이 마련된 것입니다.
다양한 요구를 하나의 구조 안으로 정리해낸 경험 자체가 가장 큰 성과였고, 앞으로 더 다양한 분석 시나리오로도 확장 가능한 가능성을 직접 확인한 프로젝트였습니다.