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

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

[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] 개발환경 만들기 - 3

Meteor

Meteor의  accounts-password를 패키지를 통한 로그인 환경 만들기를 해본다. 


  - accounts-password 패키지를 통한 미티어 사용자 가입/로그인 사용 방법 알기

  - React Router 설정

  - 미티어의 Tracker.autorun을 통해 reactivity computation과 resource 사용 방법 알기

  - 소스코드




미티어 사용자 관리


accounts-password 패키지를 설치하고, 가입 및 로그인 화면을 만든다. 

$ meteor add accounts-password

$ meteor npm install --save bcrypt

// Link 사용을 위해 

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


Signup.tsx와 Login.tsx 생성한다. 

  - email, password의 값을 읽기 위하여 typescript방식의 public refs를 정의한다. 

  - Accounts.createUser를 통해 사용자를 등록한다.

// Singup.tsx

import * as React from 'react';

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

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


export interface SignupProps {}

export default class Signup extends React.Component<SignupProps, any> {

  // @see https://goenning.net/2016/11/02/strongly-typed-react-refs-with-typescript/

  public refs: {

    email: HTMLInputElement,

    password: HTMLInputElement

  }


  constructor(props) {

    super(props);

    this.state = {

      error: ''

    };

  }


  onCreateAccount = (eany=> {

    e.preventDefault();

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

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

    Accounts.createUser({email, password}, (err=> {

      if (err) {

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

      } else {

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

      }

    });

  }


  public render() {

    return (

      <div>

        <h1>Signup to short Link</h1>

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

        <form onSubmit={this.onCreateAccount}> 

          <input type="email" ref="email" name="email" placeholder="Email"/>

          <input type="password" ref="password" name="password" placeholder="Password"/>

          <button>Create Acount</button>

        </form>

        <Link to="/login">Already have a account?</Link>

      </div>

    );

  }

}


Login.tsx

  - refs 정의

  - Meteor.loginWithPassword를 통해 로그인한다.

import * as React from 'react';

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

import { Meteor } from 'meteor/meteor';


export interface LoginProps {

  history: any;

}

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

  public refs: {

    email: HTMLInputElement,

    password: HTMLInputElement

  }


  constructor(props) {

    super(props);

    this.state = {

      error: ''

    };

  }


  onLogin = (eany=> {

    e.preventDefault();

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

    let password = this.refs.password.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="email" name="email" placeholder="Email" />

          <input type="password" ref="password" name="password" placeholder="Password" />

          <button>Login</button>

        </form>

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

      </div>

    );

  }

}


NotFound.tsx 파일도 생성한다. 

import * as React from 'react';

export default () => <div>Not Found Page</div>




React Router 설정


라우팅 설정을 통해 가입, 로그인후 Link화면으로 이동하는 설정한다. 


history 모듈을 설치한다.

$ meteor npm install --save history


client/main.tsx에 Route설정을 한다.

import * as React from 'react';

import { Meteor } from 'meteor/meteor';

import { render } from 'react-dom';

import { Provider } from 'react-redux';

import { Route, Router, Switch } from 'react-router-dom';

import { createBrowserHistory } from 'history';

 

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

import Login from '../imports/ui/pages/Login';

import Signup from '../imports/ui/pages/Signup';

import InfoContainer from '../imports/ui/Info';

import NotFound from '../imports/ui/pages/NotFound';

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


const browserHistory = createBrowserHistory();

const Root = (

  <Provider store={store}>

    <Router 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>

    </Router>

  </Provider>

);


Meteor.startup(() => {

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

});


links는 인증된 사용자만 볼 수 있으므로 Tracker.autorun이라는 Reactivity Computation 영역에서 Meteor.user()라는 Reactivity Source가 로그인 상태인지 검사한다. 

  - 로그인 상태가 아니면 무조건 login화면으로 이동

  - 로그인 상태이면서 인증이 필요없는 화면인 경우 link화면으로 이동

// client/main.tsx 

import { Tracker } from 'meteor/tracker';


Tracker.autorun(() => {

  const isAuthenticated = !!Meteor.user();

  if (isAuthenticated) {

    const pathname = browserHistory.location.pathname;

    const unauthenticatedPages: any = ['/', '/signup'];

    const isUnauthenticatedPage = unauthenticatedPages.includes(pathname);

    if (isUnauthenticatedPage) {

      browserHistory.push('/links'); // main

    } else {

      browserHistory.push(pathname);

    }

  } else {

    browserHistory.push('/'); // login

  }

});


Meteor.startup(() => {

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

});


imports/ui/Info.tsx 에 logout 기능 추가

...

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


interface InfoProps {

  links: any;

  loading: boolean

}


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


  linkList() {

    const { links, loading } = this.props;

    if (loading) {

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

    } else {

      return <LinkList links={links} />

    }

  }


  onLogout = () => {

    Accounts.logout();

  }


  render() {

    return (

      <div>

        <button onClick={this.onLogout}>Log out</button>

        <AddLink />

        {this.linkList()}

      </div>

    );

  }

}




가입 및 로그인 테스트 확인하기


가입을 하고, 로그인을 한다. 

Chrome Extension 으로 MiniMongoExplorer를 설치하고 user Collection의 내용을 확인한다. 


또는 Chrome DevTool의 Application에서 로그인 사용자의 userId와 console에서 확인해 볼 수 있다. 또한 Link페이지에서 로그아웃을 해보고 콘솔을 확인해 본다. 




<참조>

- 크롬 확장툴 MiniMongoExplorer

세번째 글 소스