본문 바로가기
FE 톺아보기🔬

MVVM 패턴

by singco 2023. 6. 16.

📖 목차
1. 비즈니스 로직과 프레젠테이션 로직
2. MVVM이란?
3. Recoil을 접목시킨 MVVM 패턴

이번에 collusic-new, about 프로젝트에 mvvm 패턴을 적용하면서 디자인 패턴에 대해 처음 공부하게 되었다. model, view, viewmodel이란 각각의 용어도 생소했지만, 무엇보다 mvvm을 react에 적용시키는 방식이 예제마다 너무 달라서 혼란스러웠다. 또한, 뷰로직과 비즈니스 로직이 정확히 어떤 차이가 있는지 와닿지 않아 프로젝트를 하는 도중, 코드를 분리하면서도 개운치가 못했다. 따라서 오늘 내용은 비즈니스 로직과 프레젠테이션 로직(뷰 로직과 비슷한 개념인 것 같다...)부터 얘기해보려고 한다.

1. 비즈니스 로직과 프레젠테이션 로직

비즈니스 로직?

위키피디아에서 정의하는 비즈니스 로직은 다음과 같다.

비지니스 로직(Business Logic) 또는 도메인 로직(Domain Logic)은현실 세계에서 어떻게 데이터를 만들고 저장하고 바꿀 것인지에 대한 비지니스 규칙(Business Rules)을 인코드(Encodes) 한,소프트 웨어 안의 프로그램의 한 부분이다.
—위키피디아(영문판), Business Logic

비즈니스 로직은 사용자가 원하는 요청에 의해 어떠한 데이터를 어떻게 CRUD 하거나, 가공할 것인지를 말한다.

특정 책에 대한 상세정보를 Read하는 비즈니스 로직을 가진 detailBook(bookId) 만들어보자.

async detailBook(bookId: number) {
    const { data } = await axios.get(`api/books/${bookId}`);
    return data;
  }

위와 같이 간단한 api요청을 통해 데이터를 불러오는 비즈니스 로직도 있지만, 받은 데이터들을 사용자의 입맛에 맞게 가공하는 비즈니스 로직도 존재한다.

사용자가 특정 책을 하트를 눌렀을 때, 사용자 좋아하는 책 목록 데이터에 특정 책을 추가하는 비즈니스 로직과 책 목록을 반환하는 비즈니스 로직을 만들어보자.

public addFavoriteList(bookId: number, bookTitle: string) {
    this.favoriteList.push({ bookId, bookTitle });
}

public getFavoriteList() {
    return this.favoriteList;
}

프레젠테이션 로직?

위키피디아에서 정의하는 프레젠테이션 로직은 다음과 같다.

프레젠테이션 로직은 비지니스 오브젝트(비지니스 로직)가 팝업 화면과 드롭 다운 매뉴 중어떤 것을 선택할 것인지와 같이소프트웨어 사용자에게 표시되는 로직을 말한다. 프레젠테이션 로직에서 비지니스 로직을 분리하는 것은소프트웨어 개발 및 프레젠테이션과 컨텐츠를 분리하려하는 사례의(an instance of) 중요한 관심사이다.
—위키피디아(영문판), Presentation Logic

프레젠테이션 로직(뷰 로직)은 수많은 비즈니스 로직들을 어떻게 보여줄 것인지에 대한 로직이다. 즉, 사용자 인터페이스(UI)를 어떻게 표시할까에 대한 로직이라고 할 수 있다.

기여작 목록을 보여줄 때 사용자의 프로필 사진이 있으면 그 사진을 프로필로, 아니면 미리 저장해놓은 defaultProfile로 보여주는 뷰 로직을 만들어보자.

{contributeList.map((project, idx) => (
  <img
    src={
      project.userProfile !== undefined
        ? project.userProfile
        : `../../assets/defaultProfile/defaultProfile.png`
    }
    alt={project.userEmail}
    className="profile"
  />
))}

2. MVVM이란?

본격적으로 MVVM 패턴에 대한 설명을 하려고 한다. 먼저 MVVM 패턴은 비즈니스 로직과 프레젠테이션 로직을 UI로 부터 분리하기 위해 만들어졌다. 비즈니스 로직과 프레젠테이션 로직을 UI로부터 분리하게 되면 유지보수, 재사용, 테스트가 쉬워진다.

MVVM은 Model, View, ViewModel의 약자이다. 각각의 역할에 대해 알아보도록 하자.

Model

  • 프로그램에서 사용되는 실제 데이터가 들어있고, api를 요청하거나, 데이터를 업데이트하는 비즈니스 로직이 있다. 즉, 사용할 데이터에 관련된 동작과 데이터를 다룬다.
class BookModel {
    constructor() {
      books = [
        {id: 'RCB-123',name: "React Cook Book", isFavorite: false},
        {id: 'VCB-123',name: "Vue Cook Book", isFavorite: false},
        {id: 'ACB-123',name: "Angular Cook Book", isFavorite: false}
      ];
    }
    
    getBooks() {
        return this.books
    }

    toggleFavorite(bookId) {
      const target = this.books.filter(item => item.id === bookId)[0];
      target.isFavorite = !target.isFavorite
    }
}

View

  • 사용자에게 보여지는 부분으로 UI와 관련된 것을 다루는 부분이다.
  • 비즈니스 로직은 없고, 뷰 로직만이 존재한다.
  • View는 ViewModel를 지속적으로 관찰한다.

ViewCotroller을 만들어 View에서 뷰 로직을 분리시키는 방법도 있는데, 더 헷갈릴 수 있으므로 아래 예제에서는 분리시키지 않도록 하겠다.

const BookView = ({bookViewModel}) => { 
    const [isNeverView, setIsNeverView] = useState(false);

    const handleToggleFavorite = useCallback((bookId) => {
      bookViewModel.toggleFavorite(bookId)
    }, [viewModel]);

    const handleClickNeverView = useCallback(() => {
      setIsNeverView(!isNeverView);
    }, [isNeverView]);
    
    return (
      <BookList 
        books={bookViewModel.getBooks()} 
        handleToggleFavorite={handleToggleFavorite}
        handleClickNeverView={handleClickNeverView}
      />
    )
}

ViewModel

  • View와 Model을 연결해주는 역할이다.
  • Model의 데이터가 업데이트되거나 View에서 Model에 있는 함수를 호출할 경우 그로인한 모든 변경 사항들이 자동으로 업데이트 되어 View에 적용된다.
  • View Model은 View 와 1:N의 형태를 이루고 있다.
class BookViewModel {
    constructor(bookStore) {
        this.store = bookStore
    }

    getBooks() {
        return this.store.getBooks()
    }

    toggleFavorite(bookId) {
       this.store.toggleFavorite(bookId)
    }
}

View ↔ ViewModel, ViewModel ↔ Model 이어주기

  • ViewModel을 인스턴스화하고 필요한 모든 의존성 주입을 담당한다.
  • ViewModel의 인스턴스는 props를 통해 View로 전달된다.

Provider를 만들어 서로를 이어주는 구성체를 형성할 수 있는데 아래 예제에서는 분리시키지 않도록 하겠다.

import React from 'react';

import BookViewModel from './viewModel/BookViewModel'
import BookModel from './model/BookModel'
import BookView from './view/BookView'

function App() {
  const bookModel = new BookModel()
  const bookViewModel = new BookViewModel(bookModel)

  return (
    <>
      <BookView 
        bookViewModel={bookViewModel}
      />
    </>
  )
}

export default App;

3. Recoil을 접목시킨 MVVM패턴

recoil은 state를 구독하는 컴포넌트에 한하여 state가 새로운 값으로 리렌더링이 되는 상태관리 라이브러리이다. recoil에 관해서는 다음에 자세히 설명해보도록 하겠다.

Recoil로 MVVM을 사용하는 예제는 많지가 않다. 예제를 따라하면서 ViewModel에서 뷰 로직이 들어가면서 View와 ViewModel의 구분이 확실치 않다는 느낌을 받았다. 아래에서는 Recoil이 적용된 Model과 내가 석연치 않게 생각했던 부분에 대해 얘기해보려 한다.

Model

recoil에서는 데이터의 상태를 atom으로 관리하고, atom의 상태값을 이용해 데이터를 가공한 상태를 selector로 관리한다. CRUD와 데이터 가공에 대한 비즈니스 로직 모두 recoil의 atom, selector를 통해 이루어진다.

export const AllCheckProductItemsSelector = selector({
    key: "ProductList/Item/AllCheck",
    get: ({ get }) => {
        const productList = get(ProductListAtom)
        if (productList) return false;
        return get(ProductListAtom).every(item => item.checked)
    },
    set: ({ set }) => {
        set(ProductListAtom, prevState => prevState.map(item => {
                const currentItem = { ...item };
                currentItem.checked = !currentItem.checked;
                return currentItem;
            })
        )
    }
})

export const CheckProductItemSelector = selector({
    key: "ProductList/Item/Check",
    get: ({ get }) => { },
    set: ({ get, set }, product_id) => {
        set(ProductListAtom, prevState => {
            return prevState.map(item => {
                 const currentItem = {...item}
                    if (item.id === product_id) {
                        currentItem.checked = !currentItem.checked
                    }
                return currentItem;
            })
        })
    }
})

ViewModel에서 뷰로직이??

MVVM예제에서 ViewModel은 의존성 주입을 통해 View와 Model을 연결해주는 역할을 담당한다. 이 구성체에서는 뷰로직도, 비즈니스 로직도 들어가지 않는다. 하지만 타 블로그에서의 ViewModel예제를 보면 뷰 로직이 들어가 있는 것을 알 수 있다.

const [deliveryMethodList] = useRecoilState(DeliveryMethodAtom);
const [selectedMethod] = useRecoilState(SelectedMethodAtom);
const [productList] = useRecoilState(ProductListAtom);
const price = useRecoilValue(PriceStatsSelector);
const [, toggleChecked] = useRecoilState(CheckProductItemSelector);
const [allChecked, allCheckedProductItem] = useRecoilState(
  AllCheckProductItemsSelector
);
const [, increaseAmount] = useRecoilState(
  IncreaseProductItemAmount
);
const [, decreaseAmount] = useRecoilState(
  DecreaseProductItemAmount
);
const [, handleDeleteProductItem] =
  useRecoilState(DeleteProductItem);
const [, changeDeliveryMethod] = useRecoilState(
  ChangeDeliveryMethod
);
const { isDropdown, toggleDropdown } = useDropdown();
const toggleAllChecked = () => {
  allCheckedProductItem(allChecked);
};
const handlechangeDeliveryMethod = (method_id: number) => {
  changeDeliveryMethod(method_id);
  toggleDropdown();
};
const handleOrder = () => {
  console.log("주문 완료 로직");
};

앞으로 collusic-new 프로젝트 리팩토링을 과정에서 recoil을 사용한 ViewModel과 View를 만들어보려고 한다.


참조
[ Eassy - Technology, IT, Web ] 비지니스 로직(규칙, 층) 과 프레젠테이션 로직(규칙, 층) 이란 무엇인가?
React에서 MVVM 패턴 알아보기
로직을 UI로부터 분리하는 MVVM Architecture Pattern
MVVM 디자인패턴
[TIL]React와 MVVM패턴
Recoil로 MVVM 사용하기 - (1) MVVM

'FE 톺아보기🔬' 카테고리의 다른 글

CSR vs SSR  (0) 2023.06.30
옵저버패턴(Observer Pattern)  (0) 2023.06.16
제너레이터(Generator)  (0) 2023.06.16
익명함수(Anonymous Function)  (0) 2023.06.16
CommonJS와 ES Modules은 왜 함께 할 수 없는가?  (0) 2023.06.16