뷰모델링에 대한 고민 with Flux 패턴

2026. 1. 21. 00:55Programming/React

 

이전 직장에서 오만가지. 상태를 관리해야하는 대규모 프론트엔드 프로젝트를 경험했습니다.

그 과정에서 상태 관리가 단순한 구현 문제가 아니라,

서비스의 성능과 사용성, 그리고 확장성까지 좌우하는 뼈대와 같은 요소라는 점을 깊이 체감하게 되었습니다.

 

특히 UI를 기준으로 상태를 지나치게 잘게 모델링하면서

디자인 변경에 대한 의존성은 점점 커지고,

반대로 도메인 자체의 응집력은 약해졌던 지난 과오들을 돌아보게 되었습니다.

 

 

subscribe 남용이나 useEffect로 얽힌 복잡한 상태 흐름처럼,

유지보수성과 예측 가능성을 해치는 패턴들도 직접 경험하면서

이후에는 컴포넌트 하나를 개발하더라도 상태 관리 구조부터 고민하게 되었습니다.

 

이러한 시행착오를 통해 쌓이게 된

View Model에 대한 고민과, 지향하게 된 상태 관리 패턴들

이번 포스팅에 정리해보고자 합니다.

 

 

View Model 이란?

백엔드 데이터와 View의 추상화 계층 모델

 

흔히 MVVM 패턴의 구성 요소로 한정해 생각되지만, 프론트엔드 관점에서 View Model은

서버의 데이터 모델을 그대로 노출하지 않기 위한 완충 지대에 가깝다고 생각합니다.

 

백엔드 API 명세 변경에 대한 의존성을 줄이기 위해,

프론트엔드에서는 서버의 데이터 모델을 직접 다루기보다

프론트엔드 전용 데이터 모델로 한 번 더 추상화하는 것이 효과적입니다.

 

이 역할을 View Model이 담당하는 것이죠.

 

View Model 내에서 백엔드 데이터를 프론트엔드 데이터 모델로 변환하여, View는 백엔드 모델에 대한 정보를 알 필요없게되죠.

이러한 관점에서 View Model은 디자인 패턴 중 Adapter 패턴의 한 예시로 볼 수 있겠네요.

 

이 구조를 도입하면 백엔드가 교체되거나 API 명세가 변경되더라도,

그리고 UI 디자인이 수정되더라도,

변경의 영향 범위를 View Model 계층으로 제한할 수 있습니다.

(현업에서 빈번하게 발생하는 디자인 수정과 API 변경을 겪으며 느낀점 이랄까요..)

 

 

Flux 패턴 이란?

단방향 데이터 흐름을 기반으로 애플리케이션 상태를 관리하는 소프트웨어 디자인 아키텍처
Redux의 기반이 되는 패턴

 

Redux 혹은 zustand 가 Flux패턴 기반의 전역 상태 관리 라이브러리입니다.

구조를 살펴보면 Action이라는 명시적인 동작을 통해서만 Store에 접근하죠.

 

이러한 단방향 데이터 흐름이 Flux 패턴의 가장 중요한 특징입니다.

다르게 말하면, 데이터 변경의 주체와 경로가 뚜렷하다는 것입니다.

 

View Model과 Flux 패턴

View Model이라는 추상화 계층을 두었다고 해서

상태 관리 문제가 자동으로 해결되지는 않습니다.

 

View Model의 데이터를 여러 곳에서 직접 수정하거나,

암묵적으로 접근하게 되면 영향 범위를 추적하기 어려워지고

무엇보다 누가 상태를 변경했는지를 파악하기 힘들어집니다.

 

이는 곧 사이드 이펙트 증가와 유지보수 문제로 이어지죠.

 

실제로 한때 Zustand를 View Model처럼 사용하며

set을 통해 상태를 자유롭게 수정하던 질풍노도의 시기가 있었죠..

단기간에는 편했지만, 시간이 지날수록 상태 변경의 흐름을 추적할 수 없게 되었고,

디버깅과 유지보수가 극도로 어려워지는 경험을 하게 되었습니다..

 

이 경험을 통해 느낀 점은,

Flux 패턴에서 말하는 Action은 단순한 함수 호출이 아니라

View Model 단에서 한 번 더 추상화된 “변경의 의도”를 담는 것 입니다.

 

예시에서는 Zustand를 사용했지만,

View Model과 Flux 패턴은 특정 라이브러리나 기술에 종속되지 않습니다.

hooks로 둘수도 있고, 클래스형으로 둘 수도 있겠죠.

 

중요한 것은 구현 방식이 아니라 원칙이죠.

  • 서버에 직접 의존하지 않는 View Model이라는 추상화 계층을 두는 것
  • View Model의 상태 변경 역시 Action을 통해서만 단방향으로 이루어지도록 제한하는 것

 

이 두 가지 원칙을 지킬 때,

상태 변경의 흐름은 명확해지고,

복잡한 UI와 비즈니스 로직 속에서도 예측 가능한 구조를 유지할 수 있습니다.

 

MVC vs MVVM vs Flux

막간을 이용해 대표적인 3가지 패턴을 비교해봅시다.

MVC 

Model : 데이터

View: 사용자 인터페이스(UI)를 담당

Controller :사용자 입력을 받아 Model을 변경하고, View를 업데이트하는 역할

 

해당 패턴은 Controller가 중심이 되어 View와 Model을 연결하고, View와 Model이 직접 연결될 수 있습니다. 

 

이로 인해 데이터 흐름이 완전히 단방향이라고 보기는 어렵고,

상태 변경 경로가 여러 갈래로 퍼질 가능성이 있습니다.

 

이러한 특성 때문에 MVC는

프론트엔드보다는 백엔드 아키텍처(서버 사이드 렌더링, 전통적인 웹 프레임워크)에서

더 많이 활용되어 왔습니다.

// counter.model.ts
export class CounterModel {
  private count = 0;

  getValue() {
    return this.count;
  }

  increment() {
    this.count += 1;
  }
}
// counter.controller.ts
import { CounterModel } from "./counter.model";

export class CounterController {
  constructor(private model: CounterModel) {}

  onIncrement() {
    this.model.increment();
  }

  getCount() {
    return this.model.getValue();
  }
}
import { useState } from "react";
import { CounterModel } from "./counter.model";
import { CounterController } from "./counter.controller";

export function CounterView() {
  const [, forceUpdate] = useState(0);

  const model = new CounterModel();
  const controller = new CounterController(model);

  const handleClick = () => {
    controller.onIncrement();
    forceUpdate((v) => v + 1); // View가 직접 갱신
  };

  return (
    <div>
      <p>Count: {controller.getCount()}</p>
      <button onClick={handleClick}>+</button>
    </div>
  );
}

 

MVVM

Model: 데이터 (MVC와 동일)
View: 사용자 인터페이스(UI)를 담당 (MVC와 동일)
ViewModel: View와 Model 사이에서 데이터를 변환하고 노출

 

MVVM 패턴에서는 ViewModel이 중심이 됩니다.

View는 Model에 직접 접근하지 않고,

ViewModel이 제공하는 데이터와 인터페이스만을 사용합니다.

 

전통적인 MVVM에서는 데이터 바인딩(Two-Way Binding)을 통해

ViewModel이 변경되면 View가 자동으로 업데이트됩니다.

(React에서는 바인딩보다는 상태 구독에 가까운 형태로 구현됩니다.)

 

MVC와의 핵심 차이는 View에서 Model 접근 유무입니다.

// domain/counter.model.ts
export class CounterModel {
  private _count: number;

  constructor(initial = 0) {
    this._count = initial;
  }

  get value() {
    return this._count;
  }

  increment() {
    this._count += 1;
  }

  canIncrement() {
    return this._count < 10;
  }
}
// view-model/useCounterViewModel.ts
import { CounterModel } from "../domain/counter.model";

export const useCounterViewModel = () => {
  const [model] = useState(() => new CounterModel(0));
  const [, forceUpdate] = useState(0);

  return {
    count: model.value,
    canIncrement: model.canIncrement(),
    increment: () => {
      if (!model.canIncrement()) return;
      model.increment();
      forceUpdate((v) => v + 1);
    },
  };
}
// view
function Counter() {
  const vm = useCounterViewModel();

  return (
    <div>
      <p>Count: {vm.count}</p>
      <button disabled={!vm.canIncrement} onClick={vm.increment}>
        +
      </button>
    </div>
  );
}

 

 

Flux

개인적으로는 MVVM과 Flux를 완전히 분리된 패턴이라기보다,

MVVM 기반 위에서 상태 변경 규칙을 명확히 하기 위해 등장한 구조라고 생각합니다.

 

큰 그림에서 보면,

MVVM의 Model / ViewModel 역할을

Store + Action(Dispatcher) 형태로 재구성한 패턴에 가깝다고 느꼈어요.

 

  • 단방향 데이터 흐름
  • 상태 변경은 반드시 Action을 통해서만
// counter.store.ts
import { create } from "zustand";

type CounterState = {
  count: number;
  canIncrement: boolean;
};

type CounterActions = {
  increment: () => void;
};

export const useCounterStore = create<CounterState & CounterActions>((set, get) => ({
  count: 0,
  canIncrement: true,

  increment: () => {
    const { count } = get();
    if (count >= 10) return;

    set({
      count: count + 1,
      canIncrement: count + 1 < 10,
    });
  },
}));
// view
function Counter() {
  const count = useCounterStore((state)=>state.count);
  const increment = useCounterStore((state)=>state.increment); // action

  return (
    <div>
      <p>{vm.count}</p>
      <button onClick={vm.increment}>
        +
      </button>
    </div>
  );
}

 

FSD 패턴에서 View Model?

저는 이전 직장부터 FSD(Feature-Sliced Design) 아키텍처를 사용해 왔습니다.

Page → Widgets → Features → Entities → Shared로 이어지는 명확한 계층 구조와

각 계층 간 참조 위계가 존재한다는 점에서,

자연스럽게 결합도를 낮춘 상태로 개발할 수 있다는 점이 큰 장점이라고 느꼈습니다.

 

특히 이전 프로젝트가 대규모였고,

하나의 애플리케이션 안에 여러 도메인과 기능이 공존하는 구조였기 때문에

기능 단위로 계층을 나누어 관리할 수 있다는 점은 FSD의 분명한 이점이었습니다.

 

다만, FSD를 사용하며 계속해서 들었던 고민 중 하나는

“계층별로 View Model을 따로 두는 것이 맞을까?”라는 질문이었습니다.

 

주로 저는 Entities 계층에

도메인 중심의 View Model을 추상화하고,

그에 대한 Action을 정의하여 FeaturesWidgets 계층까지 사용하는 방식을 택해왔습니다.

 

하지만 개발을 하다 보면,

특정 Feature에만 한정되는 Action이 필요한 경우가 종종 발생합니다.

이때마다 해당 Action을 Entities에 정의하는 것이 과연 맞는지에 대한 의문이 들었습니다.

 

계층마다 Action과 View Model을 세밀하게 쪼개기 시작하면

모델이 불필요하게 분산되고,

오히려 추상화의 밀도가 낮아지는 문제가 발생합니다.

 

이러한 이유로,

Feature에만 국한된 동작들은

간단한 hook 형태로 Features 계층에 두는 선택을 하기도 했습니다.

 

다만 이 방식이

과연 구조적으로 좋은 아키텍처인지에 대해서는

최근까지도 계속 고민하고 있는 부분입니다.

 

아직 이 고민에 대한 명확한 결론을 내리지는 못했지만,

View Model과 Action의 위치를 어디까지 끌어올릴 것인지에 대한 고민 자체가

아키텍처 설계에서 중요한 지점이라고 생각합니다.

 

이 고민에 대한 생각이 조금 더 정리되면,

다시 한 번 업데이트해보려 합니다.

 


이번 포스팅에서 가장 많이 등장한 말은 "추상화"인 것 같네요.

추상화라는 개념이 이론 속에 머무르는 말에 가깝게 느껴졌는데요,

여러 시행착오들을 겪어보면서 추상화의 중요성을 뼈저리게 느끼게 되었습니다.

 

그럼에도 추상화란 아직도 매번 어렵게 느껴집니다.

UI와 백엔드 그 사이에서 양쪽 모두를 수용할 수 있는 모델을 고민해야 하니까요.

 

그래도 고민을 하는 코드와 안하는 코드는 시간이 지날수록 더 크게 드러난다 생각하며,

계속 부딪히고 고민하며 조금씩 더 나은 설계 방법을 터득해 나가겠습니다.

 

감사합니다-

 

 

참고 자료

 

https://gangdonggil.tistory.com/133

https://be-a-weapon.tistory.com/entry/%ED%94%8C%EB%9F%AD%EC%8A%A4Flux-%ED%8C%A8%ED%84%B4

https://de-velop-humus.tistory.com/29

https://stonesy927.tistory.com/279

반응형