[SaaS] 왜 우리는 뷰모델링을 중요하게 생각하는가
![[SaaS] 왜 우리는 뷰모델링을 중요하게 생각하는가](/contents/posts/26cd9338-d3d4-80f2-bbc3-cce9cea1efd3/cover/%E1%84%87%E1%85%B2%E1%84%86%E1%85%A9%E1%84%83%E1%85%A6%E1%86%AF%E1%84%85%E1%85%B5%E1%86%BC_%E1%84%80%E1%85%B3%E1%86%AF_%E1%84%8A%E1%85%A5%E1%86%B7%E1%84%82%E1%85%A6%E1%84%8B%E1%85%B5%E1%86%AF.png-1080w.webp)
안녕하세요, 힐링페이퍼에서 미용의료 병원향 SaaS 제품인 KOS의 프론트엔드를 개발하고 있는 이썬(Ethan)입니다.
이 글에서는 DDD(Domain Driven Design)를 기반으로 제품을 개발하고 있는 KOS 팀에서 생각하는 뷰모델링과 뷰모델에 대한 개념과 함께 DDD를 하는 팀에서 뷰모델링이란 어떤 의미를 가지는지, 우리는 왜 FLUX 패턴을 선택했는지에 대해 이야기해 보려 합니다.
들어가며
흔히 모델은 특정 객체를 지칭하는 것이라고 오해받고는 합니다. 잠깐 위키백과의 내용을 살펴보면 모델이란“객체, 시스템, 또는 개념에 대한 구조나 작업을 보여주기 위한 패턴, 계획, 또는 설명이다.” 라고 정의 내리고 있으며, 모델을 만드는 과정을 모델링이라고 정의하고 있습니다. 이처럼 모델은 특정 객체를 지칭하는 것이 아니라 모델링이라는 작업을 통해 만들어지는 부산물로써 패턴이나 계획, 또는 설명을 일컫습니다.


필연적으로 우리는 제품을 개발하는 과정에서 모델링을 하게 됩니다. 도메인의 객체, 시스템, 개념 등을 추상화하고 문제를 정의하고 해석해 나가는 작업은 도메인 모델링이라고 합니다. 그리고 우리가 정의한 문제 해결 방식을 사용자에게 제공해 주기 위한 제품의 화면, 즉 뷰의 객체, 시스템, 개념 등을 추상화하는 작업은 뷰모델링이라고 합니다.
뷰모델링과 뷰모델
KOS팀에서는 뷰모델링과 뷰모델을 어떻게 정의하는지, 이를 구현하는 방법으로 왜 FLUX를 선택했는지에 대해 말씀드리겠습니다.
뷰모델링
뷰모델을 만드는 작업입니다.
뷰모델링은 프론트엔드 애플리케이션에서 UI를 구성하기 위한 상태와 변화를 일으키는 액션 등 뷰를 추상화하고 설계하는 작업입니다. 단순히 상태 관리 라이브러리를 선택하는 것이 아니라, 비즈니스 로직을 추상화하는 도메인 모델링처럼 UI 표현을 위한 추상화 작업입니다.
이 과정은 단순히 중앙 스토어에 어떤 상태를 저장할지를 의미하는 게 아니라 UI 상태 관리 전략과 설계를 포괄합니다. 뷰모델링은 UI 표현에 필요한 상태를 정의하고, 해당 상태들이 어디에 어떻게 저장되고 관리되어야 하는지, 어떻게 매핑되면서 사용될지, 어떤 액션이 발생하고 어떻게 변환되어 가는지 등을 결정하는 과정 전체를 포함합니다.
그러므로 SDUI(Server Driven User Interface)의 구조를 결정하는 것도 뷰모델링의 일부이고, 단순한 Toggle 컴포넌트의 On/Off 상태라 하더라도 생명주기에 따라 전역 컨텍스트로 공유되어야 하는지, 특정 컴포넌트의 상태로 관리해도 되는지 등을 판단하는 과정도 모두 뷰모델링의 일부입니다.
또한 이 과정을 통해 제품의 화면을 그리기 위한 핵심 정보들을 식별할 수 있습니다. 도메인 모델링 과정에서 제품이 중요하게 바라보지 않는 요소들을 도메인 모델에 포함하지 않는 것과 유사합니다.
클라이언트는 복잡도와 싸우는 새로운 전장이 된 지 오래입니다. 정적 페이지는 동적으로 변했고, 다양한 상태를 관리해야 하며, 사용자의 다양한 입력과 경험을 고려해야 하고, 네트워크나 디바이스 환경, 접근성도 고려해야 합니다. 뷰모델링은 이 복잡도와의 전투에서 큰 힘이 되어줄 수 있는 만큼 많은 고민이 필요한 영역입니다.
뷰모델
뷰모델은 어떤 객체의 인터페이스를 말하는 것이 아닙니다.
뷰모델은 UI를 그리는 데 필요한 정보와 사용자 액션, 그로 인한 상태 변화를 함께 정의하여 추상화한 모델로 팀 내에서 합의한 도메인 모델과 프론트엔드 UI 사이의 중간 계층 역할을 담당합니다. 이를 통해 도메인 논리와 UI의 결합도를 낮추어 관심사를 분리하고 각자의 영역에서 다른 곳에 미치는 영향을 최소화한 채 유지보수와 확장에 유연하게 대응할 수 있습니다.
뷰모델은 클라이언트뿐만 아니라 BFF(Backend For Frontend)나 SDUI처럼 API 응답에도 존재할 수 있습니다. 또한 단순한 상태 관리 패턴(Redux, Recoil 등)과는 구별됩니다. 상태 관리 패턴이 어떻게 상태를 저장하고 변경하는지에 대한 기술적 메커니즘을 제공한다면, 뷰모델은 화면에 필요한 데이터와 동작을 추상화하여 무엇을 어떻게 관리할지에 대해 정의한 개념적인 것에 가깝습니다. 그러므로 특정 기술에 종속되지 않으며, 다양한 상태 관리 라이브러리를 통해 구현될 수 있습니다.
뷰모델을 식별하였다면, 어떻게 구현해야 하는지에 대해 고민해 볼 수도 있습니다. 이때 MVP, MVVM, FLUX, Observer 등 여러 패턴 중 적합한 것을 사용할 수도 있으며, 특정 상태 관리 라이브러리의 도입도 고려할 수 있습니다.
이 중 KOS팀은 FLUX 패턴을 선택했습니다. 각 패턴이 가지고 있는 특징들만 생각해서 선택된 것은 아닙니다. DDD로 제품을 개발하는 과정에서 ‘어떤 패턴을 사용했을 때 가장 큰 이익을 얻을 수 있을까’에 대한 고민의 결과였습니다. 도메인 모델을 토대로 프론트엔드의 뷰모델링을 하는 과정에서 자연스럽게 사건 기반의 데이터 흐름을 구현하기에 유리한 구조를 선택하게 되었고, 이게 바로 FLUX 패턴이었습니다.
FLUX 패턴과 도메인 이벤트의 유사성
FLUX는 단방향 데이터 흐름을 가지는 패턴입니다. 상태 변경을 어떤 방식으로 구현할지에 대해서는 강제하지 않지만, 오직 Action을 통해서만 변경할 수 있기 때문에 데이터 흐름 추적이 쉬워 상태 관리의 복잡성을 해결하는 데 유리할 수 있습니다.
DDD에서는 도메인 모델에 대한 명령(Command)과 사건(Event)이 식별되고, 사건 기반으로 데이터 흐름을 정의하게 됩니다. 여기서 발생하는 사건을 생각해 보면 FLUX 패턴에 관한 생각이 같이 떠오를 수 있습니다.
- 도메인 사건: 도메인에서 발생한 사건으로, 이를 기반으로 상태를 구성하는 이벤트 소싱이나 발행-구독을 통해 결과적 일관성을 보장하는 등에 사용될 수 있습니다.
- FLUX 액션: UI에서 발생한 상호작용을 나타내며, 상태 변화를 일으킵니다.
결국 두 패턴 모두 "사건/액션을 발행하고 이후 변화에 대한 관심사를 분리한다"는 측면에서 유사한 추상화 패턴을 따르고 있습니다. 이런 단방향 데이터 흐름과 명확한 상태 변화 경로를 통해 복잡한 시스템을 예측할 수 있고 관리하기 쉽게 만들 수 있습니다.
FLUX 패턴에서는 State와 Action을 정의하고 Store에서 관리합니다. 아래는 실제 제품 내 예약 현황 페이지에서 특징별 예약 수가 몇 건이 되는지를 보여주는 모달을 구현할 때 작성한 코드 중 일부입니다.
State
화면을 표현하는 최신 스냅샷을 위한 객체입니다.
export interface SchedulesSummaryState {
status: 'idle' | 'loading' | 'success' | 'failed';
zones: Zone[];
selectedZoneId: string;
baseDate: Date;
reservationGroupIds: string[];
reservationGroupMap: Map<string, ReservationGroup>;
schedules: Map<ScheduleContract['id'], ScheduleContract>;
selectedFilters: ScheduleStateFilter[];
}
Action
상태 변화를 일으키는 상호작용을 객체로 정의합니다. 상태 변경의 단일 진입점으로, 클라이언트는 미리 정의한 액션으로만 상태를 변경할 수 있습니다. 이를 통해 상태 변경의 세부 구현에 대한 관심사를 분리하고 액션을 통해서만 상태 흐름을 통제하여 예상치 못한 부수 효과를 줄일 수 있습니다.
const actions = {
initialize: () => {},
fetchZones: () => {},
changeZoneId: () => {},
changeBaseDate: () => {},
changeSelectedFilters: () => {},
changeByVisitorCategoryViewMode: () => {},
refetch: () => {},
};
Store
State를 보관하고 관리하는 객체입니다. Action을 받아 State를 변경합니다.
const storeCreator = () =>
createZuflux(
(register) => {
register(SchedulesSummaryActions.initialize.pending, (state, { payload }) => {
return {
...state,
status: 'loading',
baseDate: payload.baseDate,
selectedZoneId: payload.zoneId,
reservationGroupIds: payload.reservationGroups.map((reservationGroup) => reservationGroup.id),
reservationGroupMap: new Map(
payload.reservationGroups.map((reservationGroup) => [reservationGroup.id, reservationGroup]),
),
selectedFilters: payload.filters,
};
});
register(SchedulesSummaryActions.initialize.fulfilled, (state, { payload }) => {
const schedulesMap = getScheduleMap(payload);
const visitorCategoryMap = getVisitorCategoryMap(payload);
return {
...state,
status: 'success',
schedules: schedulesMap,
visitorCategoryMap,
};
});
register(SchedulesSummaryActions.initialize.rejected, (state) => {
return {
...state,
status: 'failed',
};
});
register(SchedulesSummaryActions.changeSelectedFilters, (state, { payload }) => {
return {
...state,
selectedFilters: payload.selectedFilters,
};
});
// ... 다른 동작들
},
initialState,
({ api, action }) => {
if (REFETCH_ACTION_TYPES.includes(action.type)) {
api.dispatch(SchedulesSummaryActions.refetch());
}
},
);
KOS 팀에서의 DDD와 뷰모델링
KOS 팀에서는 도메인의 문제를 정의하고 해석하고 추상화하는 도메인 모델링 작업을 매우 중요하게 생각합니다. 하지만 그렇게 만들어낸 도메인 모델을 클라이언트 애플리케이션에 두지는 않습니다. 도메인 모델을 직접 뷰에 차용했다면, 추후 도메인 모델과 UI의 변화에 굉장히 취약해지고, 팀의 도메인 이해도가 성숙하거나 제품의 사용자 경험이 개선되는 과정에서 지속적인 가치를 제공하기도 어렵게 될 것입니다.
그러므로 도메인 모델과 사용자 인터페이스를 격리하고 지켜내는 것을 매우 중요하게 생각하고 이 둘을 철저히 구분합니다. 이를 달성하기 위해 고민을 하다 보면 어떤 패턴을 사용할지, 어디에 위치시킬지와 같은 고민 또한 자연스레 따라올 수 있습니다.
도메인 모델링을 통해 만들어진 재료를 서버로부터 어떻게 요청하여 어떤 형태로 제공받을지, UI 상태를 어떻게 추상화하고, 이벤트가 발생하면 어떤 상태 변화를 만들어낼지, UI를 그리기 위한 상태를 어디에 저장하고 어떻게 관리할지, 이런 고민을 통해 사용자에게 우리가 해석한 문제 해결 방식을 제품으로 전달하는 것, 이게 DDD를 하는 KOS 팀에서 생각하는 프론트엔드의 뷰모델링이 가지는 의미입니다.
복잡도에 따른 접근법 선택
에릭 에반스가 저술한 DDD 책의 도입에서 모든 제품에 DDD가 필요하지 않다고 말한 것처럼, 모든 프론트엔드 애플리케이션에서 동일한 수준의 뷰모델링을 진행할 필요는 없습니다. 도메인의 복잡도에 따라 전술적 패턴 적용 수준을 다르게 하는 것처럼, 뷰모델링의 추상화 수준도 많이 달라질 것입니다.
- 단순한 CRUD로만 구성된 기능이라면 로컬 상태 관리와 TanStack Query와 같은 도구로 단순화하는 것으로 충분할 수도 있습니다.
- 어느 정도 복잡한 기능이라면 Context를 통한 공유 상태와 커스텀 훅 등을 통해 관련 로직을 캡슐화하는 것으로 충분할 수도 있습니다.
- 엔터프라이즈 급으로 복잡한 기능이라면 UI 상태와 도메인 상태를 명확히 분리하고 관련된 Mapper를 제공하는 등 깊은 고민을 통해 여러 전술적 패턴을 차용할 수 있을 것입니다. 그리고 KOS 팀은 분명하게 이 길을 선택했습니다.
결론
뷰모델링은 화면에 필요한 상태를 정의하고, 그 상태들이 어디에 어떻게 저장되고 관리되어야 하는지 결정하는 과정이며, 클라이언트 내부 상태 관리에만 국한되지 않고 서버와 통신하는 영역까지 확장될 수 있습니다. 또한 여기서도 DDD의 사고방식과 Aggregate, Repository 등의 여러 전술적 패턴을 일부 차용함으로써 복잡한 UI 상태 관리와 백엔드 통신을 효과적으로 처리할 수 있습니다.
단, 중요한 것은 특정 아키텍처나 패턴을 맹목적으로 좇기보다 “이 방식을 통해 우리가 어떤 이점을 얻을 수 있는가?”에 대한 고민을 통해 우리의 제품과 상황에 맞는 적합한 방식을 선택하는 것입니다. 이런 트레이드오프에 따른 손익을 명확히 계산할 수 있는 것이 좋은 엔지니어링의 핵심일 것입니다. 단순히 Redux는 보일러 플레이트가 많기 때문에 다른 상태 관리 라이브러리를 사용한다고 결정하는 것이 아니라 더 합리적인 의사결정이 있어야 합니다.
이런 과정에서 포기할 것과 포기하지 않을 것을 구분하고, 고려하지 않을 것을 고려하지 않는 것도 중요하다는 것을 느끼기도 했습니다. 어떤 관점에서 바라보고 해석하느냐에 따라 다르기 때문에 이런 과정들은 항상 새로운 문제를 해결하고 있다는 뿌듯함을 느끼게 합니다. 나아가 우리의 해답이 실제 사용자들의 문제를 해결하는 것을 직접 보고 느끼면서 직무 만족을 넘어서는 자긍심을 느낍니다.
미용 의료 도메인의 문제를 해결하고 엔지니어로써 성장하는 힐링페이퍼 개발팀과 함께하고 싶으시다면, 아래 채용 링크를 확인해주세요!