Semantic UI를 React 컴포넌트로 만들고 NPM 저장소와 Meteor 저장소인 Atmosphere에 배포해 보자. 토요일 헤커톤 모임을 갖고 스마트링크 멤버분들과 함께 진행했다. 목표는 React Bootstrap 과 같은 오픈소스 패키지를 만들어 보는 것이다. 처음엔 활짝 웃고 있는 모습!
준비 운동
함께 개발을 진행하는 관계로 코딩 컨벤션은 에어비엔비의 React 코드 컨벤션으로 한다. 다음으로 깃헙(GitHub) 저장소를 만들었다. 소셜 코딩은 역시 깃헙이다. 깃을 사용하므로 커밋 메세지를 잘 작성하면 별도의 CHANGELOG를 작성할 필요가 없다. 따라서 Git Commit에 대한 컨벤션도 앵귤러에서 사용하는 방식을 쓴다. 그리고 정보를 찾고 공유하는 것은 슬랙(Slack)을 이용하고, 할일에 대한 보드는 트렐로(Trello)를 이용해 서로 공유하고 있다. 트렐로를 슬랙과 연동하면 슬랙에서 모든 것을 확인 할 수 있다.
일단 npm 초기화을 초기화하고 모듈을 설치한다. package.json 에 설치하는 모듈은 다음과 같다.
- ES6를 사용하기에 Babel을 통해 ES5로 트랜스파일한다.
- React로 설치하고, classnames는 리액트 코드안에서 클래스명을 확장해 주는 기능을 한다.
- fbjs는 ES7의 dectorator를 사용해 React Start Kit에 있는 withStyle을 컴포넌트별로 적용해 보려 했으나 원하는 동작이 안된다. 제거해도 좋음.
- *-loader는 웹팩(Webpack)을 통해 전체 패키지를 하나의 배포 파일로 번들링하기 위해 설치한다.
package.json에 설정을 넣고 한번에 npm install 해서 설치하자
// package.json 일부 내역
"dependencies": {
"babel": "^5.8.21",
"react": "^0.13.3",
"classnames": "^2.1.3",
"fbjs": "0.1.0-alpha.7"
},
"devDependencies": {
"babel-core": "^5.8.22",
"babel-loader": "^5.3.2",
"css-loader": "^0.15.6",
"fbjs": "0.1.0-alpha.7",
"semantic-ui-css": "^2.0.8",
"style-loader": "^0.12.3",
"url-loader": "^0.5.6",
"webpack": "^1.11.0"
}
다음으로 웹팩 환경파일인 webpack.config.js 를 만든다. 웹팩은 사전에 설치할 것이 있는데 홈페이지의 Getting start를 보자. 그리고 어떻게 잘 사용할지는 페북 엔지니어인 피트헌트의 Webpack Howto를 참조한다. 설정 내용은 다음과 같다.
- 배포 파일 명칭은 reactjs-semantic-ui.js 파일이고, /src/index.js 파일에 모든 참조 내역을 작성한다.
- 배포시 접근하는 네임스페이스는 RSU (React Semanctic Ui) 이다. 예로 본 패키지를 리액트 코드에서 사용하면 RSU.Button 식의 접근이다.
- .js 또는 .jsx 확장자는 babel-loader로 ES6를 ES5로 트랜스파일한다.
- 웹팩은 css 파일과 이미지 파일도 함께 번들링 하므로 style-loader 와 css-loader를 설정하고, 이미지와 폰트 관련해서 url-loader를 설정한다.
- react.js 관련 파일은 웹팩으로 번들링시에 제외토록 externals를 설정한다.
// webapck.config.js 내역
module.exports = {
entry: {
'reactjs-semantic-ui': './src/index.js'
},
output: {
path: './dist',
filename: '[name].js',
library: 'RSU',
libraryTarget: 'umd'
},
module: {
loaders: [
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,
loader: 'babel-loader'
},
{
test: /\.css$/,
exclude: /\.useable\.css$/,
loader: 'style-loader!css-loader'
},
{
test: /\.(png|jpg|svg|eot|woff|woff2|ttf)$/,
loader: 'url-loader?limit=8192'
} // inline base64 URLs for <=8k images, direct URLs for the rest
]
},
externals: [
{
'react': {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react'
}
}
]
}
Babel 사용시 주의 점은 간혹 ES7 의 decorator를 사용하거나 특정 스펙 레벨까지 적용하고 싶다면 루트에 .babelrc를 생성하고 stage를 설정해야 한다. stage 설정 가이드를 참조하자.
{
"stage" : 0
}
개발 시작
환경 준비하며 문제점을 해결하는데 3시간 가까이 소비를 했다. 본격적으로 시멘틱 UI를 리액트화 하기 위해 다음과 같은 순서로 작업을 한다.
- src/index.js안에 작성한 컴포넌트들을 설정한다.
- 시멘틱 UI가 구분해 놓은 Elements, Collections, Views ... 와 같이 구분해서 폴더를 만들어 그밑으로 컴포넌트를 만든다.
- 시멘틱 UI의 core css는 reset.css 로 Reset 클래스를 하나 만들었다.
- ES6 구문을 사용한다. ES5 Features를 참조하자
+ import ... from
+ { key : value } 가 동일 설정이면 { key1, key2 }로 나열
+ export default RSU
// Globals
import Reset from './globals/Reset';
// Elements
import { Button, Buttons } from './elements/Button';
import Container from './elements/Container';
import Dimmer from './elements/Dimmer';
import Divider from './elements/Divider';
import Flag from './elements/Flag';
import Header from './elements/Header';
import Icon from './elements/Icon';
import Image from './elements/Image';
import Input from './elements/Input';
import Label from './elements/Label';
import { List, Item } from './elements/List';
import Loader from './elements/Loader';
import Rail from './elements/Rail';
import Reveal from './elements/Reveal';
import Segment from './elements/Segment';
import { Step, Steps } from './elements/Step';
// Collections
import Breadcrumb from './collections/Breadcrumb/Breadcrumb';
import BreadcrumbDivider from './collections/Breadcrumb/BreadcrumbDivider';
import BreadcrumbSection from './collections/Breadcrumb/BreadcrumbSection';
import Form from './collections/Form/Form';
import Grid from './collections/Grid/Grid';
import Column from './collections/Grid/Column';
import Row from './collections/Grid/Row';
import Menu from './collections/Menu/Menu';
import Message from './collections/Message/Message';
import Table from './collections/Table/Table';
// Views
import Card from './views/card/Card';
import CardImage from './views/card/CardImage';
import CardContent from './views/card/CardContent';
import CardHeader from './views/card/CardHeader';
import CardMeta from './views/card/CardMeta';
import CardDescription from './views/card/CardDescription';
const RSU = {
// Elements
Button, Buttons,
Container,
Dimmer,
Divider,
Flag,
Header,
Icon,
Image,
Input,
Label,
List, Item,
Loader,
Rail,
Reveal,
Segment,
Step, Steps,
// Views
Card,
CardImage,
CardContent,
CardHeader,
CardMeta,
CardDescription,
// Collections
Breadcrumb,
BreadcrumbDivider,
BreadcrumbSection,
Form,
Grid,
Row,
Column,
Menu,
Message,
Table
}
export default RSU;
리액트 컴포넌트는 다음과 같이 만든다. 1주차엔 시멘트 UI의 css를 적용해서 잘 나오는지만 확인하는 것으로 리액트 컴포넌트의 라이프사이클 메소드를 전부 설정해서 넣는다.
- 에어빈앤비의 리액트 코딩 컨벤션에 따라 작성한다.
- shouldComponentUpdate 구문은 향후 Immutable.js 사용과 PureRenderMixin의 조합을 고려해 성능을 향상시켜야 한다.
이에 대한 좋은 글이 있으니 꼭 읽어보자. Immutable.js에 대한 연재글 [1], [2], [3]
- 리액트의 라이프 사이클 코드가 대부분의 컴포넌트에 중복해서 들어 있으므로 이것도 ES6 구문을 위한 리액트의 Mixin 패키지를 사용해 리팩토링해야 한다.
- classNames( x, y, this.props.className )은 classnames 패키지를 사용해서 <Card className="ui card <상속 className>"> 을 설정해 준다.
- <div {...this.props} 를 하면 상위에 설정한다. 모든 key="value" 애트리리뷰트를 그대로 설정해준다. 단, 주의 점은 <div {...this.props} className...> 처럼 사용하면 className은 오버라이딩 된다는 점이다. 만일 <div className={componentClass} {...this.props}> 로 하고 <Card className="dowon" /> 를 사용하면 className={componentClass} 는 적용이 되지 않는다. 즉 태그안에서 중복된 키이면 뒤에 놓은 것으로 설정이 덮어써진다.
import React, { Component, PropTypes } from 'react';
import card from 'semantic-ui-css/components/card.css';
import classNames from 'classnames';
const propTypes = {
className: PropTypes.string,
};
const defaultProps = {
className: ''
};
class Card extends Component {
constructor(props) {
super(props);
this.state = {};
};
componentWillMount() {};
componentDidMount() {};
componentWillReceiveProps(nextProps) {};
shouldComponentUpdate(nextProps, nextState) {
return true;
};
componentWillUpdate(nextProps, nextState) {};
componentDidUpdate(prevProps, prevState) {};
componentWillUnmount() {}
render() {
var componentClass = classNames(
'ui',
'card',
this.props.className
);
return (
<div {...this.props} className={componentClass}>{this.props.children}</div>
)
};
}
Card.propTypes = propTypes;
Card.defaultProps = defaultProps;
export default Card;
학학 다들 컴포넌트 만드는데 저녁 7시를 치닫고 있다. 아 나만 힘들어...
테스트 하기
만들어진 컴포넌트를 테스트하기 위해 src 폴더 외에 docs 폴더를 만들어 예제를 만든다.
- 시멘틱 UI 처럼 폴더 구조를 가지고 각각 index.html파일을 만든다.
- 패키지를 사용하는 입장이기 때문에 react.min.js를 넣고 테스트용이기 때문에 <script type="text/jsx;harmony=true"> 해석을 위해 JSXTransformer도 넣었다.
- 테스트 코드는 Card.js 이면 CardExample.js 로 해서 작성한다.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title> Reactjs Semantic Ui </title> </head> <body style="margin-left: 20px; margin-top: 20px;"> <div id="app"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js"></script> <script src="../../dist/reactjs-semantic-ui.js"></script> <script type="text/jsx;harmony=true" src="./CardExample.js"></script> <script type="text/jsx;harmony=true"> // Card React.render( <CardExample />, document.getElementById('app') ); // </script> </body>
</html>
CardExample.js를 다음과 같다.
- import 구문이 안먹는다. 이유 아는분 메세지 좀 주세요. 그래서 render안에 RSU 네임스페이스 제거하고 사용한다.
- Card 관련 <div class="xxx"> 설정을 리액트 컴포넌트로 만들어 사용해 본것이다.
// import { Card, CardImage, CardContent, CardHeader, CardMeta, CardDescription } from 'RSU'; class CardExample extends React.Component { render() { var Card = RSU.Card, CardImage = RSU.CardImage, CardContent = RSU.CardContent, CardHeader = RSU.CardHeader, CardMeta = RSU.CardMeta, CardDescription = RSU.CardDescription return ( <div> <h3><a href="http://semantic-ui.com/views/card.html" target="_blank">Card</a></h3> <Card> <CardImage onClick={Alert} src="../assets/images/kristy.png" /> <CardContent> <CardHeader desc="Kristy" /> <CardMeta> <span className="date">Joined in 2013</span> </CardMeta> <CardDescription> Kristy is an art director... </CardDescription> </CardContent> <CardContent className="extra"> <a> <i class="user icon"></i> 22 Friends </a> </CardContent> </Card> </div> ); }
}
이제 테스트를 위해 "npm install -g webpack-dev-server"를 설치하고 실행한다. 실행은 소스 루트에서 한다.
- 호출은 http://localhost:8080/docs/views 로 직접 경로를 지정한다.
$ webpack-dev-server --progress --colors
0% compilehttp://localhost:8080/webpack-dev-server/
webpack result is served from /
content is served from /Users/yunyoungsik/mobicon/open-sources/unplugdj/src/reactjs-semantic-ui
Hash: 0d552960154cf0b24cf3
Version: webpack 1.11.0
Time: 2759ms
Asset Size Chunks Chunk Names
reactjs-semantic-ui.js 113 kB 0 [emitted] reactjs-semantic-ui
chunk {0} reactjs-semantic-ui.js (reactjs-semantic-ui) 110 kB [rendered]
[0] ./src/index.js 418 bytes {0} [built]
[1] ./src/elements/Button.js 4.6 kB {0} [built]
[3] ./~/semantic-ui-css/components/button.css 870 bytes {0} [built]
[4] ./~/css-loader!./~/semantic-ui-css/components/button.css 87.5 kB {0} [built]
[5] ./~/css-loader/lib/css-base.js 1.51 kB {0} [built]
[6] ./~/style-loader/addStyles.js 6.09 kB {0} [built]
[7] ./src/utils/withStyles.js 3.86 kB {0} [built]
[8] ./~/fbjs/lib/invariant.js 1.51 kB {0} [built]
[9] (webpack)/~/node-libs-browser/~/process/browser.js 2.02 kB {0} [built]
[10] ./~/fbjs/lib/ExecutionEnvironment.js 1.09 kB {0} [built]
+ 1 hidden modules
webpack: bundle is now VALID.
결과화면이다. 예들 상단에 시멘틱 UI쪽에 관련 링크를 걸기로 한다.
앞으로 시멘틱 UI의 자바스크립트 코드를 리액트에 넣고 나머지 부분을 컴포넌트화 해야 한다. 그리고 성능 향상과 중복 코드 제거를 위한 Mixin을 이용한 리팩토링이 필요하다. 그 후엔 패키지 배포. 다음 시간에 다시 2차 헤커톤을 진행하기로 하고 저녁 10시에 종료!
To Be Continue...
'React' 카테고리의 다른 글
[React] 다시 시작하기 (0) | 2018.09.14 |
---|---|
[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 |