본문 바로가기
개발/React

React Handsontable로 csv 편집기 만들기 (17)

by 피로물든딸기 2021. 6. 26.
반응형

프로젝트 전체 링크

 

이전 - (16) colWidths 옵션 / csv 파일 파싱 보완
현재 - (17) redux로 File Upload 상태 관리하기

다음 - (18) File Upload에 대한 이벤트 보완

 

깃허브에서 코드 확인하기


현재 csv 편집기에는 아래의 문제가 있다.

 

1) file을 불러와도 drag & drop 영역이 사라지지 않는다.

2) DOWNLOAD 버튼/displayCell은 file이 업로드 된 후에 필요하다. (즉, MyTable은 file이 업로드 된 후 보인다.)

 

이 문제들을 보완하기 위해 fileUpload의 상태를 관리하는 flag가 필요하다.

FileUpload에서는 업로드 영역이 사라져야하고, MyTable이 보여야하므로

상위 component인 App.js에서 flag를 관리해야 한다.

 

하지만 만약에 더 아래의 component에서도 FileUpload Flag를 관리해야 한다면,

불필요한 하위 component에도 flag를 넘겨야하고, 계속해서 props를 하위로 넘겨줘야하는 불편함이 생긴다.

 

따라서 Redux를 이용하여 FileUpload Flag를 관리해보자.

Redux를 이용하면 컴포넌트끼리 같은 상태를 공유할 때, 여러 컴포넌트를 거치지 않고 상태를 관리할 수 있다.


redux를 설치하자.

npm install redux
npm install react-redux

 

redux를 적용하기 위해 index.js를 아래와 같이 변경해야 한다.

//index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import {Provider} from "react-redux"; /* store subscribe */
import store from './components/store';

ReactDOM.render(
  <Provider store={store}> /* store subscribe */
    <App />
  </Provider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

위의 코드에서 store를 등록하였다.

스토어는 리덕스를 적용한다. 한 개의 프로젝트는 단 하나의 스토어만 가진다.

import store from './components/store';

 

따라서 임시로 아래의 store를 components 폴더 아래에 만든다.

이 store에서는 flag를 끄거나 켜기만 한다.

즉, File이 업로드가 완료되면 flag가 on이 되고, 업로드가 되지 않은 경우에는 off가 되도록 한다.

//store.js

import { createStore } from "redux";

const FLAG_ON = "FLAG_ON";
const FLAG_OFF = "FLAG_OFF";

const reducer = (state = [], action) => {
  switch (action.type) {
    case FLAG_ON: /* File Upload 성공 */
      return true;
    case FLAG_OFF: /* File Upload 전 */
      return false;
    default:
      return state;
  }
};

/* subscribe */
const store = createStore(reducer);

export default store;

 

여기서 reducer가 가지는 Action은 FLAG를 끄거나 켜는 ON/OFF 액션을 가진다.

 

Redux test를 위해 아래의 component 폴더 아래에 컴포넌트를 만든다.

import React from 'react';

const ReduxTest = () => {
    return (
        <div>
            ReduxTest
        </div>
    );
};

export default ReduxTest;

 

App.js에 ReduxTest를 연결하고 나머지는 잠시 주석처리하자.

//App.js
import React, { useState } from "react";

//import * as lib from "./components/library";
import FileUpload from "./components/FileUpload";
import MyTable from "./components/MyTable";

import ReduxTest from "./components/ReduxTest";

const csvObjectDefault = {
  HEIGHT: 0,
  WIDTH: 0,
  csv: [],
};

const App = () => {
  const [csvObject, setCsvObject] = useState(csvObjectDefault);
  return (
    <div>
      <ReduxTest/>
      {/* <button onClick={() => console.log(csvObject)}>print csv</button>
      <div className="App">
        <MyTable csvFile={csvObject}/>
        <FileUpload setCsvObject={setCsvObject} />
      </div> */}
    </div>
  );
};

export default App;


Action Creator

 

flag on/opff 액션 생성 함수를 만들자. 액션 생성 함수(action creator)는 액션 객체를 만들어주는 함수다.

const FLAG_ON = "FLAG_ON";
const FLAG_OFF = "FLAG_OFF";

const flagOn = () => {
  return {
    type: FLAG_ON,
  };
}

const flagOff = () => {
  return {
    type: FLAG_OFF,
  };
}

const reducer = (state = [], action) => { ... };

 

만든 action creator 함수를 외부에서 쓸 수 있도록 export 한다.

export const actionCreators = {
  flagOn,
  flagOff
}

export default store;

이제 ReduxTest에 store를 연결하자.

store에서 actionCreators를 import하고, 가장 아래에 ReduxTest를 connect한다.

//ReduxTest.js

import { connect } from "react-redux";
import { actionCreators } from "./store";

...

export default connect(mapStateToProps, mapDispatchToProps)(ReduxTest);

 

connect를 해서 components를 store에 연결할 수 있게 되었다.

즉, 현재 ReduxTest props에 추가될 수 있도록 허용한다.

const ReduxTest = ({ fileUploadFlag }) => { ... }

 

mapStateToProps는 현재 state를 store로부터 가져온다.

mapDispatchToProps는 store의 상태를 바꾸기 위해 dispatch한다.

 

mapStateToProps는 현재 state를 return 받기를 원한다.

state는 redux store에서 온 state이고, ownProps는 component의 props이다.

function mapStateToProps(state, ownProps) {
    return { fileUploadFlag : state };
}

 

예를 들어 store.js에서 reducer의 state를 아래와 같이 고쳐보자.

const reducer = (state = ["hello world"], action) => { ... }

 

mapStateToProps에 log를 추가하면 아래의 결과를 얻을 수 있다.

function mapStateToProps(state , ownProps) {
    console.log(state);

    return { fileUploadFlag: state }; /* state를 return 받길 원한다 */
}

store의 초기 state가 component에 연결된 것을 확인하였다.

 

mapDispatchToProps는 actionCreators의 액션 함수를 dispatch한다. 

function mapDispatchToProps(dispatch, ownProps) {
    return {
        flagOn : () => dispatch(actionCreators.flagOn()),
        flagOff : () => dispatch(actionCreators.flagOff()),     
    }  
}

 

ReduxTest에서 flagOn과 flagOff를 쓸 수 있게 되었다.

const ReduxTest = ({ fileUploadFlag, flagOn, flagOff }) => { ... }

 

이제 flagOn과 flagOff를 테스트하기 위해 코드를 아래와 같이 바꿔보자.

flag를 보여주는 버튼, on/off로 변경하는 버튼을 추가하였다.

//ReduxTest.js

import React from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";

const ReduxTest = ({ fileUploadFlag, flagOn, flagOff }) => {
    const onClickShowFlag = () => {
        console.log(fileUploadFlag);
    }

    const onClickFlagOn = () => {
        flagOn();
    }

    const onClickFlagOff = () => {
        flagOff();
    }

    return (
        <div>
            <div>Redux Test</div>       
            <button onClick={onClickShowFlag}>flag state</button>
            <button onClick={onClickFlagOn}>flag on</button>
            <button onClick={onClickFlagOff}>flag off</button>       
            <ul>{JSON.stringify(fileUploadFlag)}</ul>     
        </div>
    );
};

function mapStateToProps(state, ownProps) {
    return { fileUploadFlag: state };
}

function mapDispatchToProps(dispatch, ownProps) {
    return {
        flagOn : () => dispatch(actionCreators.flagOn()),
        flagOff : () => dispatch(actionCreators.flagOff()),     
    }  
}

export default connect(mapStateToProps, mapDispatchToProps)(ReduxTest);

 

그리고 위의 코드를 복사하여 ReduxTest → AnotherReduxTest로 바꾼 AnotherReduxTest.js를 만들자.

//AnotehrReduxTest.js

import React from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";

const AnotehrReduxTest = ({ toDos, flagOn, flagOff }) => { ... }

export default connect(mapStateToProps, mapDispatchToProps)(AnotehrReduxTest);

 

Test를 하기 위해 App.js에도 AnotherReduxTest.js를 추가한다.

//App.js

...

import AnotherReduxTest from "./components/AnotherReduxTest";

...

const App = () => {
  const [csvObject, setCsvObject] = useState(csvObjectDefault);
  return (
    <div>
      <ReduxTest/>
      <AnotherReduxTest/>
      {/* <button onClick={() => console.log(csvObject)}>print csv</button>
      <div className="App">
        <MyTable csvFile={csvObject}/>
        <FileUpload setCsvObject={setCsvObject} />
      </div> */}
    </div>
  );
};

 

아래의 코드로 fileUploadFlag라는 이름으로 store의 초깃값을 볼 수 있다.

<ul>{JSON.stringify(fileUploadFlag)}</ul>  

아래와 같이 버튼을 눌러서 테스트해보자.

Redux Component에서 변경한 fileUploadFlag가 Another Redux Test에도 반영된 것을 확인할 수 있다.

물론 그 반대도 마찬가지이다. 

 

따라서 FileUploadFlag를 App.js에서 만들지 않고, 필요한 component에 store를 연결해서 관리하면 된다.

 

최종 코드는 아래를 참고하자.

//index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

import {Provider} from "react-redux";
import store from './components/store';

ReactDOM.render(
  <Provider store={store}> 
    <App />
  </Provider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

 

//App.js
import React, { useState } from "react";

//import * as lib from "./components/library";
import FileUpload from "./components/FileUpload";
import MyTable from "./components/MyTable";

import ReduxTest from "./components/ReduxTest";
import AnotherReduxTest from "./components/AnotherReduxTest";

const csvObjectDefault = {
  HEIGHT: 0,
  WIDTH: 0,
  csv: [],
};

const App = () => {
  const [csvObject, setCsvObject] = useState(csvObjectDefault);
  return (
    <div>
      <ReduxTest/>
      <AnotherReduxTest/>
      {/* <button onClick={() => console.log(csvObject)}>print csv</button>
      <div className="App">
        <MyTable csvFile={csvObject}/>
        <FileUpload setCsvObject={setCsvObject} />
      </div> */}
    </div>
  );
};

export default App;

 

FileUpload는 최초에 false이므로 store의 초기값은 false로 변경하였다. 

//store.js

import { createStore } from "redux";

const FLAG_ON = "FLAG_ON";
const FLAG_OFF = "FLAG_OFF";

const flagOn = () => {
  return {
    type: FLAG_ON,
  };
}

const flagOff = () => {
  return {
    type: FLAG_OFF,
  };
}

const reducer = (state = true, action) => {
  switch (action.type) {
    case FLAG_ON: /* File Upload 성공 */
      return true;
    case FLAG_OFF: /* File Upload 전 */
      return false;
    default:
      return state;
  }
};

/* subscribe */
const store = createStore(reducer);

export const actionCreators = {
  flagOn,
  flagOff
}

export default store;

 

//ReduxTest.js

import React from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";

const ReduxTest = ({ fileUploadFlag, flagOn, flagOff }) => {
    const onClickShowFlag = () => {
        console.log(fileUploadFlag);
    }

    const onClickFlagOn = () => {
        flagOn();
    }

    const onClickFlagOff = () => {
        flagOff();
    }

    return (
        <div>
            <div>Redux Test</div>       
            <button onClick={onClickShowFlag}>flag state</button>
            <button onClick={onClickFlagOn}>flag on</button>
            <button onClick={onClickFlagOff}>flag off</button>       
            <ul>{JSON.stringify(fileUploadFlag)}</ul>     
        </div>
    );
};

/* mapStateToProps : store로부터 state를 가져다 준다. */
function mapStateToProps(state /* redux store에서 온 state */ , ownProps /* component의 props */) {
    //console.log(state);
    return { fileUploadFlag: state }; /* mapStateToProps는 state를 return 받기를 원한다 */
}

function mapDispatchToProps(dispatch, ownProps) {
    //console.log(dispatch);

    return { /* 해당 component의 props에서 받아올 수 있게 된다. */
        flagOn : () => dispatch(actionCreators.flagOn()),
        flagOff : () => dispatch(actionCreators.flagOff()),     
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(ReduxTest);
/* connect()는 home으로 보내는 props에 추가될 수 있도록 허용해준다. */
/* 만약 mapStateToProps가 필요 없다면 null, mapDispatchToProps로 하면 된다. */

 

//AnotherReduxTest.js

import React from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";

const AnotherReduxTest = ({ toDos, flagOn, flagOff }) => {
    const onClickShowFlag = () => {
        console.log(toDos);
    }

    const onClickFlagOn = () => {
        flagOn();
    }

    const onClickFlagOff = () => {
        flagOff();
    }

    return (
        <div>
            <div>Another Redux Test</div>       
            <button onClick={onClickShowFlag}>flag state</button>
            <button onClick={onClickFlagOn}>flag on</button>
            <button onClick={onClickFlagOff}>flag off</button>       
            <ul>{JSON.stringify(toDos)}</ul>     
        </div>
    );
};

function mapStateToProps(state, ownProps) {
    return { toDos: state };
}

function mapDispatchToProps(dispatch, ownProps) {
    return {
        flagOn : () => dispatch(actionCreators.flagOn()),
        flagOff : () => dispatch(actionCreators.flagOff()),     
    }  
}

export default connect(mapStateToProps, mapDispatchToProps)(AnotherReduxTest);

이전 - (16) colWidths 옵션 / csv 파일 파싱 보완

다음 - (18) File Upload에 대한 이벤트 보완

반응형

댓글