이 강좌는 outdated 되었습니다. 더 좋은 내용을 다루는 강좌를 준비하도록 하겠습니다. 이 강좌는 참고용으로만 읽어주세요.
이번 포스트에서는 Express 프레임워크를 사용한 Node 웹서버에서 간단한 REST API 를 구현하고, React.js 어플리케이션에서 axios 라이브러리를 통하여 AJAX 를 통하여 통신하는 방법에 대하여 알아보겠습니다. 추가적으로, 이 강좌에서는 Redux 에 대한 설명과 Express.js 서버와 React.js 를 함께 사용하는 방법에 대해선 생략되었으니 이전 강좌들을 참고해주세요.
# 프로젝트 미리보기
이전에 Redux 를 배우면서 만들었던 예제 에서는 Redux를 통해 Flux 데이터 흐름 구조를 사용하는 카운터를 만들었었죠?
오늘은 서버와 연동한 카운터를 만들어보도록 하겠습니다. 또, 별거아닌 카운터이지만, CSS 를 사용하여 꾸며보겠습니다.
# 짚고 넘어가기
React.js 는 효율적인 UI 구현을 위한 라이브러리 입니다. HTTP Client 를 내장하고있는 Angular 와는 다르게, React.js 는 따로 내장 클래스가 존재하지 않죠. 따라서 React.js 어플리케이션에서 AJAX 를 구현하려면 JavaScript 내장객체인 XMLRequest 를 사용하거나, 다른 HTTP Client 라이브러리와 함께 사용하셔야합니다.
# 어떤 HTTP Client 라이브러리를 사용하는게 좋을까?
위 질문에 대한 해답은 없습니다. 자신이 익숙한, 혹은 편하다고 생각하는 라이브러리를 사용하시면됩니다.
jQuery 라이브러리가 익숙하신분은 jQuery를 사용하셔도 됩니다. 하지만, jQuery는 ajax 외에도 저희 React.js 어플리케이션엔 더 이상 필요하지 않을 쓸모없는 기능을 많이 가지고있죠. jQuery를 사용하고 싶은 경우엔 jQuery-builder 를 사용하여 ajax 부분만 추출하여 사용하시면 됩니다.
이 링크를 참조하시면, React.js 와 함께 쓰기에 좋은 HTTP Client 라이브러리들이 있습니다.
이 포스트에서는 axios 라이브러리를 사용하도록 하겠습니다. 사용법은 크게 어렵지않으니 이 포스트에 큰 설명은 안하도록 하겠습니다.
매뉴얼을 한번 읽어주시길 바랍니다
# 시작하기
프로젝트 환경 기반은 11편 강좌 에서 만든 환경에서부터 시작하겠습니다.
해당 강좌를 처음부터 따라하셔도 되고, 간단하게 GitHub에 있는 repository를 clone 하시면됩니다.
$ git clone https://github.com/velopert/react-express-hmr-example.git && cd react-express-hmr-example && npm install
# 디렉토리 구조 이해하기
./ ├── build ├── package.json ├── public │ ├── bundle.js │ └── index.html ├── server │ ├── main.js │ └── routes │ └── counter.js ├── src │ ├── actions │ │ └── index.js │ ├── components │ │ ├── App │ │ │ └── App.js │ │ ├── Counter │ │ │ ├── Counter.css │ │ │ └── Counter.js │ │ ├── index.js │ │ └── Spinner │ │ ├── Spinner.css │ │ └── Spinner.js │ ├── index.js │ └── reducers │ └── index.js ├── webpack.config.js └── webpack.dev.config.js
- build: 컴파일된 서버사이드 코드가 위치한 디렉토리입니다.
- public: 서버의 컨텐츠 베이스 입니다
- bundle.js: 컴파일된 클라이언트 사이드 코드가 합쳐져서 이 파일에 작성됩니다.
- server: ES6 문법으로 작성된 서버사이드 코드가 위치한 디렉토리입니다.
- src: ES6 문법으로 작성된 클라이언트사이드 코드가 위치한 디렉토리입니다.
- components/index.js: 모든 컴포넌트를 import 한다음에 export 합니다 (편의를 위하여)
- index.js: webpack entry point
저희는 Express 서버에 2개의 API – 값 불러오기 & 값 1씩 추가하기 – 를 만들것이며,
3가지의 컴포넌트 – App, Counter, Spinner 를 만들것입니다.
디렉토리 구조에 보이다시피, 저희는 컴포넌트에서 CSS 파일을 사용 할 것인데요, 이는 css-loader 를 필요로합니다.
css-loader 를 적용하는 방법은 [React.JS] Tip: Webpack css-loader 를 통하여 .css 파일을 import 하여 사용하기 를 읽어주세요.
# 강좌 진행순서
- Express.js REST API 구현
- 라우터 작성하기
- GET: 현재 값 가져오기
- POST: 값에 1 더하기
- main.js 수정하기
- 라우터 작성하기
- 빈(empty) 컴포넌트 생성
- Redux 설정하기
- action 작성
- reducer 작성
- store 생성 및 컴포넌트 렌더링
- Counter 컴포넌트 작성하기
- Redux 연동
- 기본 CSS 및 뼈대 작성
- 컴포넌트 로드 후 AJAX 요청
- 클릭하였을때 AJAX 요청
- 애니메이션 효과넣기
- Spinner 컴포넌트 작성하기
- CSS 및 JS 작성
- Counter 컴포넌트 초기 로딩 떄 Spinner 보여주기
# 1. Express.js REST API 구현
# 라우터 작성하기
server/routes/counter.js
import express from 'express'; import colors from 'colors'; export default function counter(data) { function getIP(req) { return req.connection.remoteAddress.split(":").pop(); } const router = express.Router(); router.post('/', (req, res) => { console.log(colors.green('[INC]'), ++data.number, getIP(req)); return res.json({number: data.number}); }); router.get('/', (req, res) => { console.log(colors.yellow('[REQ]'), data.number, getIP(req)); return res.json({number: data.number}); }); return router; }
* 따로 데이터베이스를 사용하지 않고, main.js 파일에서 지정한 객체변수의 값을 데이터로 사용합니다.
colors 모듈은 node.js 콘솔의 텍스트에 색상을 쉽게 입힐 수 있게 해주는 모듈입니다. 컬러가 필요없다면 생략하셔도됩니다. 이 모듈을 사용하려면, npm 을 통해 설치하셔야합니다.
$ npm install --save-dev colors
# main.js 수정하기
server/main.js
import express from 'express'; import WebpackDevServer from 'webpack-dev-server'; import webpack from 'webpack'; const app = express(); const port = 3000; const devPort = 3001; if(process.env.NODE_ENV == 'development') { console.log('Server is running on development mode'); const config = require('../webpack.dev.config'); let compiler = webpack(config); let devServer = new WebpackDevServer(compiler, config.devServer); devServer.listen(devPort, () => { console.log('webpack-dev-server is listening on port', devPort); }); } app.use('/', express.static(__dirname + '/../public')); import counter from './routes/counter'; let data = { number: 0 }; app.use('/counter', counter(data)); const server = app.listen(port, () => { console.log('Express listening on port', port); });
- 웹서버가 켜질 때 마다 카운터의 값이 0으로 초기설정됩니다.
# 테스팅
$ npm run build && npm start
코드를 제대로 입력하셨더라면 오류가 날 일은 없겠지만, 완벽함을 위하여 좋아하는 REST API 테스팅 툴로 테스팅하세요.
이 포스트에서는 Chrome 확장프로그램인 Insomnia 가 사용되었습니다.
2. 비어있는 컴포넌트 만들기
왜 비어있는 컴포넌트를 만드냐구요? 먼저 파일들을 생성해둬야 나중에 작업하기도 편하고,
개발서버(webpack-dev-server) 를 열때 차질이 생기지 않기 때문입니다.
다음 명령어를 실행하면 디렉토리와 파일들을 한꺼번에 자동으로 생성합니다.
$ mkdir components components/App components/Counter components/Spinner && touch index.js components/App/App.js components/Counter/Counter.js components/Counter/Counter.css components/Spinner/Spinner.js components/Spinner/Spinner.css
src/components/index.js
import App from './App/App.js'; import Counter from './Counter/Counter.js'; import Spinner from './Spinner/Spinner.js'; export { App, Counter, Spinner };
src/components/App/App.js
import React from 'react'; import { Counter } from '../'; class App extends React.Component { render() { return ( <Counter/> ) } } export default App;
src/components/Counter/Counter.js
import React from 'react'; class Counter extends React.Component { render() { return ( <div>Counter</div> ) } } export default Counter;
src/components/Spinner/Spinner.js
import React from 'react'; class Spinner extends React.Component { render() { return ( <div>Spinner</div> ) } } export default Spinner;
# 개발서버 실행하기
$ npm run build && npm run development
서버를 실행하고 페이지에 접속해보세요. Counter 라고 떴나요? 이제 클라이언트 사이드 코드를 뚝딱뚝딱 작성해봅시다.
# 3. Redux 설정하기
# 의존 모듈 설치하기
$ npm install --save redux react-redux
# action 작성하기
src/actions/index.js
export const RECV_VALUE = "RECV_VALUE"; export function receiveValue(value) { return { type: RECV_VALUE, value: value }; };
이 프로젝트에 필요한 action 은 단 한가지입니다. 나중에 Ajax 요청을 했을 때, 그 결과값을 처리하는 action 인데요,
저희가 준비한 두 API 둘 다 같은 종류의 결과값을 반환하므로 하나의 action으로도 충분합니다.
# reducer 작성하기
src/reducers/index.js
import { RECV_VALUE } from '../actions'; const initialState = { value: -1 }; const counterReducer = (state = initialState, action) => { switch(action.type) { case RECV_VALUE: return Object.assign({}, state, { value: action.value }); default: return state; } }; export default counterReducer;
카운터의 초기값은 -1 입니다.
-1으로 설정함으로서, 컴포넌트가 렌더링 된 후, 첫 Ajax 요청이 처리가 완료되었는지 안되었는지 구분합니다.
# store 생성 및 렌더링
src/index.js
import React from 'react'; import ReactDOM from 'react-dom'; import { App } from './components'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import counterReducer from './reducers'; const store = createStore(counterReducer); const rootElement = document.getElementById('root'); ReactDOM.render( <Provider store={store}> <App/> </Provider>, rootElement );
# 4. Counter 컴포넌트 작성하기
# Redux 연동
src/components/Counter/Counter.js
import React from 'react'; import { connect } from 'react-redux'; import { receiveValue } from '../../actions'; class Counter extends React.Component { render() { return ( <div>Counter</div> ) } } const mapStateToProps = (state) => { return { value: state.value }; }; const mapDispatchToProps = (dispatch) => { return { onReceive: (value) => { dispatch(receiveValue(value)); } } } export default connect(mapStateToProps, mapDispatchToProps)(Counter);
# 기본 CSS 및 뼈대 작성
컴포넌트에서 CSS 파일을 import 하여 사용하려면 webpack css-loader 를 사용해야합니다.
의존 모듈 설치
$ npm install --save-dev style-loader css-loaderwebpack.config.js / webpack.dev.config.js 파일 수정
loaders: [ { test: /\.js$/, loader: 'babel', exclude: /node_modules/, query: { cacheDirectory: true, presets: ['es2015', 'react'] } }, { test: /\.css$/, loader: 'style!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' } ]
여러분의 마음에 드는대로 CSS를 작성하세요.
src/components/Counter/Counter.css
body { margin: 0px; } .container { background-color: #1ABC9C; height: 100%; width: 100%; position: fixed; cursor: pointer; } .center { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); } .number { font-weight: bold; text-shadow: 0 0 5px #1B6254; font-size: 7em; color: white; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }
src/components/Counter/Counter.js
import React from 'react'; import { connect } from 'react-redux'; import { receiveValue } from '../../actions'; import style from './Counter.css'; class Counter extends React.Component { render() { return ( <div className={style.container}> <div className={style.center}> <div className={style.number}> {this.props.value} </div> </div> </div> ) } } /* 생략 */
이러한 디자인이 형성되었습니다.
# axios 설치
npm install --save axios
axios 사용 예제
불러오기
import axios from 'axios';저희는 npm 을 통해 설치하였기에 프로젝트에서 직접 불러와서 사용합니다.
상황에 따라 CDN 에서 불러와도 됩니다. (
<script src="https://npmcdn.com/axios/dist/axios.min.js"></script>
)GET 요청
axios.get('/user?id=velopert') .then( response => { console.log(response); } ) // SUCCESS .catch( response => { console.log(response); } ); // ERROR axios.get('/user', { params: { id: 'velopert' } }) .then( response => { console.log(response) } ); .catch( response => { console.log(response) } ); // catch 는 생략 될 수 있습니다.POST 요청
axios.post('/msg', { user: 'velopert', message: 'hi' }) .then( response => { console.log(response) } ) .catch( response => { console.log(response) } );더 많은 사용 예제는 메뉴얼을 참고해주세요
참고: axios 는 IE에선 기본적으로는 호환이 안됩니다. 호환시키려면 https://babeljs.io/docs/usage/polyfill/ 를 참고해주세요.
# 컴포넌트 로드 후 AJAX 요청
컴포넌트의 초기 AJAX 요청은 언제나 componentDidMount LifeCycle API 안에서 하세요.
componentWillMount 안에 작성하여도 작동하긴 하나, 여기선 DOM Manipulation 이 불가합니다.
서버사이드 렌더링시엔 componentDidMount는 실행되지 않고 componentWillMount 는 실행됩니다.
src/components/Counter/Counter.js
import React from 'react'; import axios from 'axios'; import { connect } from 'react-redux'; import { receiveValue } from '../../actions'; import style from './Counter.css'; class Counter extends React.Component { componentDidMount() { let getNumber = () => { axios.get('/counter').then(response => { this.props.onReceive(response.data.number); setTimeout(getNumber, 1000 * 5); // REPEAT THIS EVERy 5 SECONDS }); } getNumber(); } /* 생략 */
컴포넌트 초기 AJAX 요청을 하고, 매 5초마다 값을 서버와 동기화 하게끔 코드를 작성하였습니다.
브라우저로 테스팅을 해보면 처음엔 값이 -1이였다가 약 0.5초 정도 후 0으로 값이 업데이트됩니다.
보기에 좀 안좋죠? 물론, -1이 아니라 아예 공백으로 하는것도 방법이긴 합니다.
저희는, 잠시 후 Spinner 컴포넌트를 만들어서, 초기 로딩을 할때 로딩창이 보이게 할 것입니다.
# 클릭하였을 때 AJAX 요청
src/components/Counter/Counter.js
/* 생략 */ class Counter extends React.Component { constructor(props) { super(props); this.onClick = this.onClick.bind(this); } componentDidMount() { /* 생략 */ } render() { /* 생략 */ } onClick() { axios.post('/counter').then(response => { this.props.onReceive(response.data.number); }); } } /* 생략 */
꽤 간단하죠? 한번 브라우저로 테스팅해보세요. 잘 되나요?
수고하셨습니다! 이제 기능상의 부분은 모두 완성되었습니다. 서버와의 연동은 이런식으로 간단하게 하면 됩니다.
하단부는 눈을 즐겁게 하기 위한 효과를 넣는 과정입니다.
관심이 없으신분들은 생략하셔도 됩니다.
# 애니메이션 효과 넣기
src/components/Counter/Counter.css
/* 생략 */ /* * Animation */ @-webkit-keyframes bounce { 0%, 20%, 50%, 80%, 100% {-webkit-transform: translateY(0);} 40% {-webkit-transform: translateY(-10px) } 60% {-webkit-transform: translateY(-5px);} } @keyframes bounce { 0%, 20%, 50%, 80%, 100% {transform: translateY(0);} 40% {transform: translateY(-10px);} 60% {transform: translateY(-5px);} } .bounce { -webkit-animation-duration: 0.5s; animation-duration: 0.5s; -webkit-animation-name: bounce; animation-name: bounce; }
src/components/Counter/Counter.js
/* 생략 */ render() { return ( <div className={style.container} onClick={this.onClick}> <div className={style.center}> <div className={style.number} ref={ ref => { this.element = ref } }> {this.props.value} </div> </div> </div> ) } componentDidUpdate() { this.element.classList.remove(style.bounce); this.element.offsetWidth; // Triggers reflow; enables restart animation this.element.classList.add(style.bounce); } /* 생략 */
이 과정에서 React.js 에서 DOM 을 manipulate 할 때 쉽게 할 수 있게 해주는 ref가 사용되었습니다.
컴포넌트가 업데이트 될 때마다 animation 클래스를 제거하고 다시 추가하는 방식으로 애니메이션을 적용하였습니다.
# 5. Spinner 만들기
http://tobiasahlin.com/spinkit/
저희는 위 페이지에서 Spinner 코드를 가져와서 사용 할 것입니다.
예쁜 로딩 Spinner 가 많으니 한번 방문해보세요
src/components/Spinner/Spinner.css
.spinner { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); /* margin: 100px auto;*/ width: 50px; height: 40px; } .spinner > div { background-color: white; height: 100%; width: 6px; display: inline-block; margin: 1px; -webkit-animation: sk-stretchdelay 1.2s infinite ease-in-out; animation: sk-stretchdelay 1.2s infinite ease-in-out; } .spinner .rect2 { -webkit-animation-delay: -1.1s; animation-delay: -1.1s; } .spinner .rect3 { -webkit-animation-delay: -1.0s; animation-delay: -1.0s; } .spinner .rect4 { -webkit-animation-delay: -0.9s; animation-delay: -0.9s; } .spinner .rect5 { -webkit-animation-delay: -0.8s; animation-delay: -0.8s; } @-webkit-keyframes sk-stretchdelay { 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 20% { -webkit-transform: scaleY(1.0) } } @keyframes sk-stretchdelay { 0%, 40%, 100% { transform: scaleY(0.4); -webkit-transform: scaleY(0.4); } 20% { transform: scaleY(1.0); -webkit-transform: scaleY(1.0); } }
src/components/Spinner/Spinner.js
import React from 'react'; import style from './Spinner.css'; export default class Spinner extends React.Component { render() { return ( <div className={style.spinner}> <div className={style.rect1}></div> <div className={style.rect2}></div> <div className={style.rect3}></div> <div className={style.rect4}></div> <div className={style.rect5}></div> </div> ); } }
# 6. Counter 수정하기
src/components/Counter/Counter.js
/* 생략 */ import { Spinner } from '../'; /* 생략 */ render() { const number = ( <div className={style.number} ref={ ref => { this.element = ref } }> {this.props.value} </div> ); const spinner = ( <Spinner/> ); return ( <div className={style.container} onClick={this.onClick}> <div className={style.center}> { (this.props.value == -1) ? spinner : number } </div> </div> ) } /* 생략 */
위와같이, Spinner 컴포넌트를 import 하고, render 메소드를 수정하면 됩니다.
이렇게 하면, 로딩화면이 제대로 뜨긴 할텐데요, 로딩시간이 그렇게 긴건 아니라서 저희가 실컷 준비한 예쁜 로딩화면을 제대로 보여주질 못하죠..
AJAX 요청을 컴포넌트가 로딩 된 후, 1초 후 실행하게 합시다.
componentDidMount() { let getNumber = () => { axios.get('/counter').then(response => { this.props.onReceive(response.data.number); setTimeout(getNumber, 1000 * 5); // REPEAT THIS EVERY 5 SECONDS }); } setTimeout(getNumber, 1000); }
수고하셨습니다!
# 마치면서..
LIVE PREVIEW: https://remotecounter.hoah.xyz/ (링크가 짤렸습니다)
GITHUB: https://github.com/velopert/react-remote-counter
이번 예제 꽤 재미있지 않았나요? 너무 심플해서 조금 식상하기도 했습니다.
다음 강좌에선 조금더 Complex 한 프로젝트를 만들어보겠습니다 – 객체배열 형식의 데이터를 처리하는 웹 어플리케이션
그리고, 더~ 다음번엔 기회가 되면 이번에 만든 프로젝트에 Socket.io 를 적용하여 5초마다 값을 업데이트하는게 아닌,
정말 실시간 으로 업데이트 하는 방법을 알아보겠습니다.
Reference
- “Where to make an Initial AJAX request from in ReactJS”. Stackoverflow.
- “Restart CSS Animation”. CSS-Tricks.