Recoil로 MVVM 사용하기 - (1) MVVM

towel1017·2021년 6월 2일
19

MVVM

목록 보기
1/2


Recoil을 배운지도 얼마 되지 않은 상태에서 디자인 패턴에 관심을 들이기 시작했다.
MVVM에 대해선 전부터 (Model-View-ViewModel) 인건 알고는 있었지만, 제대로 공부해본 적은 한번도 없어서 막 배우기 시작한 Recoil과 함께 적용하면서 겪었던 것에 대해 말해보려한다.

처음엔 React + MVVM의 구조와 적용방법 등에 대해서 다루고 다음 편에서 Recoil에 관해서 더 자세히 다룰 예정이다.

(react + recoil + typescript 로 작성한 문서이므로 기본 MVVM 구조와는 다를 수 있습니다.)

🧃 MVVM? 이름만 아는데..?

MVVM이라는 디자인 패턴은 나온지 사실 생각보다 오래됬다. (1990년대 MVP에서 개량)
Model - View - ViewModel 이라는 것이고, 말 그대로 저 3개의 토픽으로 나뉘어 있다는 것인데, 각각의 역할과 각 토픽간의 연결이 가장 중요한 개념이다.

🖥 1. View

View사용자에게 보여지는 부분만을 작성한 곳입니다.
React에 경우 Presentational Component와 흡사합니다.
View 는 철저하게 보여지는 부분만 작성하므로, 필요한 것들은 모두 Props로 받아오고 화면을 렌더링 합니다.

const DeliveryMethod: React.FC<Props> = props => {
  const {
    deliveryMethodList,
    selectedMethod,
    isDropdown,
    toggleDropdown,
    handleChangeDeliveryMethod,
  } = props;
  return (
    <>
      <S.DeliveryMethodWrapper>
        <S.SubTitleText>방법 리스트</S.SubTitleText>
        <S.DropdownDefaultWrapper onClick={toggleDropdown}>
          <span>{selectedMethod}</span>
          <I.DropdownArrow width={20} height={20} />
        </S.DropdownDefaultWrapper>
        <S.DropdownMenuWrapper>
          {isDropdown
            ? deliveryMethodList.map(info => (
                <DeliveryMethodItem
                  {...info}
                  handleChangeDeliveryMethod={
                    handleChangeDeliveryMethod
                  }
                />
              ))
            : null}
        </S.DropdownMenuWrapper>
      </S.DeliveryMethodWrapper>
    </>
  );
};

이처럼 JSX로 작성하고, 필요한 데이터나 함수들을 모두 Props로 받아와 작성합니다.

필요에 따라 View 내에 있는 UI에 관련된 함수나, Input 관련의 부분을 ViewController라고 따로 분류해서 쓰기도 합니다. (이렇게 쓰는 것도 고려가능.)
프로젝트의 규모가 크거나 조그마한 한 View에서만 사용할 것이라면 Controller로 빼는 것도 나쁘지 않다고 생각합니다.

🔌 2. ViewModel

ViewModelViewModel을 연결해주는 역할을 뜻합니다.
React의 경우 Container Component 와 흡사합니다.
ViewModel은 View 하고도 상호작용하고, Model하고도 상호작용하는 중간점이기 때문에 테스트코드 작성이 쉽습니다.
Model의 데이터가 업데이트 되거나 View 에서 Model에 있는 함수를 호출할 경우 그로 인한 모든 변경 사항들이 자동으로 업데이트 되어 View에 적용됩니다.

const ShopViewModel: React.FC = () => {
  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("주문 완료 로직");
  };
  return (
    <ShopBasketView
      {...price}
      deliveryMethodList={deliveryMethodList}
      selectedMethod={selectedMethod.name}
      productList={productList}
      allChecked={allChecked}
      isDropdown={isDropdown}
      toggleDropdown={toggleDropdown}
      toggleChecked={toggleChecked}
      toggleAllChecked={toggleAllChecked}
      increaseAmount={increaseAmount}
      decreaseAmount={decreaseAmount}
      handleDeleteProductItem={handleDeleteProductItem}
      handleChangeDeliveryMethod={handlechangeDeliveryMethod}
      handleOrder={handleOrder}
    />
  );
};

export default ShopBasketViewModel;

이렇게 Model의 로직을 받아옴과 동시에 View에서 필요한 Props에 연결 시켜주는 역할을 수행합니다.

🗄 3. Model

Model은 그에 맞는 View 와 관련된 데이터 및 로직을 관리하는 역할을 합니다.
명확한 API가 제공되지 않은 상태에서의 DB, 서비스의 모든 데이터를 작성하고 관리합니다.
실제로 모델의 topic에 관한 CRUD 에 관한 로직을 제외한 JSX 등의 다른 로직은 없어야 합니다.

본래는 Class 형식으로 작성하는 것이 보통이지만, 상태관리와 함께 하기 위하여, Recoil 라이브러리의 atom, selector로 구현하였습니다.
Recoil에 관한 내용은 다음 편에서 작성하겠습니다.

Atom

interface DeliveryMethodItemType {
  id: number;
  name: string;
  deliveryPrice: number;
}

export const DeliveryMethodAtom = atom<DeliveryMethodItemType[]>({
    key: "DeliveryMethodState",
    default: [
        ...deliveryMethods
    ]
})

Props중 데이터를 담당하는 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;
            })
        })
    }
})

이런 식으로 Atom이 Class의 Constructor 와 비슷한 역할을 하고,
SelectorConstructor 메서드와 비슷한 역할을 합니다.

🎮 하나하나는 알았는데 그래서 연결은?

각 View - ViewModel - Model 각각의 역할을 알았고, 이제 연결하는 것도 문제이다.

이미지로 만들어본 구현도이다.

  • View 는 자신의 Prop를 바탕으로 ViewModel에서 Props요청(Read)한다.
  • 그럼 ViewModel은 자신이 연결한 useRecoilState 등으로 Modelatom, selector에서 데이터(값) 또는 CRUD 로직을 받아온다.
  • Model에서 받아온 걸로 ViewModelView의 Props에 연결해준다.

🤦‍♂️ 후기

처음 MVVM 자체의 구조 및 연결 방식을 이해하기는 그렇게 어렵지 않지만, 프로젝트에 적용하는 과정에서 디렉토리는 나누는 단위와 MVVM을 쓰면서 OOP의 Solid중 Component의 책임단위라도 확실하게 해야하기 때문에, 파일을 나누는 과정이 힘들었다.
그리고 Hooks와 Recoil 상태관리의 경계가 생각보다 모호했다.
결국 UI 관련, 즉 Controller 역할을 하는 것들을 Hooks로 만들어 넣고,
나머지 데이터나 상태를 Recoil로 구현하였다.

결국 React는 구현방법이 매우 다양할 뿐만 아니라, 방법이 정해져 있는게 아니다보니, 여러 라이브러리나 방식을 이용해서 MVVM의 구현이 가능하다는 것을 알았다.
React의 자유도를 너무 얕보고 있었던 것 같기도...

0개의 댓글