ReactStudy-07

React2-Thumbnail


React Re-Study : 7



React Study with Mark - Redux Advanced (1) -

시작하기 앞서 정리하기

컴포넌트의 모습을 결정하는 요인 2가지

  • props
  • state
  • 만약 다른것에 의해 컴포넌트가 다른 모습을 띈다면 컴포넌트를 신뢰할 수 없음(ex. error)

side effect

  • side effect란 ?
  • 함수가 일관된 결과를 보장하지 못하거나, 함수 외부 어디든지 조금이라도 영향을 주는 경우 모두 사이드 이펙트를 갖는 것
  • history.push 역시 side effect → React Re-Study : 8에서 디스패치로 해겨한다.

프리젠테이셜 컴포넌트 와 컨테이너 컴포넌트

리덕스를 사용하는 프로젝트에서 자주 사용되는 구조

Dumb 컴포넌트와 Smart 컴포넌트로도 알려져있다.

프리젠테이셔널 컴포넌트

  • 프리젠테이셔널 컴포넌트는 오직 뷰만을 담당하는 컴포넌트이다.
  • DOM 엘리먼트, 스타일을 갖고 있으며, 프리젠테이셔널 컴포넌트나 컨테이너 컴포넌트를 가지고 있을 수도 있다.
  • 하지만, 리덕스의 스토어에는 직접적인 접근 권한이 없으며 오직 props 로만 데이터를 가져올수 있다.
  • 또한, 대부분의 경우 state 를 갖고있지 않으며, 갖고 있을 경우엔 데이터에 관련된 것이 아니라 UI 에 관련된 것이어야 한다.

주로 함수형 컴포넌트로 작성되며, state 를 갖고있어야하거나, 최적화를 위해 LifeCycle 이 필요해질때 클래스형 컴포넌트로 작성된다.


컨테이너 컴포넌트

  • 프리젠테이셔널 컴포넌트들과 컨테이너 컴포넌트들을 관리하는것을 담당한다.
  • 주로 내부에 DOM 엘리먼트가 직접적으로 사용되는 경우는 적으며 사용되는 경우는 감싸는 용도일때만 사용 된다.
  • 스타일을 가지고있지 않아야 한다.
  • 스타일들은 모두 프리젠테이셔널 컴포넌트에서 정의되어야 한다.
  • 상태를 가지고 있을 때가 많으며, 리덕스에 직접적으로 접근 할 수 있다.

이 구조의 장점

UI 쪽과 Data 쪽이 분리되어 프로젝트를 이해하기가 쉬워지며, 컴포넌트의 재사용률을 높여준다.


어떤걸 컨테이너로 만들어야할까?

  • 페이지
  • 리스트
  • 헤더
  • 사이드바
  • 내부의 컴포넌트 때문에 props가 여러 컴포넌트를 거쳐야 하는 경우

Async Action with Redux

Q : 비동기 작업을 어디서 하느냐 ?

A : dispatch 를 할 때 해준다.

  • 당연히 리듀서는 동기적인 것 → Pure
  • dispatch 도 동기적인 것

비동기 처리를 위한 액션 추가 및 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
// 액션 정의
export const START_RECEIVE_BOOKS = 'START_RECEIVE_BOOKS';
export const END_RECEIVE_BOOKS = 'END_RECEIVE_BOOKS';
export const ERROR_RECEIVE_BOOKS = 'ERROR_RECEIVE_BOOKS';

// 액션 생성자 함수
export function startReceiveBooks() {
return {
type: START_RECEIVE_BOOKS,
};
}

export function endReceiveBooks(books) {
return {
type: END_RECEIVE_BOOKS,
books,
};
}

export function errorReceiveBooks() {
return {
type: ERROR_RECEIVE_BOOKS,
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// mapDispatchToProps => dispatch
// 위치 : 컨테이너 혹은 해당 페이지
const mapDispatchToProps = dispatch => ({
requestBooks: async token => {
dispatch(startLoading());
dispatch(clearError());
try {
const res = await axios.get("https://api.marktube.tv/v1/book", {
headers: {
Authorization: `Bearer ${token}`
}
});
dispatch(setBooks(res.data));
dispatch(endLoading());
} catch (error) {
console.log(error);
dispatch(setError(error));
dispatch(endLoading());
}
}
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Book.jsx 컴포넌트
import React, { useEffect } from "react";

const Book = props => <div>title : {props.title}</div>;

const Books = ({ token, loading, error, books, requestBooks }) => {
useEffect(() => {
requestBooks(token); // 컨테이너로 로직을 옮겼음.
}, [token, requestBooks]);
return (
<div>
{loading && <p>loading...</p>}
{error !== null && <p>{error.message}</p>}
{books.map(book => (
<Book title={book.title} key={book.bookId} />
))}
</div>
);
};

export default Books;

리덕스 미들웨어

  • dispatch가 일어나는 순간 dispatch의 앞과 뒤에 코드를 추가할 수 있게 해주는 것.

  • 미들웨어는 보통 행동이 일어나는 앞 뒤에 해야할 일을 붙여준다.

  • 미들웨어가 여러개면 순차적으로 실행

  • 만드는 방법 2가지

    1. 스토어를 만들 때 미들웨어를 설정한다.

      ({createStore, applyMiddleware} from redux)

    2. dispatch가 호출될 때 실제로 미들웨어를 통과하는 부분(직접 미들웨어를 만들 때 사용)


미들웨어 만들어보기

  • createStore에는 인자가 3개 들어갈 수 있다. (reducer, initialState, applyMiddleware)

    initialState를 설정안하고 applyMiddleware를 삽입해도 인식한다.

  • 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
const middleware1 = store => {
console.log(store); // {getState: ƒ, dispatch: ƒ}
return (next) => {}; // next는 함수
}

// 코드를 이렇게 추가해보자
const middleware1 = store => {
console.log(store);
return (next) => {
return action => {
console.log('middleware1', 1, action);
const value = next(action); // next는 dispatch
console.log('middleware1', 2, action);
return value;
}
};
}

const middleware2 = store => {
console.log(store);
return (next) => {
return action => {
console.log('middleware2', 1, action);
const value = next(action); // next는 dispatch
console.log('middleware2', 2, action);
return value;
}
};
}

export default function create(initialState) {
return createStore(reducer, initialState, applyMiddleware(middleware1, middleware2));
}

Redux Dev Tools

npm install --save redux-devtools-extension

현재 리덕스 관리하고 있는 상태 및 Action들을 크롬 개발자 도구를 통해 확인할 수 있는 툴

redux-dev-tools

redux-thunk

  • 리덕스 미들웨어

  • 리덕스를 만든 사람이 만듬(Dan)

  • 리덕스에서 비동기 처리를 위한 라이브러리

  • 액션 생성자를 활용하여 비동기를 처리한다. (컨테이너에 있던 비동기 함수를 액션으로 이동)

  • 액션 생성자가 액션을 리턴하지 않고, 함수를 리턴함.

  • thunk는 인자로 1. dispatch 2.현재 스테이트 를 받아올 수 있다.

    export const setBooksThunk = token => async dispatch, getState

  • 즉, 컨테이너에서도 dispatch를 제거한다.

—- 기존 redux-thunk
container redux의 state, dispatch 처리 redux의 state, thunk를 사용
Action action 타입, 생성자 정의 action 타입, 생성자 정의, thunk를 정의(dispatch)

실습

npm i redux-thunk

  1. Books에서 직접 데이터를 가져온다.
  2. 프리젠테이션 컴포넌트에서 하지 않는다 → 컨테이너로 이동
  3. thunkaction을 넘겨서 깔끔하게 처리함.
  4. action에 비동기 로직을 모두 포함한다.

redux-thunk 설정

1
2
3
4
5
6
7
8
9
10
11
import { createStore, applyMiddleware } from "redux";
import reducers from "./reducers";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk"; // import

const store = createStore(
reducers,
composeWithDevTools(applyMiddleware(thunk)) // 미들웨어 설정
);

export default store;

Before Using thunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// BooksContainer.jsx , dispatch를 컨테이너에서 설정함.
const mapDispatchToProps = dispatch => ({
requestBooks: async token => {
dispatch(startLoading());
dispatch(clearError());
try {
const res = await axios.get("https://api.marktube.tv/v1/book", {
headers: {
Authorization: `Bearer ${token}`
}
});
dispatch(setBooks(res.data));
dispatch(endLoading());
} catch (error) {
console.log(error);
dispatch(setError(error));
dispatch(endLoading());
}
}
});

Use 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
 BooksContainer.jsx
const mapDispatchToProps = dispatch => ({
requestBooks: async token => {...},
requestBooksThunk: token => {
dispatch(setBooksThunk(token)); // thunk만 로드
}
});

// actions/index.js
export const setBooksThunk = token => async dispatch => {
dispatch(startLoading());
dispatch(clearError());
try {
const res = await axios.get("https://api.marktube.tv/v1/book", {
headers: {
Authorization: `Bearer ${token}`
}
});
dispatch(setBooks(res.data));
dispatch(endLoading());
} catch (error) {
console.log(error);
dispatch(setError(error));
dispatch(endLoading());
}
};

서비스 분리

  • HTTP Request 통신 코드들만 모아놓는다. (axios, promise …)
  • 특정한 side effect의 관심사들을 모아놓은 계층.
  • 타 컨테이너나 컴포넌트에서 사용하던 axios의 의존성을 제거해줄 수 있다.
  • Dependency injection
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/services/UserService.js
import axios from 'axios';

const USER_API_URL = 'https://api.marktube.tv/v1/me';

export default class UserService {
static login(email, password) {
return axios.post(USER_API_URL, {
email,
password,
});
}

static logout(token) {
return axios.delete(USER_API_URL, {
headers: {
Authorization: `Bearer ${token}`,
},
});
}
}

HOC에 옵션을 주는 방법

  • withRouter(Component), createFragment(Component, option), connect(option)(Component)
  • 언마운트 직전의 리퀘스트를 날려야 한다. (로그인 되는데 로그아웃하면 안되기 때문)
  • 로그인, 로그아웃을 하나의 서비스로 처리한다.
Share