블로그 이미지
윤영식
Full Stacker, Application Architecter, KnowHow Dispenser and Bike Rider

Publication

Category

Recent Post

2021. 8. 28. 12:24 React/Start React

React 외에 애플리케이션 개발을 위한 라이브러리를 설치한다.

 

 

 

 

 

 

UI Components

PrimeReact, EUI, MaterialUI, AntD 검토후 소스레벨 최신으로 반영하고 있고, 다양한 비즈니스 UX 대응 가능한 AntD를 선택한다.

// UI Component
$> yarn add antd

// Icon
$> yarn add @ant-design/icons

AntD의 스타일을 apps/tube-csr/src/styles.scss에 import 하고, app.tsx에 AntD의 Button 컴포넌트를 테스트 해본다. styles.scss는 글로벌 스타일로 apps/tube-csr/project.json 환경파일에 설정되어 있다.

// styles.scss
@import 'antd/dist/antd.css';


// app.tsx
import { Button } from 'antd';

import styles from './app.module.scss';

export function App() {
  return (
    <div className={styles.app}>
      <Button type="primary">Primary Button</Button>
      <Button>Default Button</Button>
      <Button type="dashed">Dashed Button</Button>
      <br />
      <Button type="text">Text Button</Button>
      <Button type="link">Link Button</Button>
    </div>
  );
}

export default App;

테스트 서버를 수행하고 확인해 본다. 

$> nx serve tube-csr

 

 

라우터 설치

CSR의 Router를 위한 react-router, react-router-dom을 설치한다.

$> yarn add react-router react-router-dom

 

 

Ajax를 위한 라이브러리 설치

Client/Server 모두 사용할 수 있는 Axios 를 설치하고 rxjs를 통해 api 를 제어할 것이다. 

$> yarn add axios

 

테스트 프로그램 작성

// dog.tsx
import { List } from 'antd';
import Axios, { AxiosResponse } from 'axios';
import { useEffect, useState } from 'react';

function DogList() {
  const [dogList, setDogList] = useState<string[]>([]);

  useEffect(() => {
    Axios.get('https://dog.ceo/api/breeds/list').then((result: AxiosResponse) => {
      setDogList(result.data.message);
    });
  }, []);

  return (
    <List
      header={<div>Header</div>}
      footer={<div>Footer</div>}
      bordered
      dataSource={dogList}
      renderItem={(item: string) => <List.Item>{item}</List.Item>}
    />
  );
}

export default DogList;


// app.tsx 에 추가
<DogList />

axios의 처리에 대해 rxjs 라이브러리를 같이 사용해 본다. 

$> yarn add rxjs

axios와 rxjs의 Observable을 사용한 라이브러리를 구현한다. 라이브러리는 NX의 libs 폴더 밑으로 생성한다. 

$> nx g @nrwl/react:lib core

// 실행을 하면 루트의 libs/core 밑으로 package가 생성된다.

libs/core/src밑으로 ajax 폴더를 생성하고 http.service.ts 파일을 생성한다. 

  • Axios와 Observable을 조합
  • Axios cancel 적용
// libs/core/src/lib/ajax/http.service.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import axios, {
  AxiosError,
  AxiosInstance,
  AxiosPromise,
  AxiosRequestConfig,
  AxiosResponse,
  CancelTokenSource,
} from 'axios';
import { Observable, Observer } from 'rxjs';

// sample url: https://jsonplaceholder.typicode.com/users
interface RequestArgs {
  method: HttpMethod;
  url: string;
  queryParams?: any;
  payload?: any;
}

enum HttpMethod {
  GET = 'GET',
  POST = 'POST',
  PUT = 'PUT',
  PATCH = 'PATCH',
  DELETE = 'DELETE',
}

export class HttpService {
  private httpClient!: AxiosInstance;
  private cancelTokenSource!: CancelTokenSource;
  private options!: AxiosRequestConfig;
  private completed!: boolean;

  get<T>(url: string, queryParams?: any, options: AxiosRequestConfig = {}): Observable<T> {
    this.setOptions(options);
    return this.executeRequest<T>({ method: HttpMethod.GET, url, queryParams });
  }

  post<T>(url: string, payload: any, options: AxiosRequestConfig = {}): Observable<T> {
    this.setOptions(options);
    return this.executeRequest<T>({
      method: HttpMethod.POST,
      url,
      payload,
    });
  }

  put<T>(url: string, payload: any, options: AxiosRequestConfig = {}): Observable<T> {
    this.setOptions(options);
    return this.executeRequest<T>({
      method: HttpMethod.PUT,
      url,
      payload,
    });
  }

  patch<T>(url: string, payload: any, options: AxiosRequestConfig = {}): Observable<T> {
    this.setOptions(options);
    return this.executeRequest<T>({
      method: HttpMethod.PATCH,
      url,
      payload,
    });
  }

  delete<T>(url: string, options: AxiosRequestConfig = {}): Observable<T> {
    this.setOptions(options);
    return this.executeRequest<T>({
      method: HttpMethod.DELETE,
      url,
    });
  }

  cancel(forcely = false): void {
    if (!this.completed || forcely) {
      this.cancelTokenSource.cancel(`${this.options.url} is aborted`);
    }
  }

  private setOptions(options: AxiosRequestConfig = {}): void {
    if (this.options) {
      this.options = { ...this.options, ...options };
    } else {
      this.options = options;
    }
    this.cancelTokenSource = axios.CancelToken.source();
    this.httpClient = axios.create({ ...options, cancelToken: this.cancelTokenSource.token });
    this.completed = false;
  }

  private executeRequest<T>(args: RequestArgs): Observable<T> {
    const { method, url, queryParams, payload } = args;
    let request: AxiosPromise<T>;
    switch (method) {
      case HttpMethod.GET:
        request = this.httpClient.get<T>(url, { params: queryParams });
        break;
      case HttpMethod.POST:
        request = this.httpClient.post<T>(url, payload);
        break;
      case HttpMethod.PUT:
        request = this.httpClient.put<T>(url, payload);
        break;
      case HttpMethod.PATCH:
        request = this.httpClient.patch<T>(url, payload);
        break;
      case HttpMethod.DELETE:
        request = this.httpClient.delete<T>(url);
        break;
    }

    return new Observable<T>((observer: Observer<T>) => {
      request
        .then((response: AxiosResponse) => {
          observer.next(response.data);
        })
        .catch((error: AxiosError | Error) => {
          this.cancel();
          observer.error(error);
          if (axios.isAxiosError(error)) {
            console.log(error.code);
            if (error.response) {
              console.log(error.response.data);
              console.log(error.response.status);
              console.log(error.response.headers);
            }
          } else {
            console.log(error.message);
          }
        })
        .finally(() => {
          this.completed = true;
          observer.complete();
        });
    });
  }
}

export const httpService = new HttpService();

다음으로 apps/tube-csr/src/app/dog.tsx에 테스트 코드를 입력한다. httpService를 import할 때 from 구문이 Local file system에 있다하더라도 마치 node_modules에서 import하는 것처럼 위치투명성을 보장한다. 이는 루트에 위치한 tsconfig.base.json 파일에 lib 생성할 때 자동 등록된다. 

// dog.tsx

import { useEffect, useState } from 'react';

import { List } from 'antd';
// import Axios, { AxiosResponse } from 'axios';

import { httpService } from '@rnn-stack/core';

function DogList() {
  const [dogList, setDogList] = useState<string[]>([]);

  useEffect(() => {
    httpService.get('https://dog.ceo/api/breeds/list').subscribe((response: any) => {
      console.log('axios observable response:', response);
      setDogList(response.message);
    });
    // Axios.get('https://dog.ceo/api/breeds/list').then((result: AxiosResponse) => {
    //   setDogList(result.data.message);
    // });
  }, []);

  return (
    <List
      header={<div>Header</div>}
      footer={<div>Footer</div>}
      bordered
      dataSource={dogList}
      renderItem={(item: string) => <List.Item>{item}</List.Item>}
    />
  );
}

export default DogList;

tsconfig.base.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "rootDir": ".",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es2015",
    "module": "esnext",
    "lib": ["es2017", "dom"],
    "skipLibCheck": true,
    "skipDefaultLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@rnn-stack/core": ["libs/core/src/index.ts"]   <== 요놈
    }
  },
  "exclude": ["node_modules", "tmp"]
}

다음 포스팅에서 계속 진행...

 

소스: https://github.com/ysyun/rnn-stack/releases/tag/hh-3 

 

Release hh-3 · ysyun/rnn-stack

hh-3 add libraries and http.service.ts

github.com

 

 

posted by 윤영식
2018. 11. 16. 16:07 Meteor/React + 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({title, url}) {

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

  }


  removeLink(_id: string) {

    Links.remove(_id);

  }


  updateLink(_id: string, {title, url}) {

    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 = (e: any) => {

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

$ 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 = (id: string) => action(DELETE_REQUEST, id);

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

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

export const changeLink = (id: string) => 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, (links, linkFilter) => {

  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(collection: any, params: any): Observable<RequestModel> {

  return from(new Promise((resolve, reject) => {

    collection.insert(params, (error, result) => {

      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(collection: any, _id: string): Observable<RequestModel> {

  return from(new Promise((resolve, reject) => {

    collection.remove(_id, (error, result) => {

      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<AddLinkProps, any> {

  handleSubmit = (e: any) => {

    //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<LinkProps, any> {

  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 사용하여 성능 최적화 하기

posted by 윤영식
2018. 11. 13. 16:42 Meteor/React + 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 소스

posted by 윤영식
2018. 9. 14. 17:31 React

실제 프로젝트에서 사용을 안하다보니 자꾸 처음 내용이 익숙해 지지 않은 상태에서 잊어버리고 만다. React + Typescript기반을 다시 시작해 본다. 



Install React with Typescript

React와 Typescript 환경을 만든다

// 2018.9 현재 LTS NodeJS 버전

$ nvm use 8.12.0

// typescript v3.0.3 으로 업데이트

$ npm i -g typescript 

$ npm i -g create-react-app (or yarn global add create-react-app)


// TYPESCRIPT-CSS 

$ create-react-app my-app --scrips-version=react-scripts-ts


package.json에 eject기능이 있어서 webpack config를 밖으로 추출하여 직접 핸들링할 수 있게 한다. 

$ cd my-app

$ npm run eject



SCSS 환경구축

css 환경을 scss 환경으로 바꿔준다. 

// scss loader 설치

$ yarn add node-sass sass-loader --dev


App.css와 index.css의 확장자를 .scss로 바꾸고, App.tsx, index.tsx의 import 문 확장자를 .scss로 바꾼다. 

import './App.scss';


config/webpack.config.dev.js 와 webpack.config.prod.js 파일안에 scss설정을 추가한다. 빨간 부분을 

      {

            test: /\.(css|scss)$/,

            use: [

              require.resolve('style-loader'),

              {

                loader: require.resolve('css-loader'),

                options: {

                  importLoaders: 1,

                },

              },

              {

                loader: require.resolve('postcss-loader'),

                options: {

                  // Necessary for external CSS imports to work

                  // https://github.com/facebookincubator/create-react-app/issues/2677

                  ident: 'postcss',

                  plugins: () => [

                    require('postcss-flexbugs-fixes'),

                    autoprefixer({

                      browsers: [

                        '>1%',

                        'last 4 versions',

                        'Firefox ESR',

                        'not ie < 9', // React doesn't support IE8 anyway

                      ],

                      flexbox: 'no-2009',

                    }),

                  ],

                },

              },

              {

                loader: require.resolve("sass-loader"),                

                options: { } 

              }

            ],

      },


기존 App.scss내용을 다음과 같이 바꾸어 확인해 본다. 

.App {

  text-align: center;

  &-logo {

    animation: App-logo-spin infinite 20s linear;

    height: 80px;

  }


  &-header {

    background-color: rgb(197, 40, 40);

    height: 150px;

    padding: 20px;

    color: white;

  }


  &-title {

    font-size: 1.5em;

  }


  &-intro {

    font-size: large;

  }

}

yarn start하여 점검!



Component LifeCycle


일반 컴포넌트

  constructor -> componentWillMount -> render -> componentDidMount -> componentWillUnmount


props, state 사용 컴포넌트

  componentWillReceiveProps -> shouldComponentUpdate -> componentWillUpdate -> render -> componentDidUpdate

  - shouldComponentUpdate: true, false로 다음으로 이벤트를 넘길수도 안할수도 있음




Component Type

PureComponent

  shouldComponentUpdate에서 Shallow compare하므로 reference를 바꾸어 주어야 render가 호출됨. 


Functional Component (Stateless Function Component)

  const myComp: Reat.SFC<Props> = (props) => {...}

import * as React from“ react”

interface WelcomeProps {

  name: string,

}


const Welcome: React.SFC < WelcomeProps > = (props) => {

  return <h1 > Hello, {

    props.name

  } < /h1>;

}



Router for SPA

react-router v4

   BrowserRouter, Route, Link, NavLink, Redirect 사용

   BrowserRouter는 window.history.pushState() 로 동자하는 라우터

   RouteComponentProps: route되면서 url의 파라미터를 props로 받음. history, match 속성을 가짐.

<BrowserRouter>

   <Route exact={true} path="/" component={} or render={} or children={} >

      <Link to="/a" />

      <NavLink activeStyle={{ color: red }} to="/b" /> 

   </Route>

   <Redirect from="/a" to="/b" />

</BrowserRouter>


Switch로 감쌈

  <Switch>

    <Route ... />

    <Route ... /> 

    <Route ... />

 </Switch>



Redux

react-redux

  여러  action을 reducer를 통해 하나의 store를 만든다.

     - action 타입을 만들고 action 객체를 만드는 펑션을 (action creator) 정의한다.

     - action을 처리하는 reducer 펑션을 정의한다. (Pure function, immutable)

        reducer 펑션을 action별로 나눈 다음 사용시에는 combineReducers 로 합쳐서 사용한다. 

     - reducer를 createStore에 넣어주면 single Store가 만들어진다. 

        store에는 getState(), dispatch(액션), subscribe(리스너),  replaceReducer(다른리듀서) 4개 메소드가 있음

   redux를 react에 연결하기

      - componentDidMount: subscribe

      - componentWillUnMount: unsubscribe

      - Provider는 context에 store를 담아 모든 component에서 store를 사용할 수 있도록 한다. 

         Provider를 제공하는 react-redux를 사용한다.  

   connect를 통해 컴포넌트에 연결한다. 

      - App에 대한 High order component이다.

      - 전체 state의 모양과 action creator를 파라미터로 넣어준다.

$ yarn add redux react-redux @types/redux @types/react-redux


const mapStateToProps = (state: { age: number}) => {

  return {

     age: state.age

  }

}


const mapDispatchToProps = (dispatch: Function) => {

  return {

      onAddAge: () => { dispatch(addAge()); }

  };

}


interface AppProps {

  age: number;

  onAddAge: void;

}


connect(mapStateToProps, mapDispatchToProps)(App);


class App<{props: AppProps}> extends Component {

    return <div>{this.props.age}</div>

}


  dispatch안에서 async처리를 할 수 있다.   

  applyMiddleware는 dispatch 전후에 처리할 수 있다. 

  action이 async일 때 미들웨어인 redux-thunk를 이용한다.



<참조>

- 2017 Typescript Korea 강의 (유튜브)

- scss 적용하기

- scss loader 설정하기, Class 사용 in Velopert

- Typescript + React 환경 만들기 in Velopert

- React LifeCycle 설명

- Component Type - SFC 설명

- React Router in Velopert 소개





'React' 카테고리의 다른 글

[React] Semantic-UI를 React 컴포넌트로 만들기 - 1  (0) 2015.08.23
[React] CSS Framework 선정하기  (0) 2015.08.15
[Flux] Flux 배우는 방법  (0) 2015.07.04
[React] AngularJS와 ReactJS 비교  (0) 2015.07.01
[React] 배우는 방법  (0) 2015.05.15
posted by 윤영식
2017. 3. 29. 15:05 Angular/Concept

NodeJS에서 Typescript를 사용하기 위한 빠른 환경 설정 방법을 정리한다.



설치

먼저 테스트를 위한 폴더를 만들고 Node환경을 만든다. package.json을  파일 생성한다.

$ npm init -y


필요한 패키지를 설치한다.

$ npm i -g typescript@latest


$ npm i -g ts-node


$ npm i @types/node --save-dev




환경설정


Typescript 환경파일을 생성한다.

$ tsc --init


tsconfig.json파일 환경을 설정한다. typeRoots와 exclude를 추가한다. (tsocnfig.json의 상세내역 참조)

{

    "compilerOptions": {

        "module": "commonjs",

        "target": "es5",

        "noImplicitAny": false,

        "sourceMap": false

    },

    "typeRoots": ["node_modules/@types"],

    "exclude": [

        "node_modules"

    ]

}




테스트


테스트 파일을 생성한다.

$ touch index.ts

$ vi index.ts


const hi = 'hello peter';

console.log(hi);


테스트를 위해 소스 변경을 런타임시에 체크하고 적용하는 nodemone을 설치한다. 

$ npm i nodemon --save-dev


테스트 스크립트를 package.json의 scripts 항목에 추가한다.

$ vi package.json


{

...

  "scripts": {

    "start": "npm run build:live",

    "build:live": "nodemon --exec ./node_modules/.bin/ts-node  ./index.ts"

  },

...

}


테스트한다.

$ npm start


> typescript@1.0.0 start /Users/dowonyun/prototyping/typescript

> npm run build:live

> typescript@1.0.0 build:live /Users/dowonyun/prototyping/typescript

> nodemon --exec ./node_modules/.bin/ts-node ./index.ts


[nodemon] 1.11.0

[nodemon] to restart at any time, enter `rs`

[nodemon] watching: *.*

[nodemon] starting `./node_modules/.bin/ts-node ./index.ts`

hello peter

[nodemon] clean exit - waiting for changes before restart



또는 ts-node를 설치해서 수행해도 된다.

$ npm install --save ts-node 

$ ts-node ./index.ts



참조

https://basarat.gitbooks.io/typescript/docs/quick/nodejs.html

- https://nodemon.io/

- https://github.com/TypeStrong/ts-node


posted by 윤영식
2016. 10. 23. 22:59 Angular/Concept

Angular v2가 정식 릴리즈되었다. Angular v1 은 Two-way data-binding 이라는 독특한 특징으로 인해 많은 사용자 층을 확보했지만 장점 만큼이나 성능상의 단점도 존재했었다. 또한 처음엔 쉬운듯 하면서 좀 더 깊게 들어가볼려고 하면 학습곡선이 갑자기 껑충뛰기도 했다. 가장 많이 사용했던 Directive(지시자)가 대표적이다. 많은 개발자가 만들어 놓은 지시자를 쉽게 가져다 쓸 수는 있지만 직접 만들어 애플리케이션에 접목하려 할 때 첫 문턱을 만나게 된다. 그리고 jQuery사용에 익숙한 개발자에게 Angular v1 시점상의 차이로 Angular v1 방식의 개발패턴을 요구하기도 했다. 관성은 무섭다. 기존에 사용하던 방식을 버리고 Angular v1에 맞춰서 애플리케이션을 만들어 가기란 곤혹스럽다. Angular v2 또한 그런 인식의 전환을 요구할까? 그렇다 그리고 아니다. 






웹 애플리케이션 흐름

웹 애플리케이션 개발을 위해 우리가 사용하는 jQuery같은 라이브러리나 Angular, Backbone같은 프레임워크의 가장 1차적인 목적은 무엇일까? 나는 Data Projection이라 생각한다. 데이터를 화면에 출력하기 위해 DOM을 얼마나 쉽게 조작하고 상호 작용할 수 있느냐가 선택의 기준이라 생각한다. Data Projection을 일관되고 확장가능하고 배포가능하게 하는 방식으로 기술은 발전해 왔고, 현재는 화면에 대한 제어방식이 컴포넌트 기반 방식으로 발전해 오고 있다. 


Data Projection의 역사를 보면 초장기엔 Server Side Rendering 를 사용해 웹 애플리케이션을 개발했다. 예로 JSP, PHP, ASP 같이 서버 미들웨어서 데이터를 조회하고 HTML을 조작하여 결과 HTML을 브라우져에 전송하던 시대이다. 




1세대에는 AJAX가 나오고 다양한 라이브러리나 프레임워크가 나왔다. 이때는 데이터변경에 대한 DOM반영이 서버에서 클라이언트 개발자의 몫으로 넘어오게 되었다. 즉, 직접 DOM 을 얻어와서 특정 위치에 넣어 주어야 했고, DOM에서 발생하는 이벤트를 Listening해서 처리하고 DOM에 반영하는 모든 작업이 웹 개발자가 직접 코딩하던 단계였다. Java의 프레임워크 역사로 보면 Struts 로 비유할 수 있지 않을까 싶다. 




2세대로 넘어오게 되면 Model을 DOM 에 반영하는 방식은 자동화 된다. 여기에 대표적인 프레임워크가 Ember 와 Angular v1 이다. 이때 부터 Single Page Application (SPA) 개발이라는 용어가 나오게 된다. URI 변경에 대한 대응으로 Routing  개념이 나오고, Data Projection후 원하는 일부 DOM을 변경하는 역할이 프레임워크로 넘어갔고, 웹 개발자는 좀 더 애플리케이션 비즈니스 로직에 집중토록 만들었다. Java 프레임워크로 비유하자면 Struts와 Spring Framework 초기버전의 중간 지대 정도 쯤이라 생각한다.  이때부터 Frontend (프론트앤드)라는 직군이 웹 개발자와 분리되기 시작한 지점이라 생각한다. 이에 대한 자세한 설명은 태곤님이 작성한 "[번역] 프론트엔드 개발자는 왜 구하기 어렵나요?"를 참조하자. 2011년을 기점으로 2013년 웹 애플리케이션 프레임워크가 정착을 해가는 시기였고, 현재는 대부분의 스타트업이나 중견기업에서 2세대 웹 애플리케이션 프레임워크를 선택할 경우 프론트엔드 개발자와 백앤드 개발자를 구분하여 팀을 구성하고 있는 추세이다.





3세대는 2세대의 과도기를 거쳐 2세대의 장점을 흡수 하면서 성능상의 이슈를 해결하고, 점점 복잡해 지고있는 웹 애플리케이션을 보다 직관적이고 쉽게 개발할 수 있게 노력하고 있다. 대표적인 프레임워크로는 Facebook의 React와 Google의 Angular v2 (이하 Angular)이다. Angular는 Component기반 개발 방식으로 표준인 Web Components를 지원하며 Typescript를 기본 언어로 채택했다. Typescript는 Type 시스템을 제공하기 때문에 개발단계에서 버그의 가능성을 쉽게 찾을 수 있도록 도와준다. React와 Angular에 대한 장단점은 손창욱님의 "React보다 Angular v2에 더 주목해야 하는 이유"를 참조하자. Java의 Spring Framework이 성숙하면서 Annotation 같은 기능이 추가되듯, Angular v2 프레임워크는 Java의 Spring 프레임워크 최신버전과 비유할 수 있다. 



Angular v1에 대한 개발 및 컨설팅을 3년 가까이 하면서 올해 초 Angular v2를 공부하고 기존 v1 코드를 v2 코드로 전환하면서 코드 베이스는 50%가량 줄었고, 반응속도는 30%가량 개선되었다. 8명 프론트앤드 개발자와 컨버전을 진행하면서 이구동성으로 말하는 것은 "코드가 직관적으로 변했다. 코드량이 현저히 줄었다. Typescript의 타입체킹으로 인해 실수를 최소화 할 수 있었다" 이다. 



Angular v2 왜 배워야 하는가?

Angular를 왜 배워야 하는가? 답하자면 안배워도 된다. 단순 홈페이지나 업무 화면이라면 쉽고 더 빨리 만들 수 있는 워드프레스나 서비스를 이용하거나 DOM 핸들링 라이브러리나 플러그인을 사용해 개발하는 편이 낫다. 하지만 솔루션의 복잡한 요구사항을 지속적으로 반영해야 하고 DOM제어가 복잡해 질 가능성이 높다면 jQuery, React 같은 라이브러리 보다는 Angular 같은 프레임워크를 선택하는 것이 좋다. 그리고 최근에는 ES2015 표준이 확정되었고 최신 브라우져에 대부분 기능이 구현되고 있다. 2세대와 3세대 Data Projection의 가장 큰 개발 방식의 차이는 ES2015의 이해에서부터 시작한다.  즉, ES2015 문법을 잘 알고 사용하면 좀 더 쉽고 간단하게 코드 베이스를 유지하면서 오류를 최소화할 수 있다. 예로 -> 펑션은 this에 대한 오류를 방지하고, Set/Map등 Collection은 Java의 Collection과 유사한다. 



Angular v2 시작하면 초기에 배워야 하는 것들이 갑자기 늘어난다. 이것은 2세대와 3세대의 개발 패턴이 바뀌었음을 시사한다. ES2015 문법은 그대로 TypeScript에 녹아 있고, Type System과 Annotation 기능이 녹아 들어 더욱 편리한 개발을 가능토록 한다. 따라서 ES2015의 Syntax와 개념을 이해해야 한다. 그리고 Typescript를 다시 공부해야 한다. 또한 요즘 인기를 누리고 있는 Reactive Programming을 표방한 대표적인 라이브러리인 RxJS를 Angular가 근간으로 사용하고 있다. 따라서 RxJS 에 대한 개념과 사용법을 익혀야 한다. 그런후 Web Components 란 무엇인지 알아야 하고, Angular 프레임워크의 아키텍쳐를 구성하는 개념인 Change Detection 동작원리, Dependency Management, Modulization 을 알아야 하고, 다음으로 주변의 Tooling System으로 SystemJS (Webpack), Gulp 등을 알아야 한다. 


이렇게 열거해 보니 참으로 배울 것이 많다. 다시 말하지만 안 배워도 된다. 하지만 자신의 근육을 한단계 업그레이드 시키기 위해 고통스러운 인내의 시간은 필요하다. 배워야 하는 기준은 두가지 정도로 이야기 해본다. 


첫째, 서비스 버전업을 위해 요구사항이 계속 증가하고 있는가?

둘째, 더 적고 직관적인 코드 베이스를 유지하면서 성능을 높이고 싶은가?


 

프론트엔드 개발자 직군이 새롭게 자리잡게된  5년기간 동안 많은 부분이 기존의 백앤드 개발 패턴과 유사해 지고 있다. 모듈 의존성 관리, 빌드 시스템, 프레임워크의 발전은 Java개발자들이 초장기 프레임워크 없이 개발하다 Struts를 만났을 때 기쁨에서 Spring을 만나 자유를 얻었지만 여전히 배워야 할 것들은 더욱 증가했음을 알것이다. 그러나 어쩌겠는가 우리는 더 게을러 지고싶다는 욕구가 있고 프레임워크가 그것을 만족시켜줄것이라는 희망을 품고 있는 한 배움과 진보는 계속될 뿐이다. 



참조


posted by 윤영식
2016. 4. 13. 13:31 Electron & Ionic

Ionic 프레임워크는 하이브리드 웹앱을 만들기 위한 프레임워크이다. 웹앱이기에 웹을 위한 프레임워크로 Angular를 사용하고 있다. Ionic v1에서는 Angular v1Ionic v2에서는 Angular v2를 사용하고 있다. 







Ionic은 하이브리드 웹앱 프레임워크외에 다양한 서비스를 제공하고 있다. Ionic 플랫폼 상에서 화면을 디자인하고 배포하고 테스트할 수 있는 서비스들을 제공한다. Ionic.io 는 Ionic 프레임워크를 이용해 만든 앱을 배포 관리하는 곳이다. Ionic Creator는 화면을 디자인하는 서비스이며, Ionic View는 앱을 앱스토어에 배포하거나 로컬에 USB로 설치하지 않고 Ionic View앱을 설치하면 Ionic 프레임워크로 만든 모든 앱들을 iOS 또는 Android 상에서 바로 볼 수 있는 서비스이다. 


위의 그림 Ionic는 Cordova를 기반으로 하고 CLI(Command Line Interface)를 제공하며 CLI는 두가지를 통합하고 있다. 첫째는 Cordova의 plugin에 대한 install/uninstall을 위한 명령이고 두번째는 Gulp를 이용해서 웹파일(Sass, Html, Scripts)을 위한 명령 Task에 대해 Gulp의 환경파일인 gulpfile.js에 정의하고 있다. Vinyl은 Gulp에서 사용되는 모듈로 다양한 OS에 상관없이 File Stream을 지원하기 위한것으로 개념은 링크를 참조한다. Ionic은 iOS와 Android Native UI에 가깝게 Angular 기반으로 컴포넌트를 제공하고 있다. 즉, Angular의 Directive(지시자, 컴포넌트)를 이용해 화면을 만드는 방식이다. 또한 Cordova의 기능을 Angular의 서비스로 사용하기 위해 ngCordova도 제공하고 있다. 


Ionic을 사용하기 위해서는 다음과 같은 사전지식이 필요하다. 

  - Node.js 그리고 NPM 사용법

  - 단순 테스트 목적이 아니라면 반드시 Angular 프레임워크 사용경험이 필요하다  

  - XCode 또는 Android Studio에서 간혹 Cordova의 Plugin을 수정할 때도 있다. 

  - 기본 XCode, Android Studio 사용법은 알아두는게 좋다. 직접 툴에서 빌드할 경우가 많다. 


현재 Ionic v2는 Angular v2를 기반으로 하고 아직 beta 버전이다 (2016.4.12).  2016년 상반기에 Angular v2 정식버전이 나오면 비슷한 시기에 정식버전이 나오리라 기대해 본다. Angular v2가 정식버전하에 하반기부터 본격적으로 쓰일 것으로 보이기때문에 Ionic도 v2를 사용하고 준비하면 좋을 것같다. Ionic v2를 사용하기 위해서는 따라서 다음과 같은 기초 지식도 필요하다. 

  - ES2015 JavaScript 스팩 상의 문법 추가 사항을 숙지해야 한다. OOP 스타일의 Syntax로 바뀌었다고 보면 된다.  

  - Angular v2 가이드문서 (반드시 TypeScript 기반으로 참조)를 최소 한번쯤은 보도록 한다. 

  - TypeScript 기반 Cordova 서비스를 통해 Native 접근이 필요할 것이다.  


알아야 할 것은 많지만 일단 설치부터 실행까지 보도록 한다. Ionic v2 CLI 명령어Cordova 공식 CLI 명령어도 한번 훑어보는게 좋다. Cordova는 현재 6.* 버전이 최신이다.





1. 설치하기 


먼저 Node.js를 설치한다. NPM(Node Package Manager)를 통해 Cordova와 ionic 프레임워크를 설치하고 Gulp 수행의 기반을 제공한다. 

  - typescript 컴파일러 설치

  - typings 는 typescript의 definition 파일을 관리하는 메니저이다. typings 폴더에서 관리된다. 

  - cordova는 iOS와 Android Native 접근을 위한 Gateway library라고 보면 된다. 

> sudo npm install -g   typings  typescript  cordova ionic@beta



다음으로 ionic에서 제공하는 샘플 파일을 자동 다운로드 받아 설치한다. ionic v2 에 typescript 버전으로 설치하는 명령어이다. 자동으로 typings폴더에 typescript definition 파일 및 package.json에 정의된 모듈도 node_modules 밑으로 자동 설치된다.

프로젝트생성) ionic start <projectName> <templateType> --v2 --ts

예) ionic start myFirst blank --v2 --ts


blank 템플릿 타입을 주면 https://github.com/driftyco/ionic2-starter-blank/archive/typescript.zip 에서 typescript 버전을 자동 다운로드해 설치한다.

templateType에는 tutorial, tabs, sidemenu, blank가 있다. 설치를 하면 다음과 같은 안내글이 나온다. 


Make sure to cd into your new app directory:

  cd ionic2-tutorial-github


To run your app in the browser (great for initial development):

  ionic serve


To run on iOS:

  ionic run ios


To run on Android:

  ionic run android


To test your app on a device easily, try Ionic View:

  http://view.ionic.io


New! Add push notifications, update your app remotely, and package iOS and Android apps with the Ionic Platform!

  https://apps.ionic.io/signup


New to Ionic? Get started here: http://ionicframework.com/docs/v2/getting-started


설명대로 ionic2-tutorial-github 폴더로 이동후 ioinic serve 명령을 수행하면 Gulp의 serve 태스커가 수행된다. 수행 결과로 ionic $ 명령콘솔이 활성화 된다.

> ionic serve 

WARN: ionic.config.js has been deprecated, you can remove it.

Running live reload server: http://localhost:35729

Watching: www/**/*, !www/lib/**/*

√ Running dev server:  http://localhost:8100

Ionic server commands, enter:

  restart or r to restart the client app from the root

  goto or g and a url to have the app navigate to the given url

  consolelogs or c to enable/disable console log output

  serverlogs or s to enable/disable server log output

  quit or q to shutdown the server and exit


ionic $





2. ionic 폴더 구조


ionic의 폴더구조 및 환경설정 파일은 다음과 같다. 


 app

 개발자가 작성하는 모든 애플리케이션 코드가 위치한다. 

 hooks (cordova)

 Cordova 빌드과정의 일부로서 동작될 수 있는 스크립트를 포함하고 있다. 앱을 패키지 할때 필요하다면 언제든 커스터마이징 할 수 있다.  

 node_modules

 npm을 통해 설치된 모듈들이 있다.

 platforms (cordova)

 ionic platform 으로 ios, android를 설치하면 하위 폴더로 생기고, XCode 또는 Android Studio에서 import할 수 있다.

 plugins (cordova)

 ionic platform 선택시 Cordova의 플러그인이 설치되는 폴더이다. 

 resources 

 앱을 위한 icon과 splash image를 해상도가 틀린 모바일 기기별로 놓는 곳이다.  

 typings

 Typescript로 쓰여지지 않는 JS 라이브러리의 타입정의를 한 type definition 파일이 있다.  

 www (cordova)

 index.html를 포함한다. 이곳은 빌드될 때 사용되는 것으로 애플리케이션 코드가 위치하는 곳이 아님을 주의하자.  "ionic build" 를 하면 "cordova build"가 수행되어 www 해당 디렉토리에 app의 코드가 위치하고 다시 platforms/ios 또는 android의 www 폴더에 copy된다. 따라서 최종 사용되는 파일은 platforms/ios (또는 android) /www/* 에 위치한다. 

 config.xml (cordova)

 앱 패키지를 만들때 사용하기 위해 Cordova의 환경설정이 존재한다. 

 ionic.config.js

 not used로 앞으로 없어질 것이다. ionic.config.json 파일은 버전 정보만 전달

 package.json

 npm 으로 설치되는 모든 모듈에 대한 설정 

tsconfig.json / typings.json

 TypeScript 환경 설정 / type definition file 환경 설정


크게 "환경파일", "Cordova", "애플리케이션" 부분으로 나뉠 수 있다. 최초 템플릿이 생성된 이후 개발자는 "애플리케이션"폴더인 "app"를 사용하면 된다. blank타입으로 만들었을 때 platforms 폴더에는 ios 플랫폼이 기본 설치된다. 







3. TypeScript 기반 개발


최근(2016.4.8) Beta버전에 CLI 를 통해 TypeScript 기반의 Angular v2 페이지와 서비스를 만들 수 있는 명령어를 공개했다. 파일이름은 kebob-case로 my-page와 같이 되고, ES6/TypeScript기반의 class는 PascalCase로 MyData로 처럼 이름을 준다. 

> ionic generate ( 또는 축약해서 g ) <page 또는 provider> <Name>


예)

> ionic g page myPage

create app/pages/my-page/my-page.html (.js, .css)


> ionic g provider MyData

create app/providers/my-data/my-data.js 


생성된 my-page.ts 소스는 다음과 같다. @Page는 TypeScript에서 제공하는 Decorator를 이용해 Class Decorator를 Ionic을 위해 만든 것이다. 

  - TypeScript의 Decorator에 대해 자세히 알고 싶다면 링크를 참조한다. 

import {Page, NavController} from 'ionic-angular';


/*

  Generated class for the MyPagePage page.


  See http://ionicframework.com/docs/v2/components/#navigation for more info on

  Ionic pages and navigation.

*/

@Page({

  templateUrl: 'build/pages/my-page/my-page.html',

})

export class MyPagePage {

  constructor(public nav: NavController) {

    this.nav = nav;

  }

}


.ts가 .js코드로 컴파일된 내역을 보고 싶다면 app 폴더로 이동해서 TypeScript 컴파일 명령어인 "tsc"를 수행한다. @Page는 __decorate({ ... }) 안에서 "ionic-angular"의 decorators/page.js 의 Class Decorator가 수행되는 것이다. ionic_angular_1.Page( <config object> ); 결국, @Page는 구현한 decorator 펑션을 호출해 주는 역할을 수행할 뿐이다. TypeScript Decorator 공식문서를 통해 개념을 이해하자. @Page는 Angular v2에서 제공하는 데코레이터가 아니라 Ionic에서 제공하는 것이다. 내부 소스를 보면 @Page 클래스는 ion-page selector를 사용하고 결국 Angular v2 core에 있는 @Component를 호출하고 있을 뿐이다. 즉, 굳이 @Page를 쓰지 않아되 되는 Decorating이다. 

// app/pages/my-page/my-page.js TypeScript 컴파일된 소스


"use strict";

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {

    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;

    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);

    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;

    return c > 3 && r && Object.defineProperty(target, key, r), r;

};

var __metadata = (this && this.__metadata) || function (k, v) {

    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);

};

var ionic_angular_1 = require('ionic-angular');

/*

  Generated class for the MyPagePage page.


  See http://ionicframework.com/docs/v2/components/#navigation for more info on

  Ionic pages and navigation.

*/

// 자동으로 postfix로 Page가 붙는다. 

var MyPagePage = (function () {

    function MyPagePage(nav) {

        this.nav = nav;

    }

    MyPagePage = __decorate([

        ionic_angular_1.Page({

            templateUrl: 'build/pages/my-page/my-page.html',

        }), 

        __metadata('design:paramtypes', [ionic_angular_1.NavController])

    ], MyPagePage);

    return MyPagePage;

}());

exports.MyPagePage = MyPagePage;



// node_modules/ionic-angular/decorators/page.js 데코레이터 구현 소스


function Page(config) {

    return function (cls) {

        // @Page 의 기본 selector

        config.selector = 'ion-page';  

        config.directives = config.directives ? config.directives.concat(directives_1.IONIC_DIRECTIVES) : directives_1.IONIC_DIRECTIVES;

        config.host = config.host || {};

        config.host['[hidden]'] = '_hidden';

        config.host['[class.tab-subpage]'] = '_tabSubPage';

        var annotations = _reflect.getMetadata('annotations', cls) || [];

        // 기본 설정후 Angular v2 core의 @Component를 호출한다. 

        annotations.push(new core_1.Component(config)); 

        _reflect.defineMetadata('annotations', annotations, cls);

        return cls;

    };

}

exports.Page = Page;


provider로 만든 것은 ES2015의 Promise, RxJS, @Injectable이 포함되어 있다. Angular v2에서는 모든 Inject되는 Service Class에는 Class Decorator로 @Injectable를 설정토록 권고한다. 

// app/providers/my-data/my-data.ts 소스 


import {Injectable} from 'angular2/core';

import {Http} from 'angular2/http';

import 'rxjs/add/operator/map';


/*

  Generated class for the MyData provider.


  See https://angular.io/docs/ts/latest/guide/dependency-injection.html

  for more info on providers and Angular 2 DI.

*/

@Injectable()

export class MyData {

  data: any = null;


  constructor(public http: Http) {}


  load() {

    if (this.data) {

      // already loaded data

      return Promise.resolve(this.data);

    }


    // don't have the data yet

    return new Promise(resolve => {

      // We're using Angular Http provider to request the data,

      // then on the response it'll map the JSON data to a parsed JS object.

      // Next we process the data and resolve the promise with the new data.

      this.http.get('path/to/data.json')

        .map(res => res.json())

        .subscribe(data => {

          // we've got back the raw data, now generate the core schedule data

          // and save the data for later reference

          this.data = data;

          resolve(this.data);

        });

    });

  }

}





5. ionic 페이지 만들기 


일단, ionic generate를 통해 생성된 page, provider (서비스)는 참조만 한다. 다음과 같이 서비스를 파일을 만든다. Angular2 http 모듈이 RXJS를 사용하고 있고 return 오브젝트가 Promise가 아니라 Observable이다. RXJS는 Reactive Programming의 자바스크립트 구현체로 링크를 참조한다.

import { Injectable } from 'angular2/core';

import { Http, Headers } from 'angular2/http';


@Injectable()

export class GitHubService {

    constructor(private http: Http) {}


    getRepos(username) {

        let repos = this.http.get(`https://api.github.com/users/${username}/repos`);

        return repos;

    }

}


다음으로 /app/pages/home/home.ts를 수정한다. @Component 대신에 @Page를 사용하고 providers로 GitHubService를 정의한다. 

import {Page} from 'ionic-angular';

import {GitHubService} from '../../providers/github.service';


@Page({

  templateUrl: 'build/pages/home/home.html',

  providers: [GitHubService]

})

export class HomePage {

  public foundRepositories;

  public username;

  constructor(private github: GitHubService) {}

  getRepos() {

    this.github.getRepos(this.username)

      .subscribe(

          data => {

              this.foundRepositories = data.json();

          },

          err => console.error(err),

          () => console.log('getRepos completed')

      );

  }

}


home.html도 다음과 같이 재정의한다. 

<ion-navbar *navbar>

<ion-title>

Home

</ion-title>

</ion-navbar>


<ion-content class="home">

<ion-list inset>

<ion-item>

<ion-label>Username</ion-label>

<ion-input [(ngModel)]="username" type="text"></ion-input>

</ion-item>

</ion-list>

<div padding>

<button block (click)="getRepos()">Search</button>

</div>

<ion-card *ngFor="#repo of foundRepositories">

<ion-card-header>

{{ repo.name }}

</ion-card-header>

<ion-card-content>

{{ repo.git_url }}

</ion-card-content>

</ion-card>

</ion-content>


ionic CLI 명령을 통해 테스트를 해보자. serve 명령을 이용하면 로컬 서버를 띄워서 브라우져상에서 테스트를 해볼 수 있다. 옵션으로 --lab을 주면 ios, android 둘 다 테스트가 가능하다. 

> ionic serve --lab 



ionic의 ion-* 시작하는 Angular component를 사용하면 mobile 플랫폼에 따라 Look and feel이 자동으로 맞춰진다. 이를 원하지 않을 때는 최소한의 ion-* 태그만을 사용하면 된다. ion-navbar 와 ion-content 태그만을 사용하고 나머지는 서비스에 맞게 HTML을 작성해도 무방하다.





6. ionic Navigation 추가하기 


네비게이션을 위해 메인 컴포넌트인 app.ts를 살펴보면, ionic에서 제공하는 @App 데코레이터를 통해서 HomePage 를 최상위 페이지로 설정하고 있다.

import 'es6-shim';

import {App, Platform} from 'ionic-angular';

import {StatusBar} from 'ionic-native';

import {HomePage} from './pages/home/home';


@App({

  template: '<ion-nav [root]="rootPage"></ion-nav>',

  config: {} // http://ionicframework.com/docs/v2/api/config/Config/

})

export class MyApp {

  rootPage: any = HomePage;


  constructor(platform: Platform) {

    platform.ready().then(() => {

      // Cordova 준비

      StatusBar.styleDefault();

    });

  }

}


GitHub 저장소 목록에서 상세페이지 이동을 위한 detail 페이지를 만들고, 일단 home.html 과 .ts 파일을 수정한다.

// home.html 에서 click 이벤트를 추가한다. 

<ion-card *ngFor="#repo of foundRepositories" (click)="goDetail(repo)">


// home.ts 에 Navigation을 위한 controller와 추가 메소드를 정의한다. 

import {Page, NavController} from 'ionic-angular';

import {GitHubService} from '../../providers/github.service';

import {DetailPage} from '../detail/detail'


@Page({

  templateUrl: 'build/pages/home/home.html',

  providers: [GitHubService]

})

export class HomePage {

  public foundRepositories;

  public username;

  constructor(private github: GitHubService, private nav: NavController) {}

  getRepos() {

    this.github.getRepos(this.username)

      .subscribe(

          data => {

              this.foundRepositories = data.json();

          },

          err => console.error(err),

          () => console.log('getRepos completed')

      );

  }

  goDetail(repo) {

      // 페이지를 Stack에 추가하는 것이다. 

      this.nav.push(DetailPage, { repo: repo });

  }

}


home.html 상단의 ion-navbar 태그는 pagenavigation하면 <Back 버튼이 자동으로 생긴다. 즉, <Back 버튼을 클릭하는 순간 이동한 페이지를 pop 하는 것이 자동으로 이루어 지기때문에 명시적으로 해줄 필요가 없다. 다음으로 저장소의 README 파일 내역을 HTML 포멧으로 가져오는 메소드를 GitHubService에 추가한다. 

// github.service.ts


getDetail(repo) {

        let headers = new Headers();

        headers.append('Accept', 'application/vnd.github.VERSION.html');

        return this.http.get(`${repo.url}/readme`, {headers: headers});

}


상세내역을 detail.ts를 통해 상세정보를 가져오고 다시 detail.html에 뿌려준다. NavParams를 통해 전달받은 repo 객체의 url로 README 값을 받아온다. detail.html에서는 아래 소스처럼 {{ readme }} 표현식을 사용하면 HTML 태그가 text로 뿌려질 뿐이다. 따라서 innerHTML 속성을 이용한다. 

// detail.ts 


import {Page, NavController, NavParams} from 'ionic-angular';

import {GitHubService} from '../../providers/github.service';


@Page({

  templateUrl: 'build/pages/detail/detail.html',

  providers: [GitHubService]

})

export class DetailPage {

  public readme = '';

  public repo;

  constructor(private nav: NavController, private github: GitHubService, private navParams: NavParams) {

      this.repo = navParams.get('repo');

      this.github.getDetail(this.repo)

        .subscribe(

            data => this.readme = data.text(),

            err => {

                if(err.status == 404) {

                    this.readme = 'This repo does not have a README file';

                } else {

                    console.error(err);

                }

            },

            () => console.log('getDetail completed')

        );

  }

}



// detail.html 

<ion-navbar *navbar>

  <ion-title>{{ repo.name }}</ion-title>

</ion-navbar>


<ion-content padding class="detail">

  <div padding>{{ readme }}</div>

</ion-content>



detail.html을 text가 아니 DOM으로 넣기 위해 innerHTML속성으로 변경하고 최종 테스트 한다. 파일을 변경하면 ionic은 파일을 watch하고 있다가 자동으로 refresh 해준다. (ionic serve 경우)

<ion-content padding class="detail">

  <div padding [innerHTML]="readme"></div>

  <!-- <div padding>{{ readme }}</div> -->

</ion-content>


ios와 Android 플랫폼에 맞는 "< Back" 버튼이 생성된다. github 사용자 이름을 입력하고 목록이 나오면 목록중의 카드하나를 클릭하면 다음과 같이 Navigation되는 것을 볼 수 있다. ionic은 Tabs 형태 또는 Menus형태의 UI 컨테이너를 통해 Navigation을 Mobile UX에 맞게 변경할 수 있다. 





7. ionic 배포와 테스트 


ionic serve 명령으로 로컬 웹서버를 띄워 로컬 웹 브라우져를 통해 기능을 테스트 했다면 emulate 또는 기기에 App을 배포해서 테스트 한다. 브라우져에서 보는 것과 실 기기에서 테스트하는 것은 하늘과 땅 차이다. 희소식은 XCode 7 부터는 Developer Account(유료)없이도 USB를 통해 iPhone에 App을 배포 테스트할 수 있다. 

// 로컬에서 xcode emulate을 띄워준다. 

> ionic emulate ios


// 연결된 기기에서 수행할 수 있다

> ionic run ios 


MacBook 상에서 ionic run을 위해서는 "npm install -g ios-deploy" 사전에 설치가 필요하다. 



또는 ionic viewer를 모바일 기기에 설치하고 프로젝트를 ionic.io 서비스에 자신의 계정에 upload한 후 작동 여부를 테스트 할 수 있다. 하지만 작동중 디버깅에 대한 부분은 보다 많은 지면을 필요로 하기에 다음에 언급한다. upload시에 로그인 안되어 있으면 등록한 id/pwd를 물어본다. 

> ionic upload 

WARN: No 'upload:before' gulp task found!

If your app requires a build step, you may want to ensure it runs before upload.


Uploading app....

Saved app_id, writing to ionic.io.bundle.min.js...

Successfully uploaded (f3ded32c)


Share your beautiful app with someone:


$ ionic share EMAIL

Saved api_key, writing to ionic.io.bundle.min.js...


최근에는 Xamarin이나 React Native, NativeScript 같은 Native Code로 전환해 주는 프레임워크가 인기를 끌고 있다. 모바일만을 고려하지 않는다면 ionic은 웹표준 기술을 통해 빠른 기능개발과 Angular2기반의 프론트앤드의 서비스와 컴포넌트(만일 Web을 Angular2로 개발한다면)를 공유할 수 있다. 즉, Native Mobile UX/UI 특성과 빌드 환경의 편의성은 Ionic 프레임워크를 사용하면서 웹개발도 도모할 수 있는 Hybrid WebApp + Web 개발이 가능하다 판단된다. 


향후 angular2-seedNativescriptElectron을 접목한 angular2-seed-advanced가 나왔듯이, angular2-seed에 Ionic2를 접목한 seed를 만들 예정이다. 





참조


  - ionic v2 공식 문서

  - Vinyl 개념 이해하기 & Stream Handbook

  - Cordova 공식 홈페이지 문서

  - TypeScript Decorator 만들기

  - 블로깅 참조 

  - 다양한 모바일 기기 테스트 방법들

  - Ionic2 Conference App 소스

  - Ionic.io 의 다양한 서비스들: Push, Deploy, Analytics, User

  - Crosswalk



자료


  - Ionic Advantures

  - Ionic Collection

  - Ionic Resources

  - AppCamp

  - play.ionic.io: playground


     


posted by 윤영식
2015. 11. 14. 17:54 Angular/Concept

Angular2 구현 언어는 TypeScript이다. TypeScript를 배우기 위한 준비과정을 알아본다. 








Transpiler


  ES2015(EcmaScript 6) 스펙은 모던 브라우저에서 완벽히 구현되어 있지 않기 때문에 최소한 ES5기준에 맞춰서 사용을 해야 한다. TypeScript는 ES2015를 포함해 정적 타입 시스템과 ES7에서 나오는 데코레이터 @을 지원하는 슈퍼셋이다. 따라서 Babel로 ES2015를 ES5로 트랜스파일(Transpile) 하지 않고 TypeScript만의 트랜스파일러를 사용해야 한다.  


$ sudo npm install -g typescript 


TypeScript 파일의 확장자는 .ts파일로 한다. 그리고 자바스크립 파일로 트랜스파일하기 위해 tsc 명령을  사용한다. 결과물로 동일 명칭의 .js 파일이 생성된다.


$ tsc  <fileName>.ts 






Editor 


  Microsoft에서 mac에서도 사용할 수 있는 전용 Code Editor(Visual Studio Code)를 내놓았다. 설치 후 어느 위치에서든 code . 하면 현재 폴더의 파일 구조를 가지고 에디터가 열린다. 그리고 .ts 파일에 대한 스마트 위저드 헬퍼가 자동으로 뜨기 때문에 MS Visual Studio에서 코딩하는 착각을 불러 일으킨다. 필자는 최근 Sublime 3에서 Atom으로 코딩 툴을 옮겼는데 참 흥미롭게 사용중이다. 큰 파일의 경우 약간의 성능저하를 견딜 수 있다면 사용해 보길 권한다. 




Atom을 사용한다면 atom-typescript 패키지 설치를 통해 Visual Studio Code에서의 .ts 에 대한 스마트한 기능(Auto Complete, Type Info on hover...)을 그대로 사용할 수 있다. 설치후 F6를 클릭하면 tsc <fileName>.ts 명령을 수행하고 결과를 Notify UI 창으로 성공 실패여부를 알려준다. 


 





tsconfig.json


  tsconfig.json 파일은 TypeScript를 위한 프로젝트 단위 환경 파일이다. Exclude 하고 싶은 폴더나 파일, Compile 옵션, File 옵션등을 설정 할 수 있다. TypeScript의 컴파일러인 tsc 명령 수행시 참조한다. tsconfig.json의 전체 스키마를 참조한다.  또는 TypeScript Deep Dive 깃북의 설명을 참조한다.


{

    "exclude": [

        "node_modules",

        "bower_components"

    ],


    "compilerOptions": {

        "target": "es5",

        "sourceMap": true,

        "module": "commonjs",

        "declaration": false,

        "noImplicitAny": false,

        "removeComments": true,

        "noLib": false

    }

}






TypeScript 배우기 


TypeScript를 배우기 위해 ES2015의 문법을 먼저 익히는게 좋다. 

  - ES2015-features 사이트에서 새로운 기능들이 ES5 스펙과 어떤 차이가 있는지 눈여겨 보자. 

  - 동영상 강좌를 본다. 필자는 egghead.io의 동영상을 먼저 참조한다. (Pro는 유료이다)


공식홈페이지는 http://www.typescriptlang.org/ 이고, TypeScript 구현체 자체도 오픈소스로 깃헙에 공개되어 있다. 

  - 공식홈페이지에서 제공하는 TypeScript 스펙 배우기

  - TypeScript Deep Dive 깃북






TSD


TypeScript Definition Manager의 약어이다. 이미 나와있는 프레임워크(angular), 라이브러리(jquery) 같은 것을 사용하면서 애플리케이션을 TypeScript로 작성할 때 이들 구현체의 정의 내역을 미리 알고 코드에서 가이드를 주기위한 파일이다. 확장자는 angular.d.ts 또는 jquery.d.ts처럼 <name>.d.ts 가 붙는다. 예로 $('.awesome').show(); 라고 하면 타입스크립트는 $를 알지 못 한다. 먼저 declare var $:any; 라고 해주어야 사용할 수 있다.  


$ npm install -g tsd


tsd를 설치하고 tsd install <name> 으로 설치하면 수행위치에 typings 폴더 밑으로 파일을 다운로드한다. 


$ tsd init

// tsd.json 파일 

{

  "version": "v4",

  "repo": "borisyankov/DefinitelyTyped",

  "ref": "master",

  "path": "typings",

  "bundle": "typings/tsd.d.ts",

  "installed": {

    "jquery/jquery.d.ts": {

      "commit": "efd40e67ff323f7147651bdbef03c03ead7b1675"

    },

    "angularjs/angular.d.ts": {

      "commit": "efd40e67ff323f7147651bdbef03c03ead7b1675"

    }

  }

}


$ tsd install jquery

$ tsd install angular



이미 만들어져 있는 d.ts 파일을 이곳에서 http://definitelytyped.org/tsd/ 찾아 볼 수 있다. DefinitelyTyped 공식 홈페이지를 참조하자.






WorkFlow 


 Atom 에디터와 기본 툴을 설치했으니, 프로젝트를 위한 워크플로우를 만들어 보자.   


  - 프로젝트 폴더 만들기 

  - NodeJSGit 설치 

  - "npm install gulp -g" 설치 

  - npm 초기화 및 develop dependencies 설정 

$ npm init 


// package.json 첨부

"devDependencies": {

        "gulp": "^3.8.11",

        "gulp-debug": "^2.0.1",

        "gulp-inject": "^1.2.0",

        "gulp-sourcemaps": "^1.5.1",

        "gulp-tslint": "^1.4.4",

        "gulp-typescript": "^2.5.0",

        "gulp-rimraf": "^0.1.1",

        "del": *,

        "superstatic": *,

        "browser-sync": *

    }


// 의존 라이브러리 설치

$ npm install 


  - tsd 설치하고, init 초기화 명령으로 tsd.json 파일과 typings 폴더에 tsd.d.ts 파일을 자동 생성한다.

$ npm install -g tsd 


$ tsd init 



  - angular 의 TypeScript Definition 파일을 설치해 보자. 더 많은 라이브러리는 이곳에서 http://definitelytyped.org/tsd/ 에서 찾을 수 있다.

$ tsd install angular --save

 - angularjs / angular

   -> jquery > jquery


>> running install..

>> written 2 files:

    - angularjs/angular.d.ts

    - jquery/jquery.d.ts



$ tsd install jquery --save

 - jquery / jquery


>> running install..

>> written 1 file:

    - jquery/jquery.d.ts



  - gulpfile.config.js와 gulpfile.js를 https://github.com/DanWahlin/AngularIn20TypeScript 에서 복사해서 만든다. Gulp Task 설명


  - ts 파일 lint를 위해 tslint.json파일 만든다.

{

    "rules": {

        "class-name": true,

        "curly": true,

        "eofline": false,

        "forin": true,

        "indent": [true, 4],

        "label-position": true,

        "label-undefined": true,

        "max-line-length": [true, 140],

        "no-arg": true,

        "no-bitwise": true,

        "no-console": [true,

            "debug",

            "info",

            "time",

            "timeEnd",

            "trace"

        ],

        "no-construct": true,

        "no-debugger": true,

        "no-duplicate-key": true,

        "no-duplicate-variable": true,

        "no-empty": true,

        "no-eval": true,

        "no-imports": true,

        "no-string-literal": false,

        "no-trailing-comma": true,

        "no-trailing-whitespace": true,

        "no-unused-variable": false,

        "no-unreachable": true,

        "no-use-before-declare": true,

        "one-line": [true,

            "check-open-brace",

            "check-catch",

            "check-else",

            "check-whitespace"

        ],

        "quotemark": [true, "single"],

        "radix": true,

        "semicolon": true,

        "triple-equals": [true, "allow-null-check"],

        "variable-name": false,

        "whitespace": [true,

            "check-branch",

            "check-decl",

            "check-operator",

            "check-separator"

        ]

    }

}


  - Atom에서 atom-typescript를 설치해서 사용한다면 tsconfig.json도 만든다. 아래와 같이 설정값을 입력한다. 설정 값이 있어야 atom-typescript 기능을 사용할 수 있다. 보다 자세한 tsconfig.json 작성 문법을 참조한다.

{

    "compilerOptions": {

        "target": "es5",

        "module": "commonjs",

        "declaration": false,

        "noImplicitAny": false,

        "removeComments": true,

        "noLib": false

    },

    "filesGlob": [

        "./**/*.ts",

        "!./node_modules/**/*.ts"

    ],

    "files": [

        "./globals.ts",

        "./linter.ts",

        "./main/atom/atomUtils.ts",

        "./main/atom/autoCompleteProvider.ts",

        "./worker/messages.ts",

        "./worker/parent.ts"

    ]

}


  - gulp 명령을 수행하면 .js 파일과 .js.map 파일이 생성된다. 


  - TypeScript 소개 영상과 소스를 확인해 보자.






Start Kit 


  yeoman의 generator 처럼 TypeScript프로젝트 관련한 스타트 깃을 사용해 보자. 물론 angular2 기반으로 프로젝트를 가정한다. 

  - AngularClass에서 제공하는 angular2-webpack-starter 

  

   


  - AngularClass에서 제공하는 Angular2의 Server Rendering을 접목한 Universal Starter





참조 


  - TypeScript Workflow with Gulp

  - TypeScript in Awesome Angular2

 - Atom TypeScript 패키지 : atom-typescript



posted by 윤영식
2015. 6. 22. 23:20 Angular/Concept

  ECMAScript는 자바스크립트의 토대를 구성하는 스크립트 언어 명세이다. ECMA는 "Europen Computer Manufacturers Association"의 약어로 국제 표준화 기구이며, ECMAScript는 ECMA-262, ECMA-402 스펙안에서 표준화되었다. ECMAScript 3.0이 1999년 발표된 이래로 ECMAScript 5.0 이 2009년 발표되고 5.1이 2011년 그리고 마침내 ECMAScript 6.0이 2015년 6월 공표되었다. 모던 브라우저가 대부분 ECMAScript 5.0  을 지원하 있고, 현재 브라우저가 5,6,7 버전들에 대해 어느 범위까지 지원하는지 테스트 해볼 수 있다. 


  Angular 2는 TypeScript를 기반으로 개발되고 있고, TypeScript는 ES6를 포함해 별도의 추가 기능을 포함한다. ES6 기반으로 애플리케이션을 개발할 경우 BabelJS를 통해 ES5로 컴파일할 수 있다. 따라서 ES6에 대해 알아보고 필요한 문법을 사용한 후 애플리케이션 배포시에 ES5로 적용로 한다. Babel 사용을 자동화 하기위해 Browserify또는 Webpack을 사용하거나 Grunt/Gulp를 사용할 수도 있다.  




기초 문법 익히기


  - 문법을 보기전에 드미트리 소스니코드의 설명을 잠시 훑어보자

    


  - ECMAScript 6에 새롭게 추가된 기능 알아보기

  - ECMAScript 6를 5와 비교해서 실습하기 

  - Exploring JS ES6 무료 서적





ECMAScript 5 전환 자동화하기


  - Babel을 Browserify와 연동하기, Babel을 Browserify+Gulp 연동하기

  - Babel을 Webpack과 연동하기

  - Babel을 Gulp와 연동하기

  - Babel을 Grunt와 연동하기




참조 

  - ECMAScript 6.0 스펙  (PDF)

  - ECMAScript 버전별 자료

posted by 윤영식
prev 1 next