이 강좌는 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.


