본문 바로가기
  • 삽질하는 자의 블로그
React/React-TypeScript

13. React 에서 MVVM 모델 구현해보기. 이거 맞나? 틀린 거 같음... Redux-toolkit 없이

by 이게뭐당가 2023. 1. 16.

과제가 있다. MVVM 모델을 구현하여, REACT 앱을 만들어보는 과제였다.

 

우선 내가 아는 MVVM 모델을 생각해보면...

1. VIEW 에서 ACTION 을 VM 에 날린다.
2. 커맨드 패턴으로 VM 에 ACTION 을 전달한다.
3. VM 은 MODEL 에게 데이터를 요청한다.
4. MODEL 은 VM 에게 데이터를 응답한다.
5. VM 은 응답받은 데이터를 처리한다.
6. VM 은 VIEW 와 BIDING 하여 화면에 나타낸다.

이때, VM 은 VIEW 와 바인딩 되어있기에, 바로 처리가 된다.

출처 : https://velog.io/@k7120792/Model-View-ViewModel-Pattern
출처 : https://beomy.tistory.com/43

하여튼 그래서 어떻게 해보았냐면...

 

1. MODEL (App.tsx)

모든 Data 를 저장하는 저장공간을 만들었다.

Redux-toolkit 을 사용하지 않고 하려니 고통이 이만저만 삼만사만이 아니었다.
import React, { useEffect, useState } from "react";

import { KrweurType, intialState, getEurInfo } from "./helper/get-euro-info";
import EuroToKrwController from "./view-models/euro-to-krw";

export const App = () => {
  const [isReady, setReady] = useState<boolean>(false);
  const [eurInfo, setEurInfo] = useState<KrweurType>(intialState);
  const [inputedEuro, setInputedEuro] = useState<number>(0);
  const [exchangedWon, setExchangedWon] = useState<number>(0)


  const getEuroInfoState = (euroInfoState: KrweurType) => {
    setEurInfo(() => euroInfoState);
  };
		...
  const getExchangedWon = (exchangedWon : number) => {
    setExchangedWon(()=> exchangedWon)
  }

  return (
    <div className="App">
      <EuroToKrwController
        getReadyState={getReadyState}
			...
        inputedEuro={inputedEuro}
        exchangedWon={exchangedWon}
      />
    </div>
  );
};

export default App;

모든 데이터를 저장하고, 컨트롤러인 Veiw-Model 로 "모든 값"과 "데이터를 가져오는 함수" 를 보냈다.

이것으로, Redux-toolkit 에서 사용하던 "구독" 을 구성해보았다.

이제, Model 은 VM 과 연결되었다.

 

 

2. VM(euro-to-krw.tsx)

모든 값을 받는 중간역할을 하게 만들었다.
MODEL 인 APP.TSX 와는 연결했으니, VIEW 로 그 값을 보내고,
VIEW 에서 보내주는 데이터를 받아, "처리" 하여 MODEL 을 업데이트 하는 역할을 시켰다.

MODEL 로 값을 보내기 위해, MODEL 로 "STATE 끌어올리기" 를 사용한다.
(MODEL 에서 GET 할 함수를 View-Model 로 보내주었음)
import { useEffect } from "react";
import { getEurInfo, KrweurType } from "../helper/get-euro-info";
import Information from "../views/Information";
import LoadingSpinner from "../views/Loading-spinner";
import InputPrice from "../views/Input-price";

const EuroToKrwController = (props: {			// MODEL 에서 오는 모든 값과, MODEL 에서 데이터 받아갈 함수들
  getReadyState: (readyState: boolean) => void;
  getEuroInfoState: (euroInfoState: KrweurType) => void;
		...
  inputedEuro: number;
  exchangedWon: number;
}) => {
  const {
    getReadyState,
    getEuroInfoState,
		...
    inputedEuro,
    exchangedWon,
  } = props;

  useEffect(() => {
    getReadyState(true); 			// "로딩 STATE 를 모델로 넘김"
    (async (): Promise<void> => {
      const fetchedData = await getEurInfo(); 	// 데이터 페칭
      getEuroInfoState(fetchedData); 		// "데이터 모델로 넘김"
      getReadyState(false); 			// "로딩 STATE 를 모델로 넘김"
    })();
    return () => {};
  }, []);

  const exchangeEuroToKrw = (euro: number) => {		// VIEW 의 값을 받아와 처리하는 로직
    getEuroInputState(euro);
    getExchangedWon(euro * eurInfo.basePrice);		// VIEW의 값이 가공되면 다시 MODEL 로 보낸다.
  };

  return (
    <div>
      <Information eurInfo={eurInfo} />		// "VIEW 를 연결하고, VIEW에게 MODEL 에서 오는 값을 보낸다."
      <hr />
      {isReady && <LoadingSpinner />}
      <InputPrice
        exchangeEuroToKrw={exchangeEuroToKrw}
        inputedEuro={inputedEuro}
        exchangedWon={exchangedWon}
      />
    </div>
  );
};
export default EuroToKrwController;

 

3. VIEW ( 각 컴포넌트 )

VIEW 에서는 오로지 UI 만을 가지며,
ACTION(VM 에서 온 처리 로직) 을 VM 에 보낸다.
또한 VM 에서 가져온 최신 MODEL 데이터를 받고,
VM 으로는 자신의 최신 데이터를 보낸다

"양방향 바인딩" 을 이렇게 구현해보았다.
 <Information.tsx>
        import { Fragment } from "react";
        import { KrweurType } from "../helper/get-euro-info";

        const Information = (props: { eurInfo: KrweurType }) => {
          const { eurInfo } = props;
          return (
            <Fragment>
              <div>환율기준 (1 유로)</div>
              <div>
                {eurInfo.basePrice}
                {eurInfo.basePrice - eurInfo.openingPrice > 0 && "▲"}
                {eurInfo.basePrice - eurInfo.openingPrice < 0 && "▼"}
                {eurInfo.changePrice}원 (
                {(eurInfo.changePrice / eurInfo.basePrice) * 100}%)
              </div>
              <div>
                <div>살때 : {eurInfo.cashBuyingPrice}</div>
                <div>팔때 : {eurInfo.cashSellingPrice}</div>
                <div>보낼때 : {eurInfo.ttSellingPrice}</div>
                <div>받을때 : {eurInfo.ttBuyingPrice}</div>
              </div>
            </Fragment>
          );
        };
        export default Information;

 
<Input-price.tsx>

         import { Fragment } from "react";

            const InputPrice = (props: {
              exchangeEuroToKrw: (krw: number) => void;
              inputedEuro: number;
              exchangedWon: number;
            }) => {
              const { exchangeEuroToKrw, exchangedWon, inputedEuro } = props;

              const commaEuro = inputedEuro.toLocaleString("en-US", {
                maximumFractionDigits: 2,
              });

              const commaWon = exchangedWon.toLocaleString("ko-KR", {
                maximumFractionDigits: 0,
              });

              return (
                <Fragment>
                  <input
                    type={"number"}
                    onChange={(e) => exchangeEuroToKrw(+e.target.value)}
                  />
                  <div>
                    <p> 인풋된 유로</p>
                    <p>{commaEuro} </p>
                  </div>
                  <div>
                    <p>유로 ▶︎ 원 </p>
                    <input type={"text"} disabled value={commaWon} /> 원
                  </div>
                </Fragment>
              );
            };
            export default InputPrice;

 

이게 정확히 맞는지는 모르겠다...

MVVM 디자인 패턴.. 어렵다...

 

아, 추가적으로 fetch 를 대신 해줄 helper 함수를 만들었다.

HELPER

export const getEurInfo = async ():Promise<KrweurType> => {
  try {
    const response = await fetch(
      "https://quotat...EUR"
    );

    if (!response.ok) {
      throw new Error("failed to fetch data");
    }
    const responseData = await response.json();
    const krweurData = responseData[0];
    return krweurData;
  } catch (error) {
    throw new Error("failed to fetch data");
  }
};

export interface KrweurType {
  code: string;
  currencyCode: string;
  currencyName: string;
  country: string;

댓글