>> 참조: React: CRA(create-react-app)+Mobx 사용

 

앞 글에서 CRA React 앱에 Mobx를 적용하는 방법을 알아봤고, 이 방법을 이용하여 Visual Studio 2019의 ASP.NET Core 웹 애플리케이션에 React + Mobx로 상태를 관리하는 예제 코드를 만들어 봤다. Redux는 사용 템플릿이 있지만 Mobx는 없는 관계로... 직접 샘플 코드를 만들어보는 것이 의미가 있을 듯 싶어서. (기본 생성 코드는 ASP.NET Core 웹 애플리케이션에서 React 템플릿을 선택하면 생성되는 WeatherForcast ClientApp 기반이다.)

 

 

 

[./components/FetchData.js 원본]

import React, { Component } from 'react';

export class FetchData extends Component {
  static displayName = FetchData.name;

  constructor(props) {
    super(props);
    this.state = { forecasts: [], loading: true };
  }

  componentDidMount() {
    this.populateWeatherData();
  }

  static renderForecastsTable(forecasts) {
    return (
      <table className='table table-striped' aria-labelledby="tabelLabel">
        <thead>
          <tr>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
          </tr>
        </thead>
        <tbody>
          {forecasts.map(forecast =>
            <tr key={forecast.date}>
              <td>{forecast.date}</td>
              <td>{forecast.temperatureC}</td>
              <td>{forecast.temperatureF}</td>
              <td>{forecast.summary}</td>
            </tr>
          )}
        </tbody>
      </table>
    );
  }

  render() {
    let contents = this.state.loading
      ? <p><em>Loading...</em></p>
      : FetchData.renderForecastsTable(this.state.forecasts);

    return (
      <div>
        <h1 id="tabelLabel" >Weather forecast</h1>
        <p>This component demonstrates fetching data from the server.</p>
        {contents}
      </div>
    );
  }

  async populateWeatherData() {
    const response = await fetch('weatherforecast');
    const data = await response.json();
    this.setState({ forecasts: data, loading: false });
  }
}

 

 

[./components/FetchData.js 수정본]

import React, { Component } from 'react';
import { observer } from 'mobx-react';
import { weather } from './stores/Weather';

@observer
export class FetchData extends Component {
  //-- @observer와 함께 사용불가(제거)
  //static displayName = FetchData.name;

  //-- 생성자 불필요(제거)
  /*
  constructor(props) {
    super(props);
    this.state = { forecasts: [], loading: true };
  }
  */

  componentDidMount() {
    //-- WeatherStore로 구현 이동 및 호출 render()로 이동 후 제거
    //this.populateWeatherData();
  }

  static renderForecastsTable(forecasts) {
    return (
      <table className='table table-striped' aria-labelledby="tabelLabel">
        <thead>
          <tr>
            <th>Date</th>
            <th>Temp. (C)</th>
            <th>Temp. (F)</th>
            <th>Summary</th>
          </tr>
        </thead>
        <tbody>
          {forecasts.map(forecast =>
            <tr key={forecast.date}>
              <td>{forecast.date}</td>
              <td>{forecast.temperatureC}</td>
              <td>{forecast.temperatureF}</td>
              <td>{forecast.summary}</td>
            </tr>
          )}
        </tbody>
      </table>
    );
  }

  render() {
    //-- 1. mobx-persist hydrate 되지 않은 경우 기다림
    //-- 2. mobx-persist hydrate 완료인데 데이터가 없는 경우(최초) 데이터 로딩
    if (weather.hydrated && (!weather.forecasts || weather.forecasts.length === 0)) {
      weather.populateWeatherData();
    }

    let contents = /*this.state.loading*/ weather.loading
      ? <p><em>Loading...</em></p>
      : FetchData.renderForecastsTable(/*this.state.forecasts);*/ weather.forecasts);

    return (
      <div>
        <h1 id="tabelLabel" >Weather forecast</h1>
        <p>This component demonstrates fetching data from the server.</p>
        {contents}
        <p><button onClick={() => weather.reset()}>reset</button></p>
      </div>
    );
  }

  //-- WeatherStore로 구현 이동
  /*
  async populateWeatherData() {
    const response = await fetch('weatherforecast');
    const data = await response.json();
    this.setState({ forecasts: data, loading: false });
  }
  */
}

 

코드가 많이 바뀌었다. 날씨 데이터를 가져오는 부분을 별도 stores/Weather.js 파일로 분리하고, Mobx를 통한 상태 관리가 어떻게 되는지 쉽게 파악하기 위해 FetchData 컴포넌트에서는 아예 state 사용 코드를 제거했다. (이렇게 해도 Mobx로 구현하면 어차피 내부적으로는 state로 생성되어 관리된다. 상태 관리 코드의 일관성을 위해 이렇게 구현할 수도 있다는 정도만 참고할 것.)

 

 

[./components/stores/Weather.js]

import { observable, action, runInAction } from 'mobx';
import { persist, create } from 'mobx-persist';
import localForage from 'localforage';
import axios from 'axios';

class WeatherStore {
  @persist('list')
  @observable
  forecasts = [];

  @observable
  loading = true;

  //-- mobx-persist 로컬 저장소 읽음 처리 여부 속성 추가
  @observable
  hydrated = false;

  constructor() {
    //-- mobx-persist hydrate()로 저장소에서 데이터 획득
    const hydrate = create({
      storage: localForage, // AsyncStorage for RN
      jsonify: true // true for RN or saving objects
    });

    hydrate('weather', this)
      .then(() => {
        this.hydrated = true;
        this.loading = false;
      })
      .catch(e => {
        console.warn("WeatherStore hydrate error", e)
      });
  }

  @action
  populateWeatherData(callback) {
    //-- fetch()를 axios()로 변경
    //const response = await fetch('weatherforecast');
    //const data = await response.json();
    axios.get('weatherforecast')
      .then(response => {
        runInAction(() => {
          this.forecasts = response.data;
          this.loading = false;
          if (callback) callback.call(this);
        });
      });
  }

  @action
  reset() {
    this.forecasts = [];
    this.loading = true;
  }
}

let store = new WeatherStore();
export { store as weather, store as weatherStore };

 

@observable과 @observer 뿐만 아니라, 상태 변경의 일관성 유지를 위해 @action 및 runInAction( )까지 사용했고,

fetch( ) 내장함수 대신 axios 패키지 함수를 사용해서 REST API 호출을 바꿔봤다. 사용법이 크게 다르지는 않다.

 

위 예제에서는 추가로 Mobx-Persist 패키지로 오프라인 데이터 저장까지 구현해 봤다. 즉, forecast 배열은 기존 데이터가 비어있는 경우에만 서버에서 새로 가져오고, 한번 생성된 이후에는 다시 가져오지 않고 로컬 저장소에 있는 데이터를 그대로 보여주도록 구현했다. 여기에서 hydrate( ) 함수와 hydrated 속성 사용 부분이 중요한 내용인데, 로컬 저장소에서 최초 데이터를 꺼내 오는 행위를 hydrate라고 하며 이 행위에 걸리는 시간이 일정 소요되기 때문에(React에서는 밀리초, React Native에서는 컴포넌트별로 1~2초 정도씩 걸림) 로딩이 완료되었는지 판단하는 로직이 꼭 들어가야 한다.

 

>> 참조1: www.npmjs.com/package/mobx-persist

 

mobx-persist

create and persist mobx stores

www.npmjs.com

 

>> 참조2: medium.com/@Zwenza/how-to-persist-your-mobx-state-4b48b3834a41

 

How to Persist Your MobX State

This is how I manage and persist the state of Avocation a React Native App I built.

medium.com

 

 

 

[참고]

 

1. React 앱에서는 mobx-persist로 localStorage에 저장하는 코드 구현이 별 의미없는 것 같다. 오프라인 저장 기능이므로 한번 저장되면 계속 유지되어야 할 것 같은데, 크롬에서만 유지되고 Edge에서는 유지가 되지 않는다. 브라우저별로 동작이 다르면 사실상 쓸모가 없다고 봐야... => jsonify를 true로 주면 Edge에서도 오프라인 데이터가 유지된다. jsonify가 false일 때는 Edge에서만 유지된다(Edge 외 다른 브라우저로는 테스트 안 해 봤음).

 

2. localStorage말고 좀더 성능이 개선된 localForage라는 패키지를 추가해서 대신 사용할 수 있는데... 최신버전이든 옛날버전이든 put( ) 부분에서 오류가 나서 저장이 되지 않는다. => 역시 jsonify를 true로 주어야 오류 없이 동작하며, Edge 브라우저에서도 크롬과 동일하게 오프라인으로 유지가 된다!

 

>> 참조: localforage.github.io/localForage/

 

localForage

 

localforage.github.io

localForage 사용 시 특이한 점이 발견되었다: localForage는 export default new LocalForage();로 Singleton을 만들어 내보내도록 모듈에 정의되어 있기 때문에 import 시 named import를 하든 default import를 하든 다 잘 동작한다. 그런데! 위 예제 코드에서와 같이 default import를 하면 웹 브라우저(크롬/Edge 동일)의 indexedDB에 저장이 되고, 괄호{ }를 써서 named import를 하면 localStorage에 저장이 된다. 진짜 희한하다. 왜 이렇게 다르게 동작하는지 이유를 모르겠다.

 

3. 패키지, 모듈의 import/export 구문 사용이 좀 헷갈리면서 어렵다. 어떨 때는 import React from ... 이렇게 괄호 없이 쓰고, 어떨 때는 import { Component } from ... 이렇게 괄호 안에 넣어서 쓰는데, 그 이유가 무엇일까? 그리고 export 할 때도 default를 쓸 때가 있고 default 없이 쓸 때가 있는데 그 차이점은 무엇일까? 또 new 키워드로 새 인스턴스를 만들어서 export 할 때도 있고, 그냥 export 할 때도 있는데 그 의미는 무엇일까? 해답은 아래 사이트에 있다. 찬찬히 잘 읽어보면 큰 도움이 된다.

 

>> 참조: Modules • JavaScript for impatient programmers (ES2020 edition) (exploringjs.com)

 

Modules • JavaScript for impatient programmers (ES2020 edition)

(Ad, please don’t block.) 24 Modules 24.1 Overview: syntax of ECMAScript modules 24.1.1 Exporting // Named exports export function f() {} export const one = 1; export {foo, b as bar}; // Default exports export default function f() {} // declaration

exploringjs.com

 

요약하면 아래와 같다.

 

  1. default export
    • export default … => import NAME from… (O: normal default import)
    • export default … => import { NAME } from… (X: named import 사용 불가)
    • export default … => import { default as NAME } from… (O: default import and rename)
  2. named export
    • export NAME … => import NAME from… (X: normal default import 사용 불가)
    • export NAME … => import { NAME } from… (O: named import)
    • export NAME … => import { NAME as NAME2 } from… (O: named import and rename)
  3. multiple named export
    • export { NAME1, NAME2 }; => import { NAME1 } from… (O: named import)
    • export { NAME1, NAME2 }; => import { NAME1 as NAME3, NAME2 } from… (O)
    • export { NAME1, NAME2 }; => import NAME1 from… (X: normal default import 사용 불가)
  4. default singleton export
    • export default new NAME(); => import NAME from... (O: default import)
    • export default new NAME(); => import NAME2 from... (O: default import renamed)
    • export default new NAME(); => import { NAME } from... (O: named import)
    • export default new NAME(); => import { NAME2 } from... (O: named import renamed)

 

 

끝.

 

 



Posted by 떼르미
,


자바스크립트를 허용해주세요!
Please Enable JavaScript![ Enable JavaScript ]