데이터 시각화 서비스 만들기

1인 지식 기업을 꿈꾸는 30, 40대 직장인을 위한 실무 프로젝트
React, Meteor, D3.js, Chart, Data Visualization

[Chart 추천 서비스] React + Meteor + Redux + Typescript + EUI 환경 구성

Chart 추천 서비스

기획한 Chart Recommendation 서비스를 위한 React, Meteor, Typescript, SCSS, Elastic UI(eui), Redux 기반 환경을 구성하는 과정을 정리한다. 


  - Meteor create을 통한 react환경

  - Typescript, SCSS 지원

  - redux-observable을 이용한 Redux 환경 설정

  - Elastic UI 및 react-final-form 설치

  - react+meteor+eui+scss boilerplate 소스




React기반의 Meteor, Typescript, SCSS 프로젝트 생성


Node.js 8.12.0 사용, 현재 meteor 버전은 1.8.0.1 이다. 

$ meteor create --react react-meteor-redux-boilerplate

$ cd react-meteor-redux-boilerplate

// 최신 버전 설치

$ meteor update 

// 정상 작동하는지 확인 

$ meteor run


scss, typescript 컴파일러 설치

$ meteor add fourseven:scss

$ meteor add barbatus:typescript


typescript를 위한 @types 및 패키지 설치

$ meteor npm install --save-dev @types/meteor 

$ meteor npm install --save-dev @types/node

$ meteor npm install --save-dev @types/react

$ meteor npm install --save-dev @types/react-dom


$ meteor npm install --save-dev react-router-dom 

$ meteor npm install --save-dev typescript

$ meteor npm install --save-dev tslint

$ meteor npm install --save-dev tslint-react


root에 tsconfig.json 및 tslint.json 추가

// tsconfig.json

{

    "compilerOptions": {

        "target": "es6",

        "allowJs": true,

        "jsx": "preserve",

        "moduleResolution": "node",

        "types": [

            "node"

        ]

    }

}


// tslint.json

{

    // "extends": [

    //     "tslint:latest",

    //     "tslint-react"

    // ],

    "rules": {

        "quotemark": [

            true,

            "single",

            "jsx-double"

        ],

        "ordered-imports": false,

        "no-var-requires": false

    }

}


.css, .jsx, .js 를 .scss, .tsx, .ts 확장자로 코드는 다음과 같이 변경한다. 

// import

import React from 'react'; 

=> 

import * as React from 'react';


// import path: 절대경로를 상대경로 변경

import App from '/imports/ui/App';

=> 

import App from '../imports/ui/App';


// class Props, State 타입지정

class Info extends Component {

=> 

class Info extends React.Component<{links: any}, undefined> {


// export 

export default InfoContainer = withTracker(() => {

=>

const InfoContainer = withTracker(() => {

...

export default InfoContainer;



package.json 의 start 파일 변경

"mainModule": {

  "client": "client/main.tsx",

  "server": "server/main.ts"

}




Meteor Pub/Sub 환경 설정


필요없는 패키지 제거

$ meteor remove autopublish


imports/api/links.ts 에 publish 설정

// link.ts

if (Meteor.isServer) {

  Meteor.publish('links', () => Links.find());

}


// Info.tsx 에 react-meteor-data의 withTracker안에 subscribe 설정

export default withTracker(() => {

       const handle = Meteor.subscribe('links');

return {

links: Links.find().fetch(),

loading: !handle.ready()

}

})(Info);


두번째로 필요없는 패키지 제거

$ meteor remove insecure


deny/allow를 설정

  - allow: true로 설정된 것만 가능하고 그외는 모두 불가능하다. (가능한 것이 적으면 allow 설정)

  - deny: true로 서정된 것만 불가능하고 그외는 모두 가능하다.  (불가능한 것이 적으면 deny 설정)

// api/links.ts의 allow 설정 예

if (Meteor.isServer) {

  Meteor.publish('links', () => Links.find());

  Links.allow({

    insert (userIdstringdocany) {

      console.log('insert doc:', doc);

      return (userId && doc.owner === userId);

    },

    remove(userIdstringdocany) {

     console.log('delete doc:', doc);

      return (userId && doc.owner === userId);

    }

  });

}


사용자 로그인 및 Collection을 위한 simple schem 패키지 설치

$ meteor add accounts-password

$ meteor npm install --save bcrypt

$ meteor npm install --save simpl-schema



폴더 구조를 바꾼다. 

client

imports

  - api: collection

  - client

    - layouts: layout container unit

    - pages: panel unit

    - sdk: shared components, libs

  - startup

    - client

    - server

server




Redux 환경 설정


redux 기본 패키지 설치, react-router-redux는 react-router v4로 오면서 connected-react-router 패키지로 변경되었다. 

$ meteor npm install --save redux react-redux connected-react-router

$ meteor npm install --save-dev @types/react-redux


// typesafe-action 설치

$ meteor npm install --save typesafe-actions


// rxjs 기반 side effect 패키지인 redux-observable 설치

$ meteor npm install --save redux-observable rxjs


// state 성능 향상 reselect 설치 

$ meteor npm install --save reselect


redux-observble 환경에서의 store 생성 및 acction, reducer, epic 개발은 다음의 블로그를 참조한다. 




Elastic UI 환경 설정


먼저 final-form, react-final-form 설치

$ meteor npm install --save final-form react-final-form


styled-components, moment  패키지 설치

$ meteor npm install --save styled-components moment


elastic UI(이하 eui) 설치하기. eui는 yarn으로만 설치됨을 주의한다.

  - react-final-form의 third part component로 Eui wrapper component 개발하기는 다음 블로그를 참조한다.

$ meteor npm i -g yarn

$ meteor yarn info

yarn info v1.12.3


// 기존것 제거 하고 yarn으로 다시 설치 

$ rm -rf node_modules

$ rm package-lock.json


// package.json 내용 설치

$ meteor yarn 

meteor yarn add  @elastic/eui


client/main.tsx에 사용할 테마 css를 import 한다. 

import '@elastic/eui/dist/eui_theme_light.css';


주의할 부분은 eui를 yarn으로 설치한 후 추가 패키지를 위해 "meteor npm install <package>" 할 경우 다시 "meteor yarn" 명령을 수행해 주어야 한다. 이 문제는 eui가 yarn만을 지원하고 meteor가 yarn에 대한 지원이 미흡한데 있지 않을까 싶다. 추정일 뿐이고 누가 해결하면 알려주시면 좋겠다.


최종 meteor run 을 수행하면 로그인 화면이 나온다. 계정을 생성하고 로그인 하면 이제 Link에 대한 CRUD를 수행할 수 있다.



[Meteor + React] 개발환경 만들기 - 5

Meteor

React의 route 설정을 리팩토링해보고, createRef() 를 사용하지 않고 form을 변경해 본다. 


  - react-router v4에 맞는 redux 환경 재구성

  - react-final-form  적용하기

  - 사용자별 Link 목록 보여주기

  - 적용소스




React Router v4에 맞는 Redux 설정


react-router-redux는 react router v2와 v3에 맞는 패키지이고 react router v4에 맞는 connected-react-router로 교체해야 한다. recct-router-redux 설정 상태로는 redux action이 작동하지 않는다.

// 설치

$ meteor npm install --save connected-react-router

// 삭제

$ meteor npm uninstall --save react-router-redux

$ meteor npm uninstall --save @types/react-router-redux


imports/ui/store.ts 리팩토링

  - Routes.tsx의 browserHistory 를 store.ts로 옮김

  - connectRouter로 routerReducer를 생성

  - RouterAction, LocationChangeAction을 추가

import { createBrowserHistory } from 'history';

import { connectRouter, RouterAction, LocationChangeAction } from 'connected-react-router';


export const browserHistory = createBrowserHistory();


const rootReducer = combineReducers({

  router: connectRouter(browserHistory),

  links: linkReducer

});


export type RootState = StateType<typeof rootReducer>;


type ReactRouterAction = RouterAction | LocationChangeAction;

export type RootAction = ReactRouterAction | LinkAction;


imports/ui/Routes.tsx 리팩토링

  - react-router v4의 Router를 사용하지 않고, connected-react-router의 ConnectedRouter를 사용

  - 인자로 store.ts 에서 생성한 browserHistory를 사용

import { ConnectedRouter } from 'connected-react-router';

import store, { browserHistory } from './store';


export const Root = (

  <Provider store={store}>

    <ConnectedRouter history={browserHistory}>

      <Switch>

        <Route exact path="/" component={Login} />

        <Route path="/main" component={App} />

        <Route path="/signup" component={Signup} />

        <Route path="/links" component={InfoContainer} />

        <Route path="*" component={NotFound} />

      </Switch>

    </ConnectedRouter>

  </Provider>

);


이제 Redux Chrome Extension에서 LOCATION_CHANGE 액션을 볼 수 있다. 




final-form 통해 입력


form 관련부분을 final-form으로 변경한다. React버전의 react-final-form을 사용한다. 

$ meteor npm install --save final-form 

$ meteor npm install --save react-final-form


로그인 화면부터 react-final-form으로 변경해 본다. 

  - react-final-form의 Form, Field import

  - Form 의 onSubmit={this.onLogin}을 통해 입력한 values 객체를 받기, 보통 폼 필드의 name을 key로하여 json 객체를 받는다. 

  - <input> 태그를 <Field> 태그로 변경

  - <button> 태그에 disabled 속성추가

  - 필요없는 부분 추석처리: React.createRef()

import * as React from 'react';

import { Link } from 'react-router-dom';

import { Meteor } from 'meteor/meteor';

import { Form, Field } from 'react-final-form';


export interface LoginProps {

  history: any;

}


export interface LoginState {

  error: string;

}


export default class Login extends React.Component<LoginProps, LoginState> {

  // email: any = React.createRef();

  // password: any = React.createRef();


  constructor(props) {

    super(props);

    this.state = {

      error: ''

    };

  }


  onLogin = ({emailpassword}=> {

    // e.preventDefault();

    // let email = this.email.current.value.trim();

    // let password = this.password.current.value.trim();

    if (!email || !password) {

      this.setState({error: 'Please input email and password both'});

      return;

    }

    Meteor.loginWithPassword({ email }, password, (err=> {

      if (err) {

        this.setState({ error: err.reason });

      } else {

        this.setState({ error: '' });

      }

    });

  }


  makeForm = (handleSubmitsubmittingpristinevalues }) => {

    return (

      <form onSubmit={handleSubmit}>

        <Field name="email" component="input" type="email" placeholder="Email" required/>

        <Field name="password" component="input" type="password" placeholder="Passowrd"/>

        {/* <input type="email" ref={this.email} name="email" placeholder="Email" />

        <input type="password" ref={this.password} name="password" placeholder="Password" /> */}

        <button type="submit" disabled={submitting || pristine}>Login</button>

      </form>

    );

  };


  public render() {

    return (

      <div>

        <h1>Login to short Link</h1>

        {this.state.error ? <p>{this.state.error} </p> : undefined}

        <Form onSubmit={this.onLogin} render={this.makeForm} />

        <Link to="/signup">Have a account?</Link>

      </div>

    );

  }

}


Signup.tsx, AddLink.tsx 도 변경한다. 




사용자별 Link 목록 보여주기


사용자를 추가하여 각 사용자가 등록한 목록만 보기 위해서 pub/sub 설정에서 userId  파라미터를 넘겨주어 본인 목록만 조회하여 publish 한다. 

// imports/ui/Info.tsx 맨 하단의 subscribe시에 자신의 아이디를 파라미터로 보낸다. 

export default compose(

  withTracker(() => {

    const connection = Meteor.subscribe('links', {userId: Meteor.userId()});

    return {

      links: Links.find().fetch(),

      loading: !connection.ready()

    };

  }),

  connect(mapProps)

)(Info);


imports/api/links.ts에서 userId를 파라미터로 find한다. 

if (Meteor.isServer) {

  Meteor.publish('links', ({userId}) => {

    console.log('userId:', userId);

    if (!userId) {

      return this.ready();

    }

    return Links.find({owner: userId});

  });

  ...

}




<참조>

connected-react-router 저장소

react-final-form 소개 영상