Redux-Saga를 이용한 API 통신

newsapi.org에 있는 데이터를 가져오기 위해서는, newsapi.org에서 제공하는 API를 이용해야 한다. 이때, 비동기 통신(newsapi.org 서버와의 통신)을 Redux-Saga를 이용해 데이터를 가져와보자.


순서


  1. ‘create-react-app’으로 프로젝트 생성
  2. 프로젝트 앱의 index.js에서 Redux와 연동
  3. store.js에서 Redux의 store를 세팅
  4. Redux와 연결할 Container 컴포넌트와 화면에 보여줄 각 컴포넌트 생성
  5. 통신할 API와 관련된 코드 작성
  6. action 및 action types 등록
  7. Container 컴포넌트에서 액션을 dispatch
  8. 액션을 받아 비동기 통신 처리를 할 saga 작성
  9. saga 이후, state를 변경할 reducer 작성


1. 프로젝트 앱의 index.js에서 Redux와 연동

index.js

‘create-react-app’ 한 이후, 처음 상태

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';

ReactDOM.render(<App />, document.getElementById('root'));

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();



1-1. Redux 설치

1
npm install --save react-redux



1-2. Redux-saga 설치

1
npm install --save redux-saga



1-3. Redux 세팅 및 Redux-saga 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// index.js

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import { store } from './redux/store' // redux 폴더의 store.js 파일에서 store 관련 코드 작성

render(
// Provier와 store 적용
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);


2. store.js에서 Redux의 store를 세팅

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// store.js

import { createStore, compose, applyMiddleware } from 'redux';
import rootReducer from './reducer'; // reducer.js 파일안에 rootReducer 생성
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas'; // sagas 폴더와 그 안에 index.js 파일 생성

// saga 미들웨어 생성
const sagaMiddleware = createSagaMiddleware();
const enhancer = compose(
applyMiddleware(sagaMiddleware)
)

// store 만들고, reducer와 미들웨어 적용
export const store = createStore(rootReducer, enhancer)
sagaMiddleware.run(rootSaga)

이렇게 하여, Redux store 세팅이 완료되었다.


3. Redux와 연결할 Container 컴포넌트와 화면에 보여줄 각 컴포넌트 생성

  1. Contianer 컴포넌트 생성
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // ContainerRedux.js

    import React, { Component } from 'react';
    import { connect } from "react-redux";
    import Button from './components/Button'
    import Loading from './components/Loading'
    import News from './components/News'

    class ContainerRedux extends Component {
    render() {
    return (
    <div className="">
    <Button
    />
    <Loading
    />
    <News
    />
    </div>
    );
    }
    }

    function mapStateToProps(state) {
    return {
    };
    }

    function mapDispatchToProps(dispatch) {
    return {
    };
    }

    export default connect(mapStateToProps, mapDispatchToProps)(ContainerRedux);


  1. 각 컴포넌트 생성

Button.js

1
2
3
4
5
6
7
8
9
10
11
12
13
// Button.js

import React from 'react';

const Button = ({onClickGetNews}) => {
return (
<div>
<button onClick={onClickGetNews}>get news</button>
</div>
)
}

export default Button;


Loading.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Loading.js

import React from 'react';

const Loading = ({ loading }) => {
return (
<div>
{
loading ?
<div>
<h1>LOADING ...</h1>
</div>
:
null
}
</div>
)
}

export default Loading;


News.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// News.js

import React from 'react';

const News = ({ articles }) => {
return (
<div>
{
articles ?
<div>
{
articles.map((article, key) => {
return <div key={key}>
<span>{article.title}</span>
<img src={article.urlToImage} alt=""/>
</div>
})
}
</div>
:
null
}
</div>

)
}

export default News;


4. 통신할 API와 관련된 코드 작성

4-1. axios 설치

1
npm install axios



4-2. api 코드 작성

1
2
3
4
5
6
7
// index.js in 'api' folder

import axios from 'axios';

export function getNews() {
return axios.get('https://newsapi.org/v1/articles?source=cnn&apiKey=APIKEY')
}

APIKEY는 News API에서 API KEY를 요청하여 받아야 한다.


5. action 및 action types 등록

5-1. action creator 생성

1
2
3
4
5
6
7
// action.js

import * as types from './actionType';

export const getNews = () => ({
type: types.GET_NEWS_REQUEST,
});



5-2. action type 생성

1
2
3
4
5
// actionType.js

export const GET_NEWS_REQUEST = "GET_NEWS_REQUEST";
export const GET_NEWS_SUCCESS = "GET_NEWS_SUCCESS";
export const GET_NEWS_FAILURE = "GET_NEWS_FAILURE";


6. Container 컴포넌트에서 액션을 dispatch(state 및 dispatch 코드 작성)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ContainerRedux.js

import React, { Component } from 'react';
import { connect } from "react-redux";
import Button from './components/Button'
import Loading from './components/Loading'
import News from './components/News'
import { getNews } from './redux'

class ContainerRedux extends Component {
onClickGetNews = () => {
this.props.getNews();
}
render() {
const { loading, articles } = this.props;
return (
<div className="">
<Button
onClickGetNews={this.onClickGetNews}
/>
<Loading
loading={loading}
/>
<News
articles={articles}
/>
</div>
);
}
}

function mapStateToProps(state) {
return {
articles: state.getNews.articles,
loading: state.getNews.loading
};
}

function mapDispatchToProps(dispatch) {
return {
getNews: () => dispatch(getNews())
};
}

export default connect(mapStateToProps, mapDispatchToProps)(ContainerRedux);


7. 액션을 받아 비동기 통신 처리를 할 saga 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// index.js in 'sagas' folder

import * as API from '../api';
import * as types from '../redux/actionType';
import { call, put, fork, take, all } from 'redux-saga/effects';

function* fetchNews() {
while (true) {
yield take(types.GET_NEWS_REQUEST);
try {
const res = yield call(API.getNews);
console.log('res', res)
if (res.status === 200) {
yield put({
type: types.GET_NEWS_SUCCESS,
payload: res.data
})
} else {
throw new Error();
}
} catch (e) {
yield put({
type: types.GET_NEWS_FAILURE
})
}
}

}

export default function* rootSaga() {
yield all([
fork(fetchNews)
]);
}


effect란, 각각의 task를 기술하기 위한 명령어이다. 대표적인 effect는 다음과 같다.

  • take: Action을 기다린다.
  • put: Action을 Dispatch 한다.
  • call: Promise의 완료를 기다린다.(서버 API와의 통신할 때 주로 사용)
  • fork: 다른 task를 시작한다.
  • join: 다른 task의 종료를 기다린다.
  • select: state로부터 필요한 데이터를 꺼낸다.


8. saga 이후, state를 변경할 reducer 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// reducer.js

import { combineReducers } from "redux";
import * as types from "./actionType";

const initialGetNews = { payload: { articles: [] }, loading: false }

const getNews = (state = initialGetNews, action) => {
switch (action.type) {
case types.GET_NEWS_REQUEST:
return {
...state,
loading: true
};
case types.GET_NEWS_SUCCESS:
const { articles } = action.payload;
console.log('articles', articles)
return {
...state,
articles,
loading: false
};
case types.GET_NEWS_FAILURE:
return {
...state
}
default:
return state;
}
};

const rootReducer = combineReducers({
getNews
})

export default rootReducer;


화면 재로딩(refresh) 없이, post 요청한 데이터 같은 화면에 바로 가져오는 방법


만약, 화면에서 데이터를 서버에 보내고(post), 보낸 데이터와 관련된 데이터를 받아와야 하는 경우를 saga에서 처리할 수 있다.

예를 들어, ‘기부하기’ 버튼을 클릭해서, donation 액션을 dispatch 할때, payload로 기부자 이름과 메세지를 보낸다고 하자. 그리고, 아래 화면에는 기부자와 메세지를 보여준다고 해보자. 그럼 refresh 없이, ‘기부하기’ 버튼 클릭 후, 바로 화면에 데이터를 보여줄려면 어떻게 해야할까?

이것을 saga에서 처리하는 데,

  1. 데이터를 서버에 보낸다.(yield call(API, payload))
  2. 성공하면(res.status === 200), 서버에서 데이터를 받아오는 API를 get 요청하여 받아온다.(yield call(getAPI))
  3. 성공하면(res.status === 200), 액션을 dispatch 하여, reducer에서 새로 받아온 데이터로 값을 변경하여, 화면에 데이터를 뿌려주게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function* addDonation() {
while (true) {
const request = yield take(types.ADD_DONATION_REQUEST);

try {
const { payload } = request;

// 서버로 post 요청
const res = yield call(API.addDonation, payload);

if(res.status === 200 && res.data.success){
yield put({
type: types.ADD_DONATION_SUCCESS,
payload:res.data
});

// 화면에 뿌려줄 데이터 서버로부터 가져오기
const result = yield call(API.getHistoryList,{pageSize:10, pageNum:0})
if(result.status === 200){

// 서버로부터 가져온 데이터를 화면에 뿌려주기 위한 액션을 disaptch 하기
yield put({
type: types.GET_HISTORY_LIST_SUCCESS,
payload:result.data
});
}else{
console.log('throw res2')
throw new Error();
}
}else if(res.data.success === false){
}

} catch (e) {
yield put({
type: types.ADD_DONATION_FAILURE
});
}
}
}