;
+ if (!data) return null;
+
+ return ;
+}
+
+export default PostContainer;
+```
+
+
+
+### PostPage
+
+```jsx
+// pages/PostPage.js
+
+import React from 'react';
+import PostContainer from '../containers/PostContainer';
+
+function PostPage({ match }) {
+ const { id } = match.params; // URL 파라미터 조회하기
+
+ // URL 파라미터 값은 문자열이기 때문에 parseInt 를 사용하여 숫자로 변환해주어야 합니다.
+ return ;
+}
+
+export default PostPage;
+```
+
+
+
+### App
+
+```jsx
+import React from 'react';
+import { Route } from 'react-router-dom';
+import PostListPage from './pages/PostListPage';
+import PostPage from './pages/PostPage';
+
+function App() {
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export default App;
+```
+
+
+
+
+
+### 결과
+
+
+
+
+
+### 문제점
+
+1. 특정 포스트를 열은다음에 뒤로 갔을 때, 포스트 목록을 또 다시 불러오게 되면서 로딩중...이 나타나게 된다.
+2. 특정 포스트를 읽고, 뒤로 간 다음에 다른 포스트를 열면 이전에 열었던 포스트가 잠깐 보여졌다가 로딩중... 이 보여지게된다.
+ - 굳이 리로딩할 필요가 없다.
+
+
+
+## 7.6. API 재로딩 문제 해결하기
+
+사용자에게 나쁜 경험을 제공 할 수 있는 API 재로딩 문제를 해결해보자.
+
+###
+
+포스트 목록이 재로딩 되는 문제를 해결하는 방법은 두가지이다.
+
+1. 만약 데이터가 이미 존재한다면 요청을 하지 않도록 하는 방법
+ - 포스트 목록이 이미 있는데 재로딩하는 이슈를 제거할 수 있다.
+2. 로딩을 새로하긴 하는데, 로딩중... 을 띄우지 않는 방법
+ - 사용자에게 좋은 경험을 제공하면서도 뒤로가기를 통해 다시 포스트 목록을 조회 할 때 최신 데이터를 보여줄 수 있다
+
+
+
+### 첫번째, 만약 데이터가 이미 존재한다면 요청을 하지 않도록 하는 방법
+
+**PostListContainer**
+
+```jsx
+// containers/PostListContainer.js
+
+import React, { useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import PostList from '../components/PostList';
+import { getPosts } from '../modules/posts';
+
+function PostListContainer() {
+ const { data, loading, error } = useSelector(state => state.posts.posts);
+ const dispatch = useDispatch();
+
+ // 컴포넌트 마운트 후 포스트 목록 요청
+ useEffect(() => {
+ if (data) return;
+ dispatch(getPosts());
+ }, [data, dispatch]);
+
+ if (loading) return
로딩중...
;
+ if (error) return
에러 발생!
;
+ if (!data) return null;
+ return ;
+}
+
+export default PostListContainer;
+```
+
+`if (data) return;` 를 통해서 데이터가 존재한다면 데이터 요청 자체를 시작하지 않는다.
+
+
+
+### 두번째, 로딩을 새로하긴 하는데, 로딩중... 을 띄우지 않는 방법
+
+**handleAsyncActions**
+
+```jsx
+// lib/asyncUtils.js - handleAsyncActions.js
+
+export const handleAsyncActions = (type, key, keepData = false) => {
+ const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
+ return (state, action) => {
+ switch (action.type) {
+ case type:
+ return {
+ ...state,
+ [key]: reducerUtils.loading(keepData ? state[key].data : null)
+ };
+ case SUCCESS:
+ return {
+ ...state,
+ [key]: reducerUtils.success(action.payload)
+ };
+ case ERROR:
+ return {
+ ...state,
+ [key]: reducerUtils.error(action.error)
+ };
+ default:
+ return state;
+ }
+ };
+};
+```
+
+`keepData` 라는 파라미터를 추가하여 만약 이 값이 `true`로 주어지면 로딩을 할 때에도 데이터를 유지하도록 수정을 해준다.
+
+
+
+**posts 리듀서**
+
+```jsx
+// modules/posts.js - posts 리듀서
+
+export default function posts(state = initialState, action) {
+ switch (action.type) {
+ case GET_POSTS:
+ case GET_POSTS_SUCCESS:
+ case GET_POSTS_ERROR:
+ return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
+ case GET_POST:
+ case GET_POST_SUCCESS:
+ case GET_POST_ERROR:
+ return handleAsyncActions(GET_POST, 'post')(state, action);
+ default:
+ return state;
+ }
+}
+```
+
+포스트 목록을 가지고 올 때 `keepDate` 인자 활성화
+
+
+
+**PostListContainer**
+
+```jsx
+import React, { useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import PostList from '../components/PostList';
+import { getPosts } from '../modules/posts';
+
+function PostListContainer() {
+ const { data, loading, error } = useSelector(state => state.posts.posts);
+ const dispatch = useDispatch();
+
+ // 컴포넌트 마운트 후 포스트 목록 요청
+ useEffect(() => {
+ dispatch(getPosts());
+ }, [dispatch]);
+
+ if (loading && !data) return
로딩중...
; // 로딩중이면서, 데이터가 없을 때에만 로딩중... 표시
+ if (error) return
에러 발생!
;
+ if (!data) return null;
+
+ return ;
+}
+
+export default PostListContainer;
+```
+
+로딩중이면서 데이터가 null일 때만 로딩을 표시
+
+
+
+### 포스트 조회시 재로딩 문제 해결하기
+
+> - 포스트 페이지에서 떠날때마다 포스트를 비우게 되므로, 다른 포스트를 읽을 때 이전 포스트가 보여지는 문제
+> - 이미 읽었던 포스트를 불러오려고 할 경우에도 새로 요청하는 문제
+
+특정 포스트를 조회하는 과정에서 재로딩 문제를 해결하려면, 방금 했던 방식으로는 처리 할 수 없다.
+
+왜냐하면 어떤 파라미터가 주어졌냐에 따라 다른 결과물이 있기 때문이다.
+
+이 문제를 해결하는 방식 또한 두가지이다.
+
+1. 컴포넌트가 언마운트될 때 포스트 내용을 비우도록 하는 방법
+2. 요청은 하지만 로딩중을 보여주지 않는 방법
+
+
+
+### 첫번째, 컴포넌트가 언마운트될때 포스트 내용을 비우도록 하는 방법
+
+**posts 리덕스 모듈에 CLEAR_POST 라는 액션 추가**
+
+```jsx
+// modules/posts.js
+
+import * as postsAPI from '../api/posts'; // api/posts 안의 함수 모두 불러오기
+import {
+ createPromiseThunk,
+ reducerUtils,
+ handleAsyncActions
+} from '../lib/asyncUtils';
+
+/* 액션 타입 */
+
+// 포스트 여러개 조회하기
+const GET_POSTS = 'GET_POSTS'; // 요청 시작
+const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // 요청 성공
+const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // 요청 실패
+
+// 포스트 하나 조회하기
+const GET_POST = 'GET_POST';
+const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
+const GET_POST_ERROR = 'GET_POST_ERROR';
+
+// 포스트 비우기
+const CLEAR_POST = 'CLEAR_POST';
+
+// 아주 쉽게 thunk 함수를 만들 수 있게 되었습니다.
+export const getPosts = createPromiseThunk(GET_POSTS, postsAPI.getPosts);
+export const getPost = createPromiseThunk(GET_POST, postsAPI.getPostById);
+
+export const clearPost = () => ({ type: CLEAR_POST });
+
+// initialState 쪽도 반복되는 코드를 initial() 함수를 사용해서 리팩토링 했습니다.
+const initialState = {
+ posts: reducerUtils.initial(),
+ post: reducerUtils.initial()
+};
+
+export default function posts(state = initialState, action) {
+ switch (action.type) {
+ case GET_POSTS:
+ case GET_POSTS_SUCCESS:
+ case GET_POSTS_ERROR:
+ return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
+ case GET_POST:
+ case GET_POST_SUCCESS:
+ case GET_POST_ERROR:
+ return handleAsyncActions(GET_POST, 'post')(state, action);
+ case CLEAR_POST:
+ return {
+ ...state,
+ post: reducerUtils.initial()
+ };
+ default:
+ return state;
+ }
+}
+```
+
+이 작업을 하려면 posts 리덕스 모듈에 CLEAR_POST 라는 액션을 준비해주어야 한다.
+
+
+
+```jsx
+// containers/PostContainer.js
+
+import React, { useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { getPost, clearPost } from '../modules/posts';
+import Post from '../components/Post';
+
+function PostContainer({ postId }) {
+ const { data, loading, error } = useSelector(state => state.posts.post);
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch(getPost(postId));
+ return () => {
+ dispatch(clearPost());
+ };
+ }, [postId, dispatch]);
+
+ if (loading) return
로딩중...
;
+ if (error) return
에러 발생!
;
+ if (!data) return null;
+
+ return ;
+}
+
+export default PostContainer;
+```
+
+이렇게 해주면, 포스트 페이지에서 떠날때마다 포스트를 비우게 되므로, 다른 포스트를 읽을 때 이전 포스트가 보여지는 문제가 해결된다.
+
+**아직 아쉬운점! **
+이미 읽었던 포스트를 불러오려고 할 경우에도 새로 요청하는것은 해결하지 못했다.
+
+이 문제를 개선하려면, posts 모듈에서 관리하는 상태의 구조를 바꿔야 한다.
+
+지금은 다음과 같이 이루어져있는데
+
+```javascript
+{
+ posts: {
+ data,
+ loading,
+ error
+ },
+ post: {
+ data,
+ loading,
+ error,
+ }
+}
+```
+
+이를 다음과 같이 구성해야한다.
+
+```javascript
+{
+ posts: {
+ data,
+ loading,
+ error
+ },
+ post: {
+ '1': {
+ data,
+ loading,
+ error
+ },
+ '2': {
+ data,
+ loading,
+ error
+ },
+ [id]: {
+ data,
+ loading,
+ error
+ }
+ }
+}
+```
+
+이를 진행하려면 기존에 asyncUtils 에 만든 여러 함수를 커스터마이징 해야하지만
+
+기존 함수를 수정하는 대신에 새로운 이름으로 다음 함수들을 작성해주도록 한다.
+
+1. createPromiseThunkById : 특정 id 를 처리하는 Thunk 생성함수
+2. handleAsyncActionsById : id별로 처리하는 유틸함수
+
+
+
+이제부터 비동기 작업에 관련된 액션이 어떤 id를 가르키는지 알아야 한다.
+
+그렇게 하기 위해서 앞으로 action.meta 값에 id를 넣어주도록 한다.
+
+
+
+**asyncUtils**
+
+```jsx
+// lib/asyncUtils.js
+
+(...)
+
+
+// 특정 id 를 처리하는 Thunk 생성함수
+const defaultIdSelector = param => param;
+export const createPromiseThunkById = (
+ type,
+ promiseCreator,
+ // 파라미터에서 id 를 어떻게 선택 할 지 정의하는 함수입니다.
+ // 기본 값으로는 파라미터를 그대로 id로 사용합니다.
+ // 하지만 만약 파라미터가 { id: 1, details: true } 이런 형태라면
+ // idSelector 를 param => param.id 이런식으로 설정 할 수 있곘죠.
+ idSelector = defaultIdSelector
+) => {
+ const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
+
+ return param => async dispatch => {
+ const id = idSelector(param);
+ dispatch({ type, meta: id });
+ try {
+ const payload = await promiseCreator(param);
+ dispatch({ type: SUCCESS, payload, meta: id });
+ } catch (e) {
+ dispatch({ type: ERROR, error: true, payload: e, meta: id });
+ }
+ };
+};
+
+// id별로 처리하는 유틸함수
+export const handleAsyncActionsById = (type, key, keepData = false) => {
+ const [SUCCESS, ERROR] = [`${type}_SUCCESS`, `${type}_ERROR`];
+ return (state, action) => {
+ const id = action.meta;
+ switch (action.type) {
+ case type:
+ return {
+ ...state,
+ [key]: {
+ ...state[key],
+ [id]: reducerUtils.loading(
+ // state[key][id]가 만들어져있지 않을 수도 있으니까 유효성을 먼저 검사 후 data 조회
+ keepData ? state[key][id] && state[key][id].data : null
+ )
+ }
+ };
+ case SUCCESS:
+ return {
+ ...state,
+ [key]: {
+ ...state[key],
+ [id]: reducerUtils.success(action.payload)
+ }
+ };
+ case ERROR:
+ return {
+ ...state,
+ [key]: {
+ ...state[key],
+ [id]: reducerUtils.error(action.payload)
+ }
+ };
+ default:
+ return state;
+ }
+ };
+};
+```
+
+
+
+**posts 리덕스 모듈**
+
+```jsx
+// modules/posts.js
+
+import * as postsAPI from '../api/posts'; // api/posts 안의 함수 모두 불러오기
+import {
+ createPromiseThunk,
+ reducerUtils,
+ handleAsyncActions,
+ createPromiseThunkById,
+ handleAsyncActionsById
+} from '../lib/asyncUtils';
+
+/* 액션 타입 */
+
+// 포스트 여러개 조회하기
+const GET_POSTS = 'GET_POSTS'; // 요청 시작
+const GET_POSTS_SUCCESS = 'GET_POSTS_SUCCESS'; // 요청 성공
+const GET_POSTS_ERROR = 'GET_POSTS_ERROR'; // 요청 실패
+
+// 포스트 하나 조회하기
+const GET_POST = 'GET_POST';
+const GET_POST_SUCCESS = 'GET_POST_SUCCESS';
+const GET_POST_ERROR = 'GET_POST_ERROR';
+
+// 아주 쉽게 thunk 함수를 만들 수 있게 되었습니다.
+export const getPosts = createPromiseThunk(GET_POSTS, postsAPI.getPosts);
+export const getPost = createPromiseThunkById(GET_POST, postsAPI.getPostById);
+
+// initialState 쪽도 반복되는 코드를 initial() 함수를 사용해서 리팩토링 했습니다.
+const initialState = {
+ posts: reducerUtils.initial(),
+ post: {}
+};
+
+export default function posts(state = initialState, action) {
+ switch (action.type) {
+ case GET_POSTS:
+ case GET_POSTS_SUCCESS:
+ case GET_POSTS_ERROR:
+ return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
+ case GET_POST:
+ case GET_POST_SUCCESS:
+ case GET_POST_ERROR:
+ return handleAsyncActionsById(GET_POST, 'post')(state, action);
+ default:
+ return state;
+ }
+}
+```
+
+
+
+ **PostContainer**
+
+```jsx
+import React, { useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { getPost } from '../modules/posts';
+import Post from '../components/Post';
+
+function PostContainer({ postId }) {
+ const { data, loading, error } = useSelector(
+ state => state.posts.post[postId]
+ ) || {
+ loading: false,
+ data: null,
+ error: null
+ }; // 아예 데이터가 존재하지 않을 때가 있으므로, 비구조화 할당이 오류나지 않도록
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ if (data) return; // 포스트가 존재하면 아예 요청을 하지 않음
+ dispatch(getPost(postId));
+ }, [postId, dispatch, data]);
+
+ if (loading) return
로딩중...
;
+ if (error) return
에러 발생!
;
+ if (!data) return null;
+
+ return ;
+}
+
+export default PostContainer;
+```
+
+위 방식은 아예 요청을 하지 않는 방식으로 해결한 것이다.
+
+
+
+### 두번째, 요청은 하지만 로딩중을 보여주지 않는 방법
+
+**만약, 요청은 하지만 로딩중은 다시 보여주지 않는 방식으로 해결하려면???**
+
+리듀서와 PostContainer를 다음과 같이 수정한다.
+
+#### modules/posts.js - posts
+
+```javascript
+export default function posts(state = initialState, action) {
+ switch (action.type) {
+ case GET_POSTS:
+ case GET_POSTS_SUCCESS:
+ case GET_POSTS_ERROR:
+ return handleAsyncActions(GET_POSTS, 'posts', true)(state, action);
+ case GET_POST:
+ case GET_POST_SUCCESS:
+ case GET_POST_ERROR:
+ return handleAsyncActionsById(GET_POST, 'post', true)(state, action); // keepData!
+ default:
+ return state;
+ }
+}
+```
+
+PostContainer는 다음과 같이 수정하면 됩니다.
+
+#### containers/PostContainer.js
+
+```jsx
+import React, { useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { getPost } from '../modules/posts';
+import Post from '../components/Post';
+
+function PostContainer({ postId }) {
+ const { data, loading, error } = useSelector(
+ state => state.posts.post[postId]
+ ) || {
+ loading: false,
+ data: null,
+ error: null
+ }; // 아예 데이터가 존재하지 않을 때가 있으므로, 비구조화 할당이 오류나지 않도록
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch(getPost(postId));
+ }, [dispatch, postId]);
+
+ if (loading && !data) return
로딩중...
; // 로딩중이고 데이터 없을때만!!!
+ if (error) return
에러 발생!
;
+ if (!data) return null;
+
+ return ;
+}
+
+export default PostContainer;
+```
+
+
+
+
+
+## 7.7 thunk에서 라우터 연동하기
+
+프로젝트를 개발하다보면, thunk 함수 내에서 라우터를 사용해야 될 때도 있다.
+
+예를 들자면, 로그인 요청을 하는데 로그인이 성공 할 시 `/` 경로로 이동시키고, 실패 할 시 경로를 유지 하는 형태로 구현 하는 방식으로 말이다.
+
+
+
+방법
+
+1. 컨테이너 컴포넌트내에서 그냥 단순히 [withRouter](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/withRouter.md)를 사용해서 props 로 `history` 를 가져와서 사용
+2. hunk에서 처리
+ - thunk에서 처리를 하면 코드가 훨씬 깔끔해질 수 있다. (취향)
+
+
+
+### customHistory 만들어서 적용하기
+
+thunk 에서 라우터의 history 객체를 사용하려면, `BrowserHistory` 인스턴스를 직접 만들어서 적용해야한다.
+
+
+
+index.jsx
+
+```jsx
+import React from 'react';
+import ReactDOM from 'react-dom';
+import './index.css';
+import App from './App';
+import * as serviceWorker from './serviceWorker';
+import { createStore, applyMiddleware } from 'redux';
+import { Provider } from 'react-redux';
+import rootReducer from './modules';
+import logger from 'redux-logger';
+import { composeWithDevTools } from 'redux-devtools-extension';
+import ReduxThunk from 'redux-thunk';
+import { Router } from 'react-router-dom';
+import { createBrowserHistory } from 'history'; // 요기!
+
+const customHistory = createBrowserHistory(); // 요기!
+
+const store = createStore(
+ rootReducer,
+ // logger 를 사용하는 경우, logger가 가장 마지막에 와야합니다.
+ composeWithDevTools(
+ applyMiddleware(
+ ReduxThunk.withExtraArgument({ history: customHistory }), // 요기!
+ logger
+ )
+ )
+); // 여러개의 미들웨어를 적용 할 수 있습니다.
+
+ReactDOM.render(
+ // 요기!
+
+
+
+ ,
+ document.getElementById('root')
+);
+
+serviceWorker.unregister();
+```
+
+
+
+`redux-thunk` 의 `withExtraArgument` 를 사용하면 thunk함수에서 사전에 정해준 값들을 참조 할 수 있다.
+
+
+
+### 홈 화면으로 가는 thunk 만들기
+
+
+
+#### modules/posts.js - goToHome
+
+```jsx
+// 3번째 인자를 사용하면 withExtraArgument 에서 넣어준 값들을 사용 할 수 있습니다.
+export const goToHome = () => (dispatch, getState, { history }) => {
+ history.push('/');
+};
+```
+
+이게 바로 홈화면으로 가는 thunk
+
+
+
+#### containers/PostContainer.js
+
+```jsx
+import React, { useEffect } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import { getPost, goToHome } from '../modules/posts';
+import Post from '../components/Post';
+
+function PostContainer({ postId }) {
+ const { data, loading, error } = useSelector(
+ state => state.posts.post[postId]
+ ) || {
+ loading: false,
+ data: null,
+ error: null
+ }; // 아예 데이터가 존재하지 않을 때가 있으므로, 비구조화 할당이 오류나지 않도록
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch(getPost(postId));
+ }, [dispatch, postId]);
+
+ if (loading && !data) return
로딩중...
; // 로딩중이고 데이터 없을때만
+ if (error) return
에러 발생!
;
+ if (!data) return null;
+
+ return (
+ <>
+ // 요거 추가!
+
+ >
+ );
+}
+
+export default PostContainer;
+```
+
+이제 PostContainer.js 에서 이 thunk 를 디스패치를한다.
+
+
+
+**결과**
+
+
+
+지금은 단순히 다른 작업을 하지 않고 바로 홈으로 이동하게끔 했지만,
+
+실제 프로젝트에서는 `getState()` 를 사용하여 현재 리덕스 스토어의 상태를 확인하여 조건부로 이동을 하거나, 특정 API를 호출하여 성공했을 시에만 이동을 하는 형식으로 구현을 할 수 있다고 한다.
+
+
+
+
+
+## 7.8 json-server
+
+### json-server란?
+
+가짜 API 서버를 만드는 도구이다.
+
+ json 파일 하나만 있으면 연습용 서버를 쉽게 구성 할 수 있다.
+
+
+
+
+
+### 서버에서 제공할 데이터 준비
+
+프로젝트 디렉토리 `data.json` 을 준비한다.
+
+```jsx
+{
+ "posts": [
+ {
+ "id": 1,
+ "title": "리덕스 미들웨어를 배워봅시다",
+ "body": "리덕스 미들웨어를 직접 만들어보면 이해하기 쉽죠."
+ },
+ {
+ "id": 2,
+ "title": "redux-thunk를 사용해봅시다",
+ "body": "redux-thunk를 사용해서 비동기 작업을 처리해봅시다!"
+ },
+ {
+ "id": 3,
+ "title": "redux-saga도 사용해봅시다",
+ "body": "나중엔 redux-saga를 사용해서 비동기 작업을 처리하는 방법도 배워볼 거예요."
+ }
+ ]
+}
+```
+
+
+
+
+이 파일을 기반으로 서버를 연다.
+
+```bash
+$ npx json-server ./data.json --port 4000
+```
+
+또는 json-server 를 글로벌로 설치해서 다음과 같이 사용 할 수도 있다.
+
+```bash
+$ yarn global add json-server
+$ json-server ./data.json --port 4000
+```
+
+
+
+
+
+json-server 를 실행하시면 터미널에 다음과 같이 결과물이 다.
+
+```javascript
+ \{^_^}/ hi!
+
+ Loading ./data.json
+ Done
+
+ Resources
+ http://localhost:4000/posts
+
+ Home
+ http://localhost:4000
+```
+
+그러면 우리의 가짜 API 서버가 4000 포트로 열린다.
+
+- http://localhost:4000/posts
+- http://localhost:4000/posts/1
\ No newline at end of file
diff --git "a/\354\240\225\354\234\244\355\230\270/ch7.\353\246\254\353\215\225\354\212\244_\353\257\270\353\223\244\354\233\250\354\226\264.md" "b/\354\240\225\354\234\244\355\230\270/ch7.\353\246\254\353\215\225\354\212\244_\353\257\270\353\223\244\354\233\250\354\226\264.md"
new file mode 100644
index 0000000..e717c57
--- /dev/null
+++ "b/\354\240\225\354\234\244\355\230\270/ch7.\353\246\254\353\215\225\354\212\244_\353\257\270\353\223\244\354\233\250\354\226\264.md"
@@ -0,0 +1,518 @@
+## 7.1. 리덕스 프로젝트 준비하기
+
+> 리덕스 리마인드 하면서 셋업해봅시다.
+
+
+
+### 리덕스 모듈
+
+- Counter 리듀서
+
+```js
+// modules/counter.js
+
+// 액션 타입
+const INCREASE = 'INCREASE';
+const DECREASE = 'DECREASE';
+
+// 액션 생성 함수
+export const increase = () => ({ type: INCREASE });
+export const decrease = () => ({ type: DECREASE });
+
+// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
+const initialState = 0;
+
+export default function counter(state = initialState, action) {
+ switch (action.type) {
+ case INCREASE:
+ return state + 1;
+ case DECREASE:
+ return state - 1;
+ default:
+ return state;
+ }
+}
+```
+
+
+
+- 루트 리듀서
+
+```js
+// modules/index.js
+
+import { combineReducers } from 'redux';
+import counter from './counter';
+
+const rootReducer = combineReducers({ counter });
+
+export default rootReducer;
+```
+
+
+
+### 컴포넌트
+
+- Counter 프레젠테이셔널 컴포넌트
+
+```jsx
+// components/Counter.js
+
+import React from 'react';
+
+function Counter({ number, onIncrease, onDecrease }) {
+ return (
+
+
{number}
+
+
+
+ );
+}
+
+export default Counter;
+
+
+```
+
+
+
+- Counter 컨테이너 컴포넌트
+
+```jsx
+// containers/CounterContainer.js
+
+import React from 'react';
+import Counter from '../components/Counter';
+import { useSelector, useDispatch } from 'react-redux';
+import { increase, decrease } from '../modules/counter';
+
+function CounterContainer() {
+ const number = useSelector(state => state.counter);
+ const dispatch = useDispatch();
+
+ const onIncrease = () => {
+ dispatch(increase());
+ };
+ const onDecrease = () => {
+ dispatch(decrease());
+ };
+
+ return (
+
+ );
+}
+
+export default CounterContainer;
+```
+
+
+
+- index.jsx
+
+```jsx
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+import { createStore } from 'redux';
+import { Provider } from 'react-redux';
+import rootReducer from './modules';
+
+const store = createStore(rootReducer);
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById('root')
+);
+
+```
+
+
+
+- App.jsx
+
+```jsx
+import React from 'react';
+import CounterContainer from './containers/CounterContainer';
+
+function App() {
+ return ;
+}
+
+export default App;
+```
+
+
+
+## 7.2. 미들웨어 만들어보고 이해하기
+
+실무에서는 리덕스 미들웨어를 직접 만들일은 없다.
+
+하지만 직접 만들어보면 리덕스 미들웨어가 어떤 역할인지 훨씬 이해할 수 있다.
+
+
+
+### 리덕스 미들웨어 템플릿
+
+리덕스 미들웨어를 만들 땐 다음과 같은 템플릿으로 작성한다.
+
+```js
+const middleware = store => next => action => {
+ // 하고 싶은 작업...
+}
+```
+
+##### 인자
+
+- `store` : 리덕스 스토어 인스턴스로써 `store` 내부에는 `dispatch`, `getStore`, `subscribe` 내장함수들이 있다.
+
+- `next` : 액션을 다음 미들웨어에게 전달하는 함수. `next(action)` 형태로 사용한다. 만약 다음 미들웨어가 없다면 리듀서에게 액션을 전달해준다. 만약 `next` 를 호출하지 않게 된다면 액션이 무시 처리되어 리듀서에게 전달되지 않는다.
+- `action` : 현재 처리하고 있는 **액션객체**
+
+
+
+#### 동작방식
+
+리덕스 스토어에 여러개의 미들웨어를 등록할 수 있다. 그리고 아래와 같은 형태로 동작한다.
+
+
+
+- 새로운 액션이 디스패치되면 첫 번 째로 등록한 미들웨어가 호출된다.
+- 만약 미들웨어에서 `next(action)` 을 호출하면 다음 미들웨어로 액션이 넘어간다.
+- 만약 미들웨어에서 `store.dispatch`를 사용하면 다른 액션을 추가적으로 발생시킬 수 도 있다.
+
+
+
+### 미들웨어 직접 작성하기
+
+- MyLogger 미들웨어
+
+```js
+// middlewares/myLogger.js
+
+const myLogger = store => next => action => {
+ console.log(action); // 먼저 액션을 출력합니다.
+ const result = next(action); // 다음 미들웨어 (또는 리듀서) 에게 액션을 전달합니다.
+
+ // 업데이트 이후의 상태를 조회합니다.
+ console.log('\t', store.getState()); // '\t' 는 탭 문자 입니다.
+
+ return result; // 여기서 반환하는 값은 dispatch(action)의 결과물이 됩니다. 기본: undefined
+ // return을 반환하지 않아도 된다.
+};
+
+export default myLogger;
+```
+
+
+
+- index.jsx
+
+```jsx
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+import { createStore, applyMiddleware } from 'redux'; //추가!
+import { Provider } from 'react-redux';
+import rootReducer from './modules';
+import myLogger from './middlewares/myLogger'; // 추가!
+
+const store = createStore(rootReducer, applyMiddleware(myLogger)); // 추가!
+
+ReactDOM.render(
+
+
+ ,
+ document.getElementById('root')
+);
+```
+
+
+
+- 결과
+
+
+
+
+
+#### 미들웨어 안에서는 무엇이든지 할 수 있다.
+
+- 액션 값을 객체가 아닌 함수도 받아오게 만들어서 액션이 함수타입이면 이를 실행시키게끔 할 수도 있다.
+ - 이것이 `redux-thunk` 이다.
+
+
+
+예시
+
+```js
+const thunk = store => next => action =>
+ typeof action === 'function'
+ ? action(store.dispatch, store.getState)
+ : next(action)
+```
+
+이렇게 하면 다음과 같이 할 수 있다.
+
+```js
+const myThunk = () => (dispatch, getState) => {
+ dispatch({ type: 'HELLO' });
+ dispatch({ type: 'BYE' });
+}
+
+dispatch(myThunk());
+```
+
+
+
+이게 무엇인지는 `redux-thunk` 파트에서 알아보자.
+
+
+
+## 7.3. redux-logger 사용 및 미들웨어와 DevTools 함께 사용하기
+
+#### redux-logger란?
+
+리덕스 관련 값들을 콘솔에 로깅하도록 도와주는 미들웨어
+
+
+
+### 예제
+
+```jsx
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+import { createStore, applyMiddleware } from 'redux';
+import { Provider } from 'react-redux';
+import rootReducer from './modules';
+import myLogger from './middlewares/myLogger';
+import logger from 'redux-logger';
+
+const store = createStore(rootReducer, applyMiddleware(myLogger, logger));
+// 여러개의 미들웨어를 적용 할 수 있습니다.
+
+ReactDOM.render(
+
+
+
+
+ ,
+ document.getElementById('root')
+);
+```
+
+
+
+
+
+### Redux DevTools 사용하기
+
+리덕스 미들웨어와 리덕스 데브툴스 함께 사용하는 방법에 대해 알아보자.
+
+```jsx
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+import { createStore, applyMiddleware } from 'redux';
+import { composeWithDevTools } from 'redux-devtools-extension';
+import { Provider } from 'react-redux';
+import rootReducer from './modules';
+import myLogger from './middlewares/myLogger';
+import logger from 'redux-logger';
+
+// 단순하게 composeWithDevTools의 인자로 applyMiddleware를 넘겨주면 된다.
+const store = createStore(
+ rootReducer,
+ composeWithDevTools(applyMiddleware(logger))
+);
+
+ReactDOM.render(
+
+
+
+
+ ,
+ document.getElementById('root')
+);
+
+```
+
+
+
+## 7.4 4. redux-thunk
+
+### redux-thunk란?
+
+redux-thunk를 사용하면 **액션 객체가 아닌 함수를 디스패치 할 수 있다.**
+
+일반적인 dispatch는 값을 반환하지만
+
+dispatch를 실행하면서 다양한 함수의 동작을 하고싶을 때가 있을 것이다. 그리고 함수를 분리하고 싶을 때가 있을것이다.
+
+**가장 유용하게 사용할 수 있는 케이스는 비동기 로직 이후에 dispatch를 하기 위할 때이다.**
+
+> **일반적인 dispatch의 인자로 전달되는 액션생성함수는 `async/await` 을 적용할 수 없다.**
+
+dispatch, 리듀서 로직에는 비동기로직이 없는 순수함수여야한다.
+
+```js
+const thunk = store => next => action =>
+ typeof action === 'function'
+ ? action(store.dispatch, store.getState)
+ : next(action)
+```
+
+redux-thunk 또한 미들웨어이다. redux-thunk의 로직은 위와 같다.
+
+인자 `action` 이 함수이면 `store.dispatch`, `store.getState` 를 인자로 넘겨서 실행을 시킨다.
+
+
+
+> 함수를 디스패치 할 때에는,
+>
+> 해당 함수에서 `dispatch` 와 `getState` 를 파라미터로 받아와주어야 한다.
+>
+> 이 함수를 **만들어주는 함수**를 우리는 thunk 라고 부른다.
+
+
+
+### thunk의 사용 예시
+
+```js
+const getComments = () => (dispatch, getState) => {
+ // 이 안에서는 액션을 dispatch 할 수도 있고
+ // getState를 사용하여 현재 상태도 조회 할 수 있습니다.
+ const id = getState().post.activeId;
+
+ // 요청이 시작했음을 알리는 액션
+ dispatch({ type: 'GET_COMMENTS' });
+
+ // 댓글을 조회하는 프로미스를 반환하는 getComments 가 있다고 가정해봅시다.
+ api
+ .getComments(id) // 요청을 하고
+ .then(comments => dispatch({ type: 'GET_COMMENTS_SUCCESS', id, comments })) // 성공시
+ .catch(e => dispatch({ type: 'GET_COMMENTS_ERROR', error: e })); // 실패시
+};
+```
+
+
+
+thunk함수에서 async/await을 사용해도 된다.
+
+```js
+const getComments = () => async (dispatch, getState) => {
+ const id = getState().post.activeId;
+ dispatch({ type: 'GET_COMMENTS' });
+ try {
+ const comments = await api.getComments(id);
+ dispatch({ type: 'GET_COMMENTS_SUCCESS', id, comments });
+ } catch (e) {
+ dispatch({ type: 'GET_COMMENTS_ERROR', error: e });
+ }
+}
+```
+
+
+
+### 카운터앱에 적용해보기
+
+- index.jsx
+
+```js
+import React from 'react';
+import ReactDOM from 'react-dom';
+import App from './App';
+import { createStore, applyMiddleware } from 'redux';
+import { composeWithDevTools } from 'redux-devtools-extension';
+import { Provider } from 'react-redux';
+import rootReducer from './modules';
+import logger from 'redux-logger';
+import ReduxThunk from 'redux-thunk';
+
+const store = createStore(
+ rootReducer,
+ composeWithDevTools(applyMiddleware(ReduxThunk, logger)) // logger 를 사용하는 경우, logger가 가장 마지막에 와야합니다.
+);
+
+ReactDOM.render(
+
+
+
+
+ ,
+ document.getElementById('root')
+);
+```
+
+
+
+- Counter 리덕스 모듈
+
+`increaseAsync`와 `decreaseAsync`라는 thunk 함수를 만들었다.
+
+```js
+// modules/counter.js
+
+// 액션 타입
+const INCREASE = 'INCREASE';
+const DECREASE = 'DECREASE';
+
+// 액션 생성 함수
+export const increase = () => ({ type: INCREASE });
+export const decrease = () => ({ type: DECREASE });
+
+// getState를 쓰지 않는다면 굳이 파라미터로 받아올 필요 없습니다.
+export const increaseAsync = () => dispatch => {
+ setTimeout(() => dispatch(increase()), 1000);
+};
+export const decreaseAsync = () => dispatch => {
+ setTimeout(() => dispatch(decrease()), 1000);
+};
+
+// 초깃값 (상태가 객체가 아니라 그냥 숫자여도 상관 없습니다.)
+const initialState = 0;
+
+export default function counter(state = initialState, action) {
+ switch (action.type) {
+ case INCREASE:
+ return state + 1;
+ case DECREASE:
+ return state - 1;
+ default:
+ return state;
+ }
+}
+
+```
+
+
+
+- Counter 컴테이너 컴포넌트
+
+```jsx
+import React from 'react';
+import Counter from '../components/Counter';
+import { useSelector, useDispatch } from 'react-redux';
+import { increaseAsync, decreaseAsync } from '../modules/counter';
+
+function CounterContainer() {
+ const number = useSelector(state => state.counter);
+ const dispatch = useDispatch();
+
+ const onIncrease = () => {
+ dispatch(increaseAsync());
+ };
+ const onDecrease = () => {
+ dispatch(decreaseAsync());
+ };
+
+ return (
+
+ );
+}
+
+export default CounterContainer;
+```
+