Books Review - 미들웨어 추가 Redux Thunk -

book-review-thumb-img4


개발 서적 평가 서비스

- 미들웨어 추가하기 : Redux Thunk -

목차

  1. 라이브러리 인스톨하기

  2. action에 dispatch 추가하기 + redux-thunk 적용

  3. Store 수정하기

  4. 리듀서 추가 및 수정

  5. Services 추가

  6. HOC Code 수정

  7. Container 추가하기

  8. Home 관련 Component 추가하기

  9. SigninLoginForm 에 thunk 도입하기


라이브러리 인스톨하기

  • redux-thunk
  • redux-devtools-extension
1
2
npm i redux-thunk
npm i redux-devtools-extension

action에 dispatch 추가하기 + redux-thunk 적용

  • 액션 타입과, 액션, 리듀서 한 세트(Ducks 패턴)를 만드는 작업 추가
  • 미들웨어 redux-thunk 적용
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// src/actions.js
import axios from 'axios';
import BookService from './services/BookService';

export const SET_TOKEN = 'SET_TOKEN';
export const REMOVE_TOKEN = 'REMOVE_TOKEN';
export const START_LOADING = 'START_LOADING';
export const END_LOADING = 'END_LOADING';
export const SET_ERROR = 'SET_ERROR';
export const CLEAR_ERROR = 'CLEAR_ERROR';
export const SET_BOOKS = 'SET_BOOKS';

export const setToken = token => ({
type: SET_TOKEN,
token,
});
export const removeToken = () => ({
type: REMOVE_TOKEN,
});
export const startLoading = () => ({
type: START_LOADING,
});
export const endLoading = () => ({
type: END_LOADING,
});
export const setError = error => ({
type: SET_ERROR,
error,
});
export const clearError = () => ({
type: CLEAR_ERROR,
});

export const setBooks = books => ({
type: SET_BOOKS,
books,
});

// Thunk
export const loginThunk = (email, password) => async dispatch => {
try {
dispatch(clearError());
dispatch(startLoading());
const response = await axios.post('https://api.marktube.tv/v1/me', {
email,
password,
});
const { token } = response.data;
dispatch(endLoading());
localStorage.setItem('token', token);
dispatch(setToken(token));
} catch (err) {
dispatch(endLoading());
dispatch(setError(err.response.data.error));
throw err;
}
};

export const logoutThunk = token => async dispatch => {
try {
await axios.delete('https://api.marktube.tv/v1/me', {
headers: {
Authorization: `Bearer ${token}`,
},
});
} catch (error) {
console.log(error);
}

localStorage.removeItem('token'); // 토큰 지우기

dispatch(removeToken()); // 리덕스 토큰 지우기
};

export const setBooksThunk = token => async dispatch => {
try {
dispatch(startLoading());
dispatch(clearError());
const res = await BookService.getBooks(token);
dispatch(endLoading());
dispatch(setBooks(res.data));
} catch (error) {
dispatch(endLoading());
dispatch(setError(error));
}
};

export const addBookThunk = (token, books, book) => async dispatch => {
try {
dispatch(startLoading());
dispatch(clearError());
const res = await BookService.addBook(token, book);
dispatch(endLoading());
dispatch(setBooks([...books, { ...res.data, deletedAt: null }]));
} catch (error) {
dispatch(endLoading());
dispatch(setError(error));
}
};

export const deleteBookThunk = (token, books, bookId) => async dispatch => {
try {
dispatch(startLoading());
dispatch(clearError());
const res = await BookService.deleteBook(token, bookId);
dispatch(endLoading());
if (res.data.success === true) {
dispatch(setBooks(books.filter(book => book.bookId !== bookId)));
}
} catch (error) {
dispatch(endLoading());
dispatch(setError(error));
}
};

Store 수정하기

  • create함수의 2번째 인자 제거
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import 'antd/dist/antd.css';
import './index.css';
import App from './App';
import create from './store';
import { Provider } from 'react-redux';

const token = localStorage.getItem('token');
const store = create({ token });

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);

  • applyMiddleware 에 redux-thunk 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/store.js
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducer';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk';

export default function create(initialState) {
return createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(thunk)),
);
}

리듀서 추가 및 수정

Books Reducer 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/reducer/books.js
import { SET_BOOKS } from '../actions';

const initialState = [];

const books = (state = initialState, action) => {
switch (action.type) {
case SET_BOOKS:
return [...action.books];
default:
return state;
}
};

export default books;

Combine Reducer 수정 - books 리듀서 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/reducer/index.js
import token from './token';
import loading from './loading';
import error from './error';
import books from './books';

const reducer = combineReducers({
token,
loading,
error,
books,
});

export default reducer;

Loading Reducer 수정

  • 초기값을 null로 변경
  • 로딩이 끝날 경우 false아닌 null로 변경
1
2
3
4
5
6
7
8
9
10
11
12
13
// src/reducer/loading.js
import { START_LOADING, END_LOADING } from '../actions';

const initialState = null;

const loading = (state = initialState, action) => {
if (action.type === START_LOADING) {
return true;
} else if (action.type === END_LOADING) {
return null;
}
return state;
};

Services 추가

  • XHR 부분을 따로 Service로 만들어 별도 관리한다.
  • 전체 조회 : getBooks
  • 단일 조회 : getBook
  • 삭제 : deleteBook
  • 추가 : addBook
  • 수정 : editBook
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
46
47
48
// src/services/BookService.js
import axios from 'axios';

const BOOK_API_URL = 'https://api.marktube.tv/v1/book';

export default class BookService {
static async getBooks(token) {
return axios.get(BOOK_API_URL, {
headers: {
Authorization: `Bearer ${token}`,
},
});
}

static async getBook(token, bookId) {
return axios.get(`${BOOK_API_URL}/${bookId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
}

static async deleteBook(token, bookId) {
return axios.delete(`${BOOK_API_URL}/${bookId}`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
}

static async addBook(token, book) {
return axios.post(BOOK_API_URL, book, {
headers: {
Authorization: `Bearer ${token}`,
},
});
}

static async editBook(token, bookId, book) {
return axios.patch(`${BOOK_API_URL}/${bookId}`, book, {
headers: {
Authorization: `Bearer ${token}`,
},
});
}

static;
}

HOC Code 수정

  • withAuth 의 코드를 수정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/hocs/withAuth.js
import { Redirect } from 'react-router-dom';
import { useSelector } from 'react-redux';

function withAuth(Component, loggedin = true) {
function WrappedComponent(props) {
const token = useSelector(state => state.token);
if (loggedin) {
if (token === null) {
return <Redirect to="/signin" />;
}
return <Component {...props} token={token} />;
} else {
if (token !== null) {
return <Redirect to="/" />;
}
return <Component {...props} />;
}
}

WrappedComponent.displayName = `withAuth(${Component.name})`;
return WrappedComponent;
}
export default withAuth;

Container 추가하기

  • 컴포넌트에서 화면에 뿌려질 데이터만을 작성한다.
  • 모든 로직, 데이터 가공은 Container에서 처리하여 컴포넌트에게 전달한다.

SigninLoginFormContainer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/containers/SigninLoginFormContainer.jsx
import { connect } from 'react-redux';
import SigninLoginForm from '../components/Signin/SigninForm/SigninLoginForm';
import { loginThunk } from '../actions';

export default connect(
state => ({
loading: state.loading,
error: state.error,
}),
dispatch => ({
loginThunk: (email, password) => {
dispatch(loginThunk(email, password));
},
}),
)(SigninLoginForm);

AddBookContainer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/containers/AddBookContainer.jsx
import { connect } from 'react-redux';
import { addBookThunk } from '../actions';
import AddBookModal from '../components/Home/AddBookModal';

const mapStateToProps = state => ({
books: state.books,
token: state.token,
});

const mapDispatchToProps = dispatch => ({
addBook: async (token, books, book) => {
dispatch(addBookThunk(token, books, book));
},
});

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

BooksContainer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/containers/BooksContainer.jsx
import { connect } from 'react-redux';
import { setBooksThunk, deleteBookThunk } from '../actions';
import ContentUI from '../components/Home/ContentUI';

const mapStateToProps = state => ({
books: state.books,
token: state.token,
loading: state.loading,
error: state.error,
});

const mapDispatchToProps = dispatch => ({
setBooks: async token => {
dispatch(setBooksThunk(token));
},
deleteBook: async (token, books, bookId) => {
dispatch(deleteBookThunk(token, books, bookId));
},
});

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

HeaderContainer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/containers/HeaderContainer.jsx

import { connect } from 'react-redux';
import Header from '../components/Home/Header';
import { logoutThunk } from '../actions';

export default connect(
state => ({ token: state.token }),
dispatch => ({
logoutThunk: token => {
dispatch(logoutThunk(token));
},
}),
)(Header);

Home 관련 Component 추가하기.

AddBookModal Component

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// src/components/Home/AddBookModal.jsx
import React from 'react';
import { Button, Modal } from 'antd';

import InputBookInfo from './InputBookInfo';

const AddBookModal = ({
visible,
handleOk,
handleCancel,
addBook,
token,
books,
}) => {
const titleRef = React.createRef();
const messageRef = React.createRef();
const authorRef = React.createRef();
const urlRef = React.createRef();

const initValue = () => {
titleRef.current.state.value = '';
messageRef.current.state.value = '';
authorRef.current.state.value = '';
urlRef.current.state.value = '';
};

const click = () => {
const book = {
title: titleRef.current.state.value,
message: messageRef.current.state.value,
author: authorRef.current.state.value,
url: urlRef.current.state.value,
};

async function add(token, book) {
try {
await addBook(token, books, {
title: book.title,
message: book.message,
author: book.author,
url: book.url,
});
} catch (error) {
console.log(error);
}
}
add(token, book);
initValue();
handleCancel();
};

return (
<Modal
title="Add Book List"
visible={visible}
onOk={handleOk}
onCancel={handleCancel}
footer={[
<Button key="back" onClick={handleCancel}>
Cancel
</Button>,
<Button key="submit" type="primary" onClick={click}>
Add
</Button>,
]}
>
<InputBookInfo info="Title" reference={titleRef} />
<InputBookInfo info="Message" reference={messageRef} />
<InputBookInfo info="Author" reference={authorRef} />
<InputBookInfo info="URL" reference={urlRef} />
</Modal>
);
};

export default AddBookModal;

ContentUI Component

  • 기존 ContentUI Component를 수정.
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
// src/components/Home/ContentUI.jsx
import React from 'react';
import uuid from 'uuid';

import styled from 'styled-components';
import { Layout, Button, Icon } from 'antd';
import { useEffect } from 'react';

const { Content } = Layout;

... // style components

const ContentUI = ({ token, books, setBooks, deleteBook, editBook }) => {
useEffect(() => {
setBooks(token);
}, [token, setBooks]);

return (
<StyledContent>
<ul>
{books &&
books.map(book => (
<li key={uuid.v4()}>
<div>
<Icon type="read" />
</div>
<StyledTitleh3>{book.title}</StyledTitleh3>
<StyledContentP>
<span>{book.author}</span>
<span>{book.message}</span>
<span>{book.url}</span>
</StyledContentP>
<StyledDeleteButton
onClick={() => deleteBook(token, books, book.bookId)}
>
<Icon type="delete" />
</StyledDeleteButton>
</li>
))}
</ul>

Header Component

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/components/Home/Header.jsx
import React from 'react';
import HeaderUI from './HeaderUI';

const Header = ({ token, logoutThunk }) => {
function logout() {
logoutThunk(token);
}

return (
<>
<HeaderUI logout={logout}></HeaderUI>
</>
);
};
export default Header;

SigninLoginForm 에 thunk 도입하기

  • 주석 친 부분은 제거한다.
  • redux-thunk를 도입한다.
  • 기존 코드들은 Container에서 대부분 처리하고 미들웨어인 thunk에서 서비스를 처리한다.
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
// src/components/Signin/SigninForm/SigninLoginForm.jsx
import { useHistory } from 'react-router-dom';
import { Button, message } from 'antd';

// const SigninLoginForm = ({ className, loading, login, error }) => {
const SigninLoginForm = ({ className, loading, loginThunk, error }) => {
const history = useHistory();
const emailRef = React.createRef();
const passwordRef = React.createRef();

async function click() {
const email = emailRef.current.value;
const password = passwordRef.current.value;

try {
await loginThunk(email, password);
history.push('/');
} catch {}
// try {
// // setLoading(true);
// await login(email, password);
// history.push('/');
// await login(email, password);
// // history.push('/');
// } catch (error) {}
}

useEffect(() => {
if (error === null) return;
if (error === 'USER_NOT_EXIST') {
message.error('유저가 없습니다.');

... // 동일

View Project Source

by GitHub

Share