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

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를 수행할 수 있다.



[Chart 추천 서비스] Voyager 기술 스택과 신규 서비스 SRS

Chart 추천 서비스

Voyager 기술 스택을 살펴보고, 새로 디자인 하려는 기술스택을 정리해 보자. 


  - Voyager 기술 스택 알기

  - 새로운 기술 스택 준비하기 




Voyager 기술 스택


기술 스택 이해를 위해 voyager의 의존성 패키지를 살펴보자. 


  - Typescript

  - React

  - React third part components: datetime, dnd, file-download, modal, spinners, split-pane, tabs, tether

  - Redux: redux-action-log, redux-thunk, redux-undo, reselect

  - common packages: moment, font-awesome

  - vega: vega-datasets, vega-expression, vega-lite, vega-tooltip, vega-typings, vega-util

  - compassql

  - webpack 빌드


React 개발시 사용하는 환경에서 개발되었음으로 알 수 있다. Voyager 설명과 사용방법은 Gitbook을 사용하고 있다. 


  - 로컬 데이터 로딩, URL입력으로 불러오기: JSON, CSV, TSV 파일형식 지원

  - 데이터 Field 자동 분류: quantitative(숫자), temporal(날짜), categorical(문자)

  - quantitative에 대해 min, max, mean등의 함수 적용

  - temporal에 대해 year, month, date, hours등의 함수 적용





Lime 기술 스택


라임은 Voyager의 기능을 흡수하고 내가 사용하고 싶은 UX 를 가미한 서비스이다. 라임은 다음의 기술 스택으로 만들어 보려한다. 


  - React + Redux

  - Meteor

  - MongoDB

  - vega-lite, compassQL


라임 서비스의 SRS(Software Requirements Specification, 소프트웨어 요구사항 명세)는 다음과 같다.


  - 데이터는 Voyager에서 지원하는 형태와 동일하다. 이후 데이터에 대한 수집, 클렌징 작업은 또 하나의 프로젝트가 될 정도의 규모이므로 여기서는 배제한다. 

  - 차트를 디자인한 후, 직접 Vega-lite의 JSON 환경설정을 에디팅 할 수 있도록 한다. 에디터는 Vega-editor를 사용한다. 

  - 원하는 차트 형태가 나왔다면 데이터와 차트 설정을 저장할 수 있다. 

  - 저장한 차트 목록에서 원하는 것을 대시보드화 할 수 있다. 즉, 차트 -> 대시보드 기능을 가진다. 

  - 각 차트와 대시보드는 다른 애플리케이션에 embedding하여 표현할 수 있다. 

  - 대시보드는 생성한 Vega차트를 표현하거나, Text, Image등의 Widget을 제공하여 사용자 정의 정적 표현을 할 수 있다. 


차트 생성


대시보드 생성


어짜피 혼자 개발하는 것이니 한장 짜리 SRS로 시작해 본다.




<참고>

- Voyager Github

- Voyager GitBook

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

Meteor

컴포넌트에 style을 직접 적용하는 대신, class를 적용해 본다. 


  - meteor 기반에서 css 적용의 어려운 부분들

  - styled-component 적용하기

  - 적용소스




styled-components 적용하기


meteor는 자체 builder를 통해 패키지를 빌드하는 관계로 webpack과 같은 custom 설정에 제약이 있어 보인다. 물론 잘 알지 못해서 아직 찾지 못했을 수도 있지만 다음과 같은 class 적용이 안된다. 

  - import * as style from './link.scss': from 문구를 사용하지 못 한다. 나머지 다양한 class적용 방법도 잘 안되는 듯 하다. 

  - import './link.scss' 만 사용가능하다. local 적용이 아닌 global 적용을 한다. <head> 태그에 prefix없이 들어간다. 


Meteor의 제약사항을 고려하여 별도의 scss 파일 작성을 최대한 줄이고, 컴포넌트의 style을 확장하기 위해 styled-components를 사용토록 한다. 

$ meteor npm install --save styled-components


imports/ui/sdk/eui/flexgroup.style.tsx 파일 을 생성한다. 

  - 테스트로 확장 컴포넌트로 부터 받은 색을 적용해 본다. 

  - props는 camelCase로 설정한다.

import styled from 'styled-components';

import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';


export const StyledWidthEuiFlexGroup = styled(EuiFlexGroup)`

  width: ${props => props.width || '100%'};

`;


export const StyledLongEuiFlexItem = styled(EuiFlexItem)`

  max-width: 400px;

  input {

    background${props => props.background || 'yellow'};  // 동적으로 background color를 적용한다.

    color: white;

  }

`;


export const StyledShortEuiFlexItem = styled(EuiFlexItem)`

  max-width: 100px;

`;


AddLink.tsx에서 사용한다. 

import { StyledLongEuiFlexItem, StyledShortEuiFlexItem } from '../../sdk/eui/flexgroup.style';


class AddLink extends React.Component<AddLinkProps, any> {

...

  makeForm = ({handleSubmitsubmittingpristine}) => {

    return (

      <form onSubmit={handleSubmit}>

        <EuiFlexGroup direction="row" gutterSize="s">

          <EuiFlexItem><EInput name="title" component="input" type="text" placeholder="Title" /></EuiFlexItem>

          <StyledLongEuiFlexItem background="red">

             <EInput name="url" component="input" type="text" placeholder="Url" />

          </StyledLongEuiFlexItem>

          <StyledShortEuiFlexItem>

            <EuiButton type="submit" fill disabled={submitting || pristine}>Add Link</EuiButton>

          </StyledShortEuiFlexItem>

        </EuiFlexGroup>

      </form>

    );

  }

...

}



Login.tsx에도 적용한다. 

  - FlexGroup  의 width=600px으로 설정한다. 

  - JPage, JRow를 설정한다. (아래에서 계속 개발)

import { JPage, JRow } from '../layouts/common.style';


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

...

  makeForm = ({ handleSubmitsubmittingpristine }) => {

    return (

      <form onSubmit={handleSubmit}>

        <StyledWidthEuiFlexGroup width="600px" direction="row" gutterSize="s">

          <EuiFlexItem><EInput name="email" type="email" placeholder="Email" /></EuiFlexItem>

          <EuiFlexItem><EInput name="password" type="password" placeholder="Passowrd" /></EuiFlexItem>

          <StyledShortEuiFlexItem>

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

          </StyledShortEuiFlexItem>

        </StyledWidthEuiFlexGroup>

      </form>

    );

  };


  public render() {

    return (

      <JPage>

        <JRow padding="10px" fontSize="20px">Login to short Link</JRow>

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

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

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

      </JPage>

    );

  }

}


JPage와 JRow 컴포넌트를 imports/ui/layouts/common.style.ts 생성한다. 

  - JPage는  padding

  - JRow는 flexbox의 row direction으로 설정한 컴포넌트이다.

  - 컴포넌트의 properties를 통해 값을 받아 동적으로 설정한다. 

import styled from 'styled-components';


export const JPage = styled.div`

  display: block;

  position: relative;

  padding: 10px;

  height: 100%;

  width: 100%;

`;


export const JRow = styled.div`

  display: flex;

  justify-content: ${ props => props.align || 'flex-start' };

  align-items: ${ props => props.align || 'center' };

  width: ${ props => props.width || '100%' };

  padding: ${ props => props.padding || '5px' };

  font-size: ${ props => props.fontSize || '14px' };

`;



styled-components를 통해 얻을 수 있는 장점


  - 기존 컴포넌트의 style 확장을 컴포넌트 개념을 할 수 있다. 기존은 .css 작성

  - 일반 태그를 style을 동적으로 적용할 수 있는 컴포넌트로 만들어 React컴포넌트에서 Common 컴포넌트처럼 사용할 수 있다. 즉 Style만을 담당하는 컴포넌트임.




<참조>

class 적용 방법들

styled-components 홈페이지

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

Meteor

Ant Design 컴포넌트 또는 Elatstic UI 같은 컴포넌트 적용방법을 알아보자. 


  - Ant Design 컴포넌트 적용

  - Elastic UI  컴포넌트 적용

  - 적용 소스




Final Form 에 Ant Design 컴포넌트 적용하기


적용하기-1 블로그에서 Antd을 설치했다. Ant Design의 Form을 사용하여 Login, SignOut, AddLink의 Input을 수정한다. 

  - imports/ui/sdk/antd/antd-final-input.tsx 파일 생성

  -antd 의 input react 컴포넌트를  react-final-form의 input의 custom component로 설정한다. (third component support )

import * as React from 'react';

import { Input } from 'antd';

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


// antd 의 react input component를 react-final-form에서 사용할 수 있는 custom component로 만듦

const AntdInput = ({input, ...rest}) => {

  return (

    <Input {...input} {...rest} />

  )

}


// 최종 애플리케이션에서 사용할 react-final-form 컴포넌트

const AInput = (propsany=> {

    return (

      <Field {...props} component={AntdInput} />

    );

}


export default AInput;


antd의 layout 컴포넌트은 Col, Row를 사용해서 레이아웃을 잡는다. 

import { Row, Col, Button } from 'antd';

import AInput from '../sdk/antd/antd-final-input';


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

...

  makeForm = ({ handleSubmitsubmittingpristine }) => {

    return (

        <form onSubmit={handleSubmit}>

          <Col span={4}><AInput name="email" type="email" placeholder="Email" /></Col>

          <Col span={4}><AInput name="password" type="password" placeholder="Passowrd" /></Col>

          <Col span={2}><Button type="primary" htmlType="submit" disabled={submitting || pristine}>Login</Button></Col>

        </form>

    );

  };


  public render() {

    return (

      <div>

        <h1>Login to short Link</h1>

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

        <Row gutter={5}>

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

        </Row>

        <Row gutter={5}>

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

        </Row>

      </div>

    );

  }

}



AddLink.tsx, Signup.tsx도 동일하게 바꾸어 본다. 




Final Form에 Elastic UI 적용하기 


eui(Elastic UI)는 Elastic Search의 React기반 오픈소스이다. Elastic Search의 Kibana에서 사용중이다. AntD대신 Eui를 적용해 본다. 

eui는 yarn install 만 지원하는 관계로 Meteor에서 yarn을 사용하기 위해서 다음 과정을 최초 한번 설정한다. 

$ meteor npm i -g yarn

$ meteor yarn info 

yarn info v1.12.3


$ rm -rf node_modules (MS는 윈도우 명령으로)

$ rm package-lock.json


// package.json 내용 설치

$ meteor yarn 


  - @elastic/eui 패키지 설치

  - css 설정

// eui는 npm install을 지원하지 않는다. 

meteor yarn add  @elastic/eui


// client/main.tsx에서 import한다. 

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


// client/theme.less 안은 import는 주석처리한다.

// @import '{}/node_modules/antd/dist/antd.less';


Lisk화면을 다음과 같이 전환한다. 


react-final-form에 EuiFieldText (input tag) 컴포넌트를 적용한다. 

  - imports/ui/sdk/eui/eui-final-input.tsx 파일 생성

import * as React from 'react';

import { EuiFieldText } from '@elastic/eui';

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


const EuiInput = ({ input, ...rest }) => {

  return (

    <EuiFieldText {...input} {...rest} />

  )

}


const EInput = (propsany=> {

  return (

    <Field {...props} component={EuiInput} />

  );

}


export default EInput;


imports/ui/Info.tsx 에서 EuiPage 관련 컴포넌트로 레이아웃을 꾸민다. 

  - EuiPageHeader, EuiPageContent로 나눔

  - 태그안의 정렬은 FlexBox가 적용된 EuiFlexGroup과 EuiFlexItem을 사용한다.

  - width, padding은 style일 직접 설정한다.

import {

  EuiFlexGroup, EuiFlexItem, EuiButton, EuiPage, EuiSpacer,

  EuiPageBody, EuiPageHeader,  EuiPageContent, EuiPageContentBody, EuiTitle

} from '@elastic/eui';


class Info extends React.Component<InfoProps, any> {

...

  render() {

    return (

      <EuiPage>

        <EuiPageBody>

          <EuiPageHeader>

              <EuiFlexGroup justifyContent="spaceBetween">

                <EuiFlexItem grow={1} style={{ paddingLeft: 20 }}>

                  <EuiTitle size="m"><h1>Add Link & List</h1></EuiTitle>

                </EuiFlexItem>

                <EuiFlexItem style={{ maxWidth: 130, paddingRight: 30 }}>

                  <EuiButton style={{ maxWidth: 100 }} onClick={this.onLogout}>Log out</EuiButton>

                </EuiFlexItem>

              </EuiFlexGroup>

          </EuiPageHeader>

          <EuiPageContent>

            <EuiPageContentBody>

              <EuiFlexGroup direction="column" justifyContent="spaceBetween">

                <EuiFlexItem style={{ maxWidth: 800 }}>

                  <AddLink /> 

                </EuiFlexItem>

                <EuiSpacer />

                <EuiFlexItem style={{ maxWidth: 400 }}>

                  {this.linkList()}

                </EuiFlexItem>

              </EuiFlexGroup>

            </EuiPageContentBody>

          </EuiPageContent>

        </EuiPageBody>

      </EuiPage>

    );

  }

}


imports/ui/pages/link/AddLink.tsx도 수정한다.

  - EuiFlexGroup으로 레이아웃 적용: flexbox direction은 row이다.

  - EInput 컴포넌트 적용

  - EuiButton 적용하기: type="submit" 설정

  - AntD적용 내용은 모두 주석처리한다. 

import EInput from '../../sdk/eui/eui-final-input';

import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';



class AddLink extends React.Component<AddLinkProps, any> {

...

  makeForm = ({handleSubmitsubmittingpristine}) => {

    return (

      <form onSubmit={handleSubmit}>

        {/* <Col span={4}><AInput name="title" component="input" type="text" placeholder="Title" /></Col>

        <Col span={4}><AInput name="url" component="input" type="text" placeholder="Url" /></Col>

        <Col span={2}><Button type="primary" htmlType="submit" disabled={submitting || pristine}>Add Link</Button></Col> */}

        <EuiFlexGroup direction="row" gutterSize="s">

          <EuiFlexItem><EInput name="title" component="input" type="text" placeholder="Title" /></EuiFlexItem>

          <EuiFlexItem><EInput name="url" component="input" type="text" placeholder="Url" /></EuiFlexItem>

          <EuiFlexItem style={{ maxWidth: 100 }}>

            <EuiButton type="submit" fill disabled={submitting || pristine}>Add Link</EuiButton>

          </EuiFlexItem>

        </EuiFlexGroup>

      </form>

    );

  }


  public render() {

    return (

      // <Row gutter={5}>

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

      // </Row>

      );

  }

}


Link.tsx도 수정한다. 

  - EuiButton 사이즈 small 설정

  - EuiButton minWidth 적용: default가 120px 이므로 overrriding을 minWidth: 40 을 설정한다

class Link extends React.Component<LinkProps, any> {

...

  public render() {

    const { link } = this.props;

    return (

      <li key={link._id}>

        <a href={link.url} target="_blank">{link.title}</a>

        <EuiButton size="s" style={{minWidth: 40, marginLeft: 10}} onClick={this.deleteLink}>x</EuiButton>

      </li>

    );

  }

}


Login.tsx, Signup.tsx도 eui 컴포넌트로 변경해 본다. 개인적으로 FlexBox를 많이 사용하는데 이에대한 컴포넌트 레벨 지원을 Eui가 제공하므로, 앞으로 Eui 컴포넌트를 사용한다. 다음은 style을 직접 적용하는 방식이 아니라 class 적용방법을 알아본다. 




<참조> 

Ant Design React Component

Elastic UI React Component 

Meteor에서 yarn 사용하기 - 마지막 답변 참조

[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 소개 영상

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

Meteor

Meteor와 React 환경을 좀 더 Production에 가깝게 만들어 본다. 


  - Insecure 제거 및 allow/deny 설정

  - React의 Refs에 대해 변경

  - Meteor methods/call 사용한 목록 조회

  - SimpleSchema 적용

  - 적용소스




Insecure 패키지 제거 및 Allow/Deny 설정


모든 컬렉션에 대한 자동 subscribe에 대한 autopublish 패키지는 이미 제거를 했고, 다음으로 insecure 패키지를 제거하여 컬렉션의 접근 권한을 제어하는 allow/deny를 설정한다. 

$ meteor remove insecure


insecure를 제거하게 되면 read만 가능하고 update, delete, insert가 불가능하다. 따라서 권한부분은 서버에서 실행되기 때문에 allow/deny설정을 Meteor.isServer블럭안에서 해주어야 한다.

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

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

// api/links.ts

import { Meteor } from 'meteor/meteor';

import { Mongo } from 'meteor/mongo';


const Links = new Mongo.Collection('links');


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);

    }

  })

}

export default Links;


 allow에서 owner 아이디를 비교하므로 epic에서 owner 값을 설정한다. 

// imports/ui/pages/link/link.epic.ts

const addLink: Epic = (

  action$,

  store

=>

  action$.pipe(

    filter(isOfType(actions.ADD_REQUEST)),

    switchMap(action => {

      const { title, url } = action.payload;

      const owner = Meteor.userId();

      return insertCollection(Links, { title, url, owner, createdAt: new Date() })

    }),

  ...

);


userId는 로그인을 하게되면 이미 서버와 동기화 되어 서버가 가지고 있는 userId를 첫번째 파라미터로 넘겨준다. 두번째 파라미터 doc은 insert할 때 입력한 파라미터값 이고, remove의 doc은 저장된 몽고디비의 document이다. 

// meteor run을 수행한 콘솔창에 찍힌다. 

=> Meteor server restarted

I20181123-15:00:36.619(9)? insert doc: { title: 'google2',

I20181123-15:00:36.676(9)?   url: 'http://www.google.com',

I20181123-15:00:36.676(9)?   owner: 'AmMBJzZ33Nc8Bsqhe',

I20181123-15:00:36.676(9)?   createdAt: 2018-11-23T06:00:36.611Z }

I20181123-15:00:39.955(9)? remove doc: { _id: 'Zrb8jGSpt2AyRn4Q3',

I20181123-15:00:39.955(9)?   title: 'google2',

I20181123-15:00:39.955(9)?   url: 'http://www.google.com',

I20181123-15:00:39.955(9)?   owner: 'AmMBJzZ33Nc8Bsqhe',

I20181123-15:00:39.955(9)?   createdAt: 2018-11-23T06:00:36.611Z }




Form 입력의 React의 Refs 변경


React에서 DOM객체 접근 방법은 3가지 이고, string과  콜백펑션말고 16.3 버전이후 나온 createRefs()를 사용한다. 

  - ref="string"

  - ref={callback-function}

  - createRefs()


DOM 객체를 받을 변수를 선언하고, ref={this.변수}로 할당한다. 값 접근은 this.변수.current 객체를 통한다.

// imports/ui/pages/Login.tsx

.. 중략 ..

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 = (eany=> {

    e.preventDefault();


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

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

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

      if (err) {

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

      } else {

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

      }

    });

  }


  public render() {

    return (

      <div>

        <h1>Login to short Link</h1>

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

        <form onSubmit={this.onLogin}>

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

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

          <button>Login</button>

        </form>

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

      </div>

    );

  }

}


Signup.tsx과 AddLink.tsx도 동일하게 변경한다. 




Meteor methods/call을 사용한 저장/삭제


imports/sdk/utils/ddp.util.ts 에 method call에 대한 observable 반환 메소드 추가

export function insertCall(methodNamestringparamsany): Observable<RequestModel> {

  return from(new Promise((resolvereject=> {

    Meteor.call(methodName, params, (errorresult=> {

      if (error) {

        reject({ error: true, result: { ...error }, params: { ...params } });

      }

      if (typeof result === 'string' || typeof result === 'number') {

        resolve({ success: true, result, params: { ...params } });

      } else {

        resolve({ success: true, result: { ...result }, params: { ...params } });

      }

    });

  }));

}


export function removeCall(methodNamestring_idstring): Observable<RequestModel> {

  return from(new Promise((resolvereject=> {

    Meteor.call(methodName, _id, (errorresult=> {

      if (error) {

        reject({ error: true, result: { ...error }, params: { _id } });

      }

      if (typeof result === 'string' || typeof result === 'number') {

        resolve({ success: true, result, params: { _id } });

      } else {

        resolve({ success: true, result: { ...result }, params: { _id } });

      }

    });

  }));

}


imports/api/links.ts 에서 Meteor.methods를 추가한다. 

if (Meteor.isServer) {

 ... 

  Meteor.methods({

    insertLink(paramsany) {

      if (!this.userId) {

        throw new Meteor.Error('Please login');

      }

      return Links.insert(params);

    },

    removeLink(_idstring) {

      if (!this.userId) {

        throw new Meteor.Error('Please login');

      }

      return Links.remove(_id);

    }

  });

}


imports/ui/pages/link/link.epic.ts 에서 collection 을 호출하지 않고, Meteor.call을 호출 한다. takeUntil은 브라우져 이동등을 할때 호출을 끊는 역할을 한다. 현재는 주석처리로 미구현상태임.

const addLink: Epic = (

  action$,

  store

=>

  action$.pipe(

    filter(isOfType(actions.ADD_REQUEST)),

    switchMap(action => {

      const { title, url } = action.payload;

      const owner = Meteor.userId();

      return insertCall('insertLink', { title, url, owner, createdAt: new Date() })

      // return insertCollection(Links, { title, url, owner, createdAt: new Date() })

    }),

    map((response: RequestModel) => {

      if (response.error) {

        return actions.addLinkFailed({ ...response.result })

      }

      return actions.addLinkSuccess(response.result)

    }),

    // takeUntil(action$.pipe(

    //   filter(isOfType(actions.ADD_REQUEST))

    // ))

  );


const removeLink: Epic = (

  action$,

  store

=>

  action$.pipe(

    filter(isOfType(actions.DELETE_REQUEST)),

    switchMap(action => {

      return removeCall('removeLink', action.payload);

      // return removeCollection(Links, action.payload);

    }),

    map((response: RequestModel) => {

      if (response.error) {

        return actions.removeLinkFailed({ ...response.result, ...response.params })

      }

      return actions.removeLinkSuccess(response.params._id);

    }),

    // takeUntil(action$.pipe(

    //   filter(isOfType(actions.ADD_REQUEST))

    // ))

  );




SimpleSchema 적용하기 


Validation을 위해 simple schema 패키지를 설치한다. 

$ meteor npm install --save simpl-schema


/imports/api/links.ts 에 schema를 정의한다. 

import { Meteor } from 'meteor/meteor';

import { Mongo } from 'meteor/mongo';

import SimpleSchema from 'simpl-schema';


const Links = new Mongo.Collection('links');


if (Meteor.isServer) {

 ...

  const linkSchema = new SimpleSchema({

    title: {

      type: String,

      min: 3

    },

    url: {

      type: String

    },

    owner: {

      type: String

    },

    createdAt: {

      type: Date 

    }

  });


  Meteor.methods({

    insertLink(paramsany) {

      if (!this.userId) {

        throw new Meteor.Error('Please login');

      }

      try {

        linkSchema.validate(params);

        return Links.insert(params);

      } catch(e) {

        throw new Meteor.Error('no valid schema');

      }

    },

   ...

  });

}

export default Links;


account에 대한 validate도 정의해 본다. imports/api/ 폴더아래에 account-validate.ts 파일을 생성하고 server/main.ts에서 import한다. 

// account-validate.ts

import { Meteor } from 'meteor/meteor';

import { Accounts } from 'meteor/accounts-base';

import SimpleSchema from 'simpl-schema';


if (Meteor.isServer) {

  Accounts.validateNewUser((user=> {

    const email = user.emails[0].address;


    try {

      new SimpleSchema({

        email: {

          type: String,

          regEx: SimpleSchema.RegEx.Email

        }

      }).validate({ email });

    } catch(e) {

      throw new Meteor.Error(400, e.message);

    }


    return true;

  });

}


// server/main.ts

import { Meteor } from 'meteor/meteor';

import '../imports/api/links';

import '../imports/api/account-validate';


Meteor.startup(() => {

});


// Singup.tsx 파일에 테스트를 위해 noValidate를 추가한다. 

<form onSubmit={this.onCreateAccount} noValidate>




<참조>

React createRefs() 사용하기

Meteor methods/call 사용하기

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

Meteor

Meteor에서 Redux 환경을 설정하고, 애플리케이션에 적용해 본다.


  - Meteor Collection의 Pub/Sub 설정

  - Collection의 CRUD 서비스 클래스 개발

  - Redux 환경 설정

  - Redux의 action, reducer 개발 및 store 설정

  - 애플리케이션에 Redux action 호출하기

  - 소스코드 




MongoDB Collection Pub/Sub 설정


기본설치를 하게 되면 autopublish 미티어 패키지가 설치되어 있다. 삭제한다.

meteor remove autopublish


삭제를 하면 links 컬렉션으로 부터 데이터를 받지 못한다. 이제 필요한 것을 받기 위해 publish, subscribe를 설정한다. 

  - server/main.tsx에서 insertLink 코드를 제거한다.

  - server/main.tsx에서 links 코드 import 문을 수정한다. 

// main.tsx 전체코드

import { Meteor } from 'meteor/meteor';

import '../imports/api/links';  // 서버에서 사용하기 위해 반드시 import해야 한다. 하지 않으면 collection 생성, 제어를 못 한다.


Meteor.startup(() => { }); // 텅빈 코드 블록


  - imports/api/links.ts 에 publish 한다. 

if (Meteor.isServer) {

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

}


  - subscribe를 사용할 때 미티어의 Tracker.autorun을 사용하지 않고, React의 props에 subscribe정보를 바로 맵핑해 주는 withTracker를 사용하기 위해 react-meteor-data 패키지를 사용한다.

$ meteor npm install --save react-meteor-data


  - imports/api/ui/Info.tsx에 withTracker를 설정한다. 

import { Meteor } from 'meteor/meteor';

import Links from '../api/links';


interface InfoProps {

links: any;

loading: boolean;

}


// 맨 하단

export default withTracker(() => {

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

return {

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

loading: !handle.ready()

}

})(Info);




MongoDB Collection CRUD


간단한 Link CRUD 애플리케이션을 만든다. 


  - Collection의 CRUD 로직을 담은 service를 생성한다. imports/ui밑에 link폴더를 생성한다. 

// imports/ui/link/link.service.ts

import Links from '../api/links';


class LinkService {

  addLink({titleurl}) {

    Links.insert({title, url, createAt: new Date()});

  }


  removeLink(_idstring) {

    Links.remove(_id);

  }


  updateLink(_idstring, {titleurl}) {

    Links.update(_id, {title, url, updateAt: new Date()});

  }

}


export const linkService = new LinkService();


  - imports/ui/link/AddLink.tsx 파일 추가

import * as React from 'react';

import { linkService } from './links.service';

export interface AddLinkProps {

}

export default class AddLink extends React.Component<AddLinkProps, any> {

  handleSubmit = (eany=> {

    //ignore validation

    e.preventDefault();

    const param = {

      title: e.target.title.value,

      url: e.target.url.value

    }

    linkService.addLink(param);

  };


  public render() {

    return (

      <form onSubmit={this.handleSubmit} >

        <input type="text" name="title" placeholder="title" />

        <input type="text" name="url" placeholder="url" />

        <button>Add Link</button>

      </form>

    )

  }

}


  - imports/ui/App.tsx에서 <Info/> 만 남기고 삭제하고, Info.tsx에 AddLink를 추가한다. 

// App.tsx

class App ... {

  render() { 

    return (

      <div></InfoContainer/></div>

    )

  }

}


// Info.tsx
class Info ... {
  render() {
    return(
      <div> 
          <AddLink />
          ....
       </div>
    )
  }
}


  - link폴더 밑에 LinkList.tsx와 Link.tsx를 추가한다. 

// Link.tsx

import * as React from 'react';

export interface LinkProps {

  link: any

}


export default class Link extends React.Component<LinkProps, any> {

  public render() {

    const { link } = this.props;

    return (

      <li key={link._id}>

        <a href={link.url} target="_blank">{link.title}</a>

      </li>

    );

  }

}


// LinkList.tsx

import * as React from 'react';

import Link from './Link';

export interface LinkListProps {

  links: any[]

}


export default class LinkList extends React.Component<LinkListProps, any> {

  public render() {

    const links = this.props.links.map(

      link => <Link link={link}/>

    );

    return (

      <div>

        <h2>Links</h2>

        <ul>{links}</ul>

      </div>

    );

  }

}



  - Info.tsx에서 LinkList를 사용토록 변경한다. 

class Info extends React.Component<InfoProps, any> {

  linkList() {

    const { links, loading } = this.props;

    if (loading) {

      return <div>loading...</div>

    } else {

      return <LinkList links={links} />

    }

  }


  render() {

    const { links } = this.props;

    return (

      <div>

        <AddLink />

        {this.linkList()}

      </div>

    );

  }

}


  - 마지막으로 link 삭제를 한다. 

// imports/ui/link.Link.tsx

import * as React from 'react';

import { linkService } from './links.service';


export interface LinkProps {

  link: any

}


export default class Link extends React.Component<LinkProps, any> {

  removeLink = () => {

    const { link } = this.props;

    linkService.removeLink(link._id);

  }


  public render() {

    const { link } = this.props;

    return (

      <li key={link._id}>

        <a href={link.url} target="_blank">{link.title}</a>

        <button onClick={this.removeLink}> x </button>

      </li>

    );

  }

}





Redux 관련 패키지 설치


redux 기본 패키지를 설치한다. 

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

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


redux action, reducer을 보다 편하게 사용할 수 있는 typesafe-actions 모듈을 설치한다.

$ meteor npm install --save typesafe-actions 


redux에서 비동기를 처리할 수 있는 redux-observable 과 rxjs 모듈을 설치한다. 

$ meteor npm install --save redux-observable rxjs


state 갱신으로 인한 반복 rendering을 제거해 성능향상을 위한 reselect 모듈도 설치한다. 

$ meteor npm install --save reselect




Redux 코드 개발


link action 개발

  - action type constants 정의

  - action Model 정의

  - action  에 대한 정의

// imports/ui/pages/link/link.action.ts

import { action } from 'typesafe-actions';


/**************************

 * constants, model

 **************************/

// constants

export const ADD_REQUEST = '[link] ADD_REQUEST';

export const ADD_SUCCESS = '[link] ADD_SUCCESS';

export const ADD_FAILED = '[link] ADD_FAILED';

export const DELETE_REQUEST = '[link] DELETE_REQUEST';

export const DELETE_SUCCESS = '[link] DELETE_SUCCESS';

export const DELETE_FAILED = '[link] DELETE_FAILED';

export const CHANGE = '[link] CHANGE';


// model

export type LinkModel = {

  _id?: string;

  title?: string;

  url?: string;

  visited?: boolean;

  error?: boolean;

  errorMsg?: any;

  success?: boolean;

  createdAt?: any;

  updatedAt?: any;

};


/**************************

 * actions, action-type

 **************************/

export const addLink = (params: LinkModel) => action(ADD_REQUEST, params);

export const addLinkSuccess = (params: LinkModel) => action(ADD_SUCCESS, params);

export const addLinkFailed = (params: LinkModel) => action(ADD_FAILED, params);

export const removeLink = (idstring=> action(DELETE_REQUEST, id);

export const removeLinkSuccess = (idstring=> action(DELETE_SUCCESS, id);

export const removeLinkFailed = (idstring=> action(DELETE_FAILED, id);

export const changeLink = (idstring=> action(CHANGE, id);


link reducer 개발

  - Link State 정의

  - LinkAction 타입 

  - Link reducer 분기

  - reselect를 이용한 selector는 별도로 만들지 않고, link.reduer.ts 파일이 함께 둔다. (파일이 너무 많아져서...)

// imports/ui/pages/link/link.reducer.ts

import { ActionType } from 'typesafe-actions';

import { combineReducers } from 'redux';

import { createSelector } from 'reselect';

import * as actions from './link.action';


/**************************

 * state

 **************************/

// state

export type LinkState = {

  list: actions.LinkModel[],

  linkFilter: string

};


/**************************

 * reducers

 **************************/

export type LinkAction = ActionType<typeof actions>;

export const linkReducer = combineReducers<LinkState, LinkAction>({

  list: (state = [], action=> {

    switch (action.type) {

      case actions.ADD_FAILED:

        return state;

      case actions.ADD_SUCCESS:

        return [...state, action.payload];

      case actions.DELETE_FAILED:

        return state;

      case actions.DELETE_SUCCESS:

        return state.filter(item => item._id === action.payload);

      case actions.CHANGE:

        return state.map(

          item =>

            item._id === action.payload

              ? { ...item, visited: !item.visited }

              : item

        );

      default:

        return state;

    }

  },

  linkFilter: (state = '', action=> {

    switch (action.type) {

      case actions.CHANGE:

        if (action.payload === 'visited'){

          return '';

        } else {

          return 'visited';

        }

      default:

        return state;

    }

  }

})


/**************************

 * selectors

 **************************/

export const getLinks = (state: LinkState) => state.list;

export const getLinkFilter = (state: LinkState) => state.linkFilter;

export const getFilteredLinks = createSelector(getLinks, getLinkFilter, (linkslinkFilter=> {

  switch (linkFilter) {

    case 'visited':

      return links.filter(t => t.visited);

    default:

      return links;

  }

});


redux-observable을 이용한 epic을 만든다.

  - async 처리

  - rxjs 이용

  - takeUntil은 cancel을 위한 장치이다.

// imports/ui/pages/link/link.epic.ts

import { Epic, combineEpics } from 'redux-observable';

import { filter, map, switchMap, takeUntil } from 'rxjs/operators';

import { isOfType } from 'typesafe-actions';


import Links from '../../../api/links';

import { insertCollection, removeCollection, RequestModel } from '../../sdk';

import * as actions from './link.action';


const addLink: Epic = (

  action$,

  store

=>

  action$.pipe(

    filter(isOfType(actions.ADD_REQUEST)),

    switchMap(action => {

      const { title, url } = action.payload;

      return insertCollection(Links, { title, url, createdAt: new Date() })

    }),

    map((response: RequestModel) => {

      if (response.error) {

        return actions.addLinkFailed({ ...response.result })

      }

      return actions.addLinkSuccess(response.result)

    }),

    // takeUntil(action$.pipe(

    //   filter(isOfType(actions.ADD_REQUEST))

    // ))

  );


const removeLink: Epic = (

  action$,

  store

=>

  action$.pipe(

    filter(isOfType(actions.DELETE_REQUEST)),

    switchMap(action => {

      return removeCollection(Links, action.payload);

    }),

    map((response: RequestModel) => {

      if (response.error) {

        return actions.removeLinkFailed({ ...response.result, ...response.params })

      }

      return actions.removeLinkSuccess(response.params._id);

    }),

    // takeUntil(action$.pipe(

    //   filter(isOfType(actions.ADD_REQUEST))

    // ))

  );


export const linkEpic = combineEpics(addLink, removeLink);


Meteor Client Collection 을 Observable로 전환

  - meteor client collection의 insert, remove시에 call 결과를 Observable로 변환하여 반환하는 유틸을 만든다. 

  - 이는 redux-observable의 epic에서 async 데이터를 switchMap 오퍼레이터로 다루기 위함이다. 

// imports/sdk/util/ddp.util.ts

import { from, Observable } from 'rxjs';


export type RequestModel = {

  error?: boolean;

  success?: boolean;

  result: any;

  params?: any;

}


export function insertCollection(collectionanyparamsany): Observable<RequestModel> {

  return from(new Promise((resolvereject=> {

    collection.insert(params, (errorresult=> {

      if (error) {

        reject({ error: true, result: { ...error }, params: { ...params } });

      }

      if (typeof result === 'string' || typeof result === 'number') {

        resolve({ success: true, result, params: {...params} });

      } else {

        resolve({ success: true, result: { ...result }, params: { ...params } });

      }

    });

  }));

}


export function removeCollection(collectionany_idstring): Observable<RequestModel> {

  return from(new Promise((resolvereject=> {

    collection.remove(_id, (errorresult=> {

      if (error) {

        reject({ error: true, result: { ...error }, params: { _id } });

      }

      if (typeof result === 'string' || typeof result === 'number') {

        resolve({ success: true, result, params: { _id } });

      } else {

        resolve({ success: true, result: { ...result }, params: { _id } });

      }

    });

  }));

}




RootReducer와 Store 설정


link관련 action, reducer, epic 개발이 끝나면 이를 등록하는 설정을 한다. 

  - root Reducer 정의

  - root State  타입 정의

  - root Action 타입 정의

  - root Epic 정의

  - root Store 설정

// imports/ui/store.ts

import { combineEpics, createEpicMiddleware } from 'redux-observable';

import { createStore, applyMiddleware, combineReducers, compose } from 'redux';

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

import { routerReducer } from 'react-router-redux';

import { StateType } from 'typesafe-actions'; 


import { linkEpic } from './pages/link/link.epic';

import { linkReducer, LinkAction } from './pages/link/link.reducer';


/***********************

 * root reducer

 ***********************/

const rootReducer = combineReducers({

  router: routerReducer,

  links: linkReducer

});


/***********************

 * root state

 ***********************/

export type RootState = StateType<typeof rootReducer>;


type ReactRouterAction = RouterAction | LocationChangeAction;

export type RootAction = ReactRouterAction | LinkAction;


/***********************

 * root epic

 ***********************/

const composeEnhancers =

  (process.env.NODE_ENV === 'development' &&

    (window as any) &&

    (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ||

  compose;


const rootEpic = combineEpics(linkEpic);

const epicMiddleware = createEpicMiddleware();


/***********************

 * root store

 ***********************/

function configureStore(initialState?: object) {

  // configure middlewares

  const middlewares = [epicMiddleware];

  // compose enhancers

  const enhancer = composeEnhancers(applyMiddleware(...middlewares));

  // create store

  const store = createStore(rootReducer, enhancer);

  // run epic: https://redux-observable.js.org/docs/basics/SettingUpTheMiddleware.html

  epicMiddleware.run(rootEpic);

  return store;

}


const store = configureStore();

export default store;


애플리케이션에 store를 최종 설정한다. 

// client/main.tsx

import * as React from 'react';

import { Meteor } from 'meteor/meteor';

import { render } from 'react-dom';

import { Provider } from 'react-redux';


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

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


const Root = (

  <Provider store={store}>

    <App />

  </Provider>

);


Meteor.startup(() => {

  render(Root, document.getElementById('react-target'));

});


window 객체 인식을 위해 type definition을 정의한다. 

  - 루트에 typings 폴더 생성

  - typings/index.d.ts 파일 생성

declare var window: any;




Application에서 Redux Action 호출하기


add/remove link 리덕스 액션을 애플리케이션에 적용한다. link.service.ts 는 사용하지 않는다. 

  - AddLink.tsx에 addLink 액션 적용

// imports/ui/link/AddLink.tsx

import * as React from 'react';

import { connect } from 'react-redux';

import { addLink } from './link.action';


export interface AddLinkProps {

  addLink: Function;

}


class AddLink extends React.Component<AddLinkPropsany> {

  handleSubmit = (eany=> {

    //ignore validation

    e.preventDefault();

    const param = {

      title: e.target.title.value,

      url: e.target.url.value

    }

    const { addLink } = this.props;

    addLink(param);

  };


  public render() {

    return (

      <form onSubmit={this.handleSubmit} >

        <input type="text" name="title" placeholder="title" />

        <input type="text" name="url" placeholder="url" />

        <button>Add Link</button>

      </form>

    )

  }

}


export default connect(undefined, { addLink })(AddLink);


  - Link.tsx에 removeLink 액션 적용

// imports/ui/pages/link/Link.tsx

import * as React from 'react';

import { removeLink } from './link.action';

import { connect } from 'react-redux';


export interface LinkProps {

  link: any,

  removeLink: Function

}

class Link extends React.Component<LinkPropsany> {

  removeLink = () => {

    const { link, removeLink } = this.props;

    removeLink(link._id);

  }


  public render() {

    const { link } = this.props;

    return (

      <li key={link._id}>

        <a href={link.url} target="_blank">{link.title}</a>

        <button onClick={this.removeLink}> x </button>

      </li>

    );

  }

}


export default connect(undefined, { removeLink })(Link);





<참조>

블로그 소스 코드

react-redux-typescript-guide 소스

redux-actions 사용하기

redux-observable 사용하기

typesafe-actions 사용하기

reselect 사용하여 성능 최적화 하기

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

Meteor

Meteor v1.8이 되면서 CLI에 react기반 애플리케이션 자동 생성 기능이 추가되었다. 여기에 Typescript와 Redux을 접목해서 개발환경을 꾸며 본다. 


  - React기반 Meteor 애플리케이션 생성

  - SCSS 환경설정

  - Typescript 환경설정

  - 미티어의 Reactivity Computation인 withTracker 확인

  - AntDesign CSS Framework 설정

  - 소스코드




React 기반환경 만들기 


meteor를 설치한다. 

// Node Version Manager를 통해 Node LTS 버전을 설치한다. 최신 Meteor 1.8 은 Node v8.*을 사용한다.

$ nvm install 8.12.0


// meteor를 설치한다. 

$ curl https://install.meteor.com | sh


// 향후 meteor v1.8.1 버전이 릴리즈 되면 성능향상을 위해 업그레이드를 반드시 수행한다. 

$ meteor update --release 1.8.1


애플리케이션 생성

// 명령어: meteor create --react <AppName>

$ meteor create --react react-first

$ cd react-first

$ meteor run




SCSS 환경 전환


.css파일을 .scss파일로 전환한다. 이를 지원하기 위해 scss 패키지를 설치한다. 

meteor add fourseven:scss




Typescript 관련 모듈 설치


meteor의 typescript 지원 패키지를 설치한다.

meteor add barbatus: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 typescript

$ meteor npm install --save-dev tslint

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




Typescript 환경 전환


root에 tsconfig.json 파일 추가

{

    "compilerOptions": {

        "target": "es6",

        "allowJs": true,

        "jsx": "preserve",

        "moduleResolution": "node",

        "types": [

            "node"

        ]

    }

}


root에 tslint.json 파일 추가 

// 과도한 lint가 귀찮아 extends는 주석처리함 

{

    // "extends": [

    //     "tslint:latest",

    //     "tslint-react"

    // ],

    "rules": {

        "quotemark": [

            true,

            "single",

            "jsx-double"

        ],

        "ordered-imports": false,

        "no-var-requires": false

    }

}


root의 client와 imports, server 폴더안의 파일 명칭 .jsx / .js 를 .tsx / .ts 로 변경한다. tests 폴더는 .js 그대로 둔다.



마직막으로 package.json에서 mainModule의 client를 main.tsx로 수정한다. 





.tsx 파일 내용을 typescript에 맞게 수정하기


1) import 문 수정

// client, imports 폴더안의 모든 .tsx 파일


// react import

import React from 'react'; ==> import * as React from 'react';


// 절대경로를 상대경로로 수정

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


// .jsx 파일을 확장자 제거 App.tsx

import Hello from './Hello.jsx' ==> import Hello from './Hello';

import Info from './Info.jsx' ==> import Info from './Info';


2) class 문 수정

class Hello extends Components {  ==> class Hello extends React.Component {


3) class에 Props 타입 설정

// info.tsx


interface InfoProps {

  links: any;

}


class Info extends React.Component<InfoProps, any> {

  ...

}


4) export default 문 수정

    withTracker는 Meteor의 Reactivity Computation 공간이다. Computation에는 Meteor의 Reactivity Resource가 위치한다. 즉, source가 변경되면 Computation영역이 재실행된다. 

// imports/ui/info.tsx 

export default InfoContainer = withTracker(...)

   ==> 

export default withTracker(...);


// imports/api/links.ts

export default Links = new Mongo.Collection('links');

   ==>

const Links = new Mongo.Collection('links');

export default Links;


이제 다시 meteor run 을 실행한다. 



Ant Design 설치하기


CSS Component를 제공하는 antd를 설치한다. 

$ meteor npm install --save antd

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


client/main.tsx 안에 antd css 를 import 한다. 

import '../node_modules/antd/dist/antd.css';


또는, antd.less 파일을 직접 import할 수 있다. 

$ meteor add less

$ meteor npm install --save indexof


// client/theme.less 파일 생성하고 antd.less 파일 import하기

@import '{}/node_modules/antd/dist/antd.less'; 


antd의 Menu를 App.tsx에 적용해 본다. 

import * as React from 'react';

import Hello from './Hello';

import Info from './Info';

import { Menu, Icon } from 'antd';


const SubMenu = Menu.SubMenu;

const MenuItemGroup = Menu.ItemGroup;


class App extends React.Component {

  state = {

    current: 'mail',

  };


  handleClick = (e=> {

    console.log('click ', e);

    this.setState({

      current: e.key,

    });

  }


  render() {

    return (

      <div>

        <Menu

          onClick={this.handleClick}

          selectedKeys={[this.state.current]}

          mode="horizontal"

        >

          <Menu.Item key="mail">

            <Icon type="mail" />Navigation One

            </Menu.Item>

          <Menu.Item key="app" disabled>

            <Icon type="appstore" />Navigation Two

            </Menu.Item>

          <SubMenu title={<span className="submenu-title-wrapper"><Icon type="setting" />Navigation Three - Submenu</span>}>

            <MenuItemGroup title="Item 1">

              <Menu.Item key="setting:1">Option 1</Menu.Item>

              <Menu.Item key="setting:2">Option 2</Menu.Item>

            </MenuItemGroup>

            <MenuItemGroup title="Item 2">

              <Menu.Item key="setting:3">Option 3</Menu.Item>

              <Menu.Item key="setting:4">Option 4</Menu.Item>

            </MenuItemGroup>

          </SubMenu>

          <Menu.Item key="alipay">

            <a href="https://ant.design" target="_blank" rel="noopener noreferrer">Navigation Four - Link</a>

          </Menu.Item>

        </Menu>

        <Hello />

        <Info />

      </div>

    );

  }

}


export default App;




지금까지의 적용 소스코드, Redux 환경 설정하기는 다음 글에서..




<참조>


meteor-react-typescript 코드 샘플

meteor changelog

meteor에 antd .less 파일 import하기 (데모소스)

meteor-react-typescript-boilerplate 소스