---
description: >-
  사실 예발자 프로젝트는 앱 규모가 복잡하지 않고, 동적으로 state 값이 변할 일이 없기 때문에 굳이 상태관리 라이브러리가 필요 없을수도
  있겠지만, 편리함을 경험해보자는 취지로 redux를 사용해보기로 했다.
---

# 8. \[React 리팩토링] 예발자 프로젝트에 Redux 적용하기

> 완성된 [예발자닷컴](https://www.yebalja.com)에 놀러오세요! 😊

## 1. 상태관리 라이브러리의 필요성

![](https://github.com/hidaehyunlee/Fork-my-brain/tree/4f7d19f4487fe8d450e22a9b6451bbed865ce125/images/c0ea2123-6225-44d7-91b1-94a46e16d2e9-image.png)

리액트 컴포넌트의 계층구조는 데이터 관리에 있어 때로는 비효율적다. 뎁스가 깊은 하위 컴포넌트에 필요한 데이터가 있을때 실제로 그 데이터를 사용하지 않는 컴포넌트를 거쳐가야하기 때문이다.

사실 예발자 프로젝트는 앱 규모가 복잡하지 않고, 동적으로 state값이 변할 일이 없기 때문에 굳이 상태관리 라이브러리가 필요 없을수도 있겠지만 편리함을 경험해보자는 취지로 `redux`를 사용해보기로 했다.

> redux가 배우기 쉽지 않은 개념이었는데 백을 담당하신 [secho](https://velog.io/@secho)님께서 프론트 팀보다 먼저 정말 열심히 공부하시고 우리 프로젝트에 바로 적용할 수 있는 정도까지의 사용법을 공유해주셨다. 덕분에 리덕스를 너무 잘 이해한 것 같아서 감사할따름.. 이 글은 secho님께 배운 내용을 정리한 글이다.

## 2. Redux의 동작방식

undefined

*   **Store**

    프로젝트당 1개만 등록한다. 모든 state와 reducer를 스토어에 모아서 관리한다.
*   **Reducer**

    컴포넌트의 상태값이 변하면 action을 전달받은 reducer 함수가 호출되고 action을 참고하여 새로운 state를 반환한다.
*   **Action**

    리덕스에서는 프로젝트에서 상태변화를 일으키는 것을 하나의 action 이라고 본다. 구체적으로 action은 `type` 값, 바뀐 `data`를 가지고 있는 **객체**이다. type은 문자열이며, 어떤 값을 변화시킬지 정해 reducer에게 알려주는 역할을 한다.
*   **Dispatch**

    action을 매개변수로 받아 reducer를 호출한다.
*   **액션생성함수 (Action Creator)**

    type을 결정하고 `action`을 반환하는 함수.

더 자세한 내용은 [여기](https://react.vlpt.us/redux/01-keywords.html) 확인

## 3. 적용하기

### 1. **redux 폴더 만들기**

`redux` 폴더를 만들고 그 안에 `actions`, `reducers`, `store` 폴더를 만든다.

### 2. **store 만들기**

```javascript
   // redux/store/configureStore.js

   import { applyMiddleware, compose } from 'redux';
   import { createWrapper} from 'next-redux-wrapper';
   import { createStore } from 'redux';
   import { composeWithDevTools } from 'redux-devtools-extension';
   import { combineReducers } from 'redux';
   import { createLogger } from 'redux-logger';
   import thunk from 'redux-thunk';
   import reducer from '../reducers/index';
   import reviewReducer from '../reducers/reviewReducer';

   const configureStore = () => {
     const logger = createLogger();
     const middlewares = [thunk , logger];
     //배포용과 개발용의 미들웨어 차이를 두기 위함
     const enhancer = process.env.NODE_ENV === 'production'
     ? compose(applyMiddleware(...middlewares))
     : composeWithDevTools(applyMiddleware(thunk))
     const store = createStore(
       combineReducers({
         reducer,
         reviewReducer}), enhancer);
     return store;
   };
```

`store/configureStore.js` 파일에 스토어를 정의한다.

몇 가지 유용한 리덕스 라이브러리들을 추가해줬는데 하나씩 알아보자면,

*   **createWrapper**

    컴포넌트에 store 등록하는 과정을 편하게 해준다.
*   **composeWithDevTools**

    state 변화를 한 눈에 확인할 수 있는 `Redux DevTolls` 라는 크롬 확장프로그램을 우리 프로젝트에 사용할 수 있게 해준다.
*   **combineReducers**

    sub reducer 들를 하나로 합쳐줌으로서 여러 리듀서를 한 store에 등록할 수 있게 해준다.
*   **createLogger**

    store가 바뀔때마다 로깅해주는 미들웨어. 리덕스 관련 정보들을 콘솔에서 예쁘게 확인할 수 있다.

    ![](https://github.com/hidaehyunlee/Fork-my-brain/tree/4f7d19f4487fe8d450e22a9b6451bbed865ce125/images/i.imgur.com-CgAuHlE.png)
*   **thunk**

    `json` 데이터를 불러올 때 비동기 처리를 하는데, 비동기 처리를 가능하게 해주는 미들웨어. 이 미들웨어를 사용하면 액션(객체)이 아닌 함수를 디스패치 할 수 있다.

    이제 reducer를 만들때마다 store에 추가하면서 관리하면 된다.

### 3. **store 등록하기**

원래 store를 사용하려면 `provider`로 컴포넌트를 감싸줘야되는데, `_app.js`에 `wrapper`를 쓰면 일일이 컴포넌트를 감싸주지 않아도 된다.

`configureStore.js` 파일 하단에 아래 코드를 추가해주자.

```javascript
   // redux/store/configureStore.js

   const wrapper = createWrapper(configureStore, {
     debug: process.env.NODE_ENV === 'development',
   });

   export default wrapper;
```

### 4. **reducer 만들기**

#### 4.1.  초기상태값(initialState) 등록하기

```javascript
   // redux/reducers/reviewReducer.js

   const initialState = {
     data: {}
   };
```

컴포넌트 별로 리듀서의 초기 state값을 빈 객체로 만들어준다. 여러 리듀서를 결합해서 store를 만들기 때문에 `store.state`를 찍으면 각 컴포넌트 리듀서들의 초기값이 모두 들어있는 걸 확인할 수 있다. 이래서 중앙관리인가보다.

#### 4.2. action.type에 따라 state 변환하기

```javascript
   // redux/reducers/reviewReducer.js
   import { HYDRATE } from 'next-redux-wrapper';

   const initialState = {
     data: {}
   };

   const reviewReducer = (state = initialState, action) => {
     switch (action.type) {
       case HYDRATE:
         return { ...state, ...action.payload };

       case 'GET_REVIEW_DATA':
         return { ...state, data: action.payload };

       default:
         return state;
     }
   };

   export default reviewReducer;
```

초기 state값을 등록했으면 액션을 받아 state를 바꿀 수 있도록 switch/case 문을 만들어준다.

아직 액션생성함수는 만들지 않았지만 `'GET_REVIEW_DATA'` type을 가진 액션이 dispatch를 통해 전달되면, state 즉 reviewReducer.data가 action.payload.data 로 바뀌도록 reducer를 만들었다.

이제 액션생성함수를 만들어 `type`과 `payload`를 가진 액션 객체를 정의해주자.

### 5. **액션생성함수 만들기**

```javascript
   // redux/actions/reviewAction.js

   import axios from 'axios';

   export const getData =  async () => {
     const reviewData = await axios.get('http://localhost:5000/api/json/reviews');
     return {
       type: 'GET_REVIEW_DATA',
       payload : reviewData.data
     }
   }
```

axios를 사용해 백 서버에 올라와있는 json 파일을 불러오는 액션생성함수를 만든다. 이 함수는 `type과` 바꿀 데이터(`payload`)를 지정한 **액션 객체를 반환**한다.

axios를 사용하면 서버 데이터만 가져오는 게 아니라 header 등등 여러 정보들을 함께 가져온다. 따라서 payload에는 `reviewData.data`를 담아줘야 우리에게 필요한 json 데이터를 가져올 수 있다.

#### axios 에러 해결하기

![image](https://user-images.githubusercontent.com/37580034/90272063-c2ed9700-de97-11ea-82ee-5b866e8c3fb5.png)

엑시오스를 사용하면 Network Error가 난다. CORS(교차출처리소스공유) 정책에 의한 오류이다. 허가되지 않는 곳에서 리소스를 요청하면 보안상 위협이 되기 때문에 요청을 받는 곳에서 CORS를 허용해줘야한다.

CORS 패키지를 인스톨 한 뒤 json.js 에서 `cors('http://localhost:3000')` 를 추가해주자.

```javascript
   router.get('/reviews', cors('http://localhost:3000'), function(req, res, next){
```

### 6. **dispatch()를 통해 컴포넌트에서 action 발생시키기**

```javascript
   // component/FAQ.js

   import React , { useEffect }from 'react';
   import { useDispatch } from 'react-redux';
   import { useSelector } from 'react-redux';
   import { getData } from '../redux/actions/faqAction';

   export default function Review( {program} ) {
     const reviewData = useSelector((state) => state.reviewReducer.data);
     const dispatch = useDispatch();
     useEffect(()=>{
       getData().then(function(result){
         dispatch(result)    
       });
     },[]);
     const programData = reviewData[program] ?? [];

     return (
       <>
         <div className={styles.box}>
           {programData?.map((v, idx, id) => 
             <CommentList data={v} key={idx} id={idx}/>)}
         </div>
       </>
     );
   }
```

`useSelector`를 통해 `state` 값을 가져올 수 있는데, `reviewData` 에 처음에는 빈 객체(아까 만든 초기값)이 들어갈 것이다. 왜냐면 `dispatch` 함수를 써야 `action` 이 발생하고 그래야 `reducer` 가 호출되어 `state` 값을 변화시킬 수 있기 때문이다.

그래서 **dispatch는 useEffect 함수 안에 넣어줘야 한다**. useEffect은 비동기적으로 리액트 앱의 렌더링이 끝난 뒤 가장 마지막에 실행되는 리액트 내장함수이기 때문이다.

즉, Review 컴포넌트는 아주 빠른시간에 두 번 렌더링 된다. 상태변화가 일어나고 액션이 발생하면 리렌더링을 통해 바뀐 state를 갖게 되는 것이다.

#### dispatch 사용법

dispatch를 구체적으로 어떻게 사용하는지, 코드를 살펴보자.

```javascript
   const dispatch = useDispatch();
     useEffect(() => {
       getData().then(function(result){
         dispatch(result) 
       });
     },[]);
```

* `dispatch`는 `reducer`를 호출한다. 따라서 `action` 객체를 매개변수로 받는다.&#x20;
* `action`을 생성하기위해 `getData()` 함수를 호출한다.
* getData() 함수 비동기처리를 기다리기 위해 `then` 메서드와 `콜백함수`를 사용한다. 만약 위 코드에서 콜백함수를 사용하지 않는다면 데이터를 온전히 받아오지 못해 우리가 참조해 사용할 수 없게 된다. 자바스크립트 비동기처리의 문제점을 해결하고 싶다면 꼭 콜백함수를 사용하자.&#x20;
* `result`에는 getData()의 반환값이 담긴다. 이를 dispatch에 전달해 reducer를 호출한다!

#### 초기 렌더링 예외처리

이제 `reviewData`에는 바뀐 state, 즉 우리에게 필요한 서버로부터 전달받은 데이터가 담기게 된다. 원래 프론트에서 만들었던 더미데이터처럼 마음껏 참조해 사용할 수 있는 것이다.

사실 이때 문제가 발생한다. 리뷰 컴포넌트가 두 번 렌더링 된다면 처음 렌더링 시점에서는 `reviewData[program]` 에 아무 값도 들어있지 않기 때문에 값을 참조할 수 없어 렌더링 ERROR가 나기 때문이다.

그래서 이 처음보는... `??` 이라는 자바스크립트 문법을 사용해줘야 한다.

```javascript
   const programData = reviewData[program] ?? [];
```

`??` 은 왼쪽 피연산자 값이 `null`이나 `undefined` 인 경우 오른쪽 피연산자를 반환한다. 이를 통해 초기 렌더링 시점에서 존재하지 않는 값에 대해 예외처리를 할 수 있다.

마찬가지로 `?.` 연산자도 에러 예외처리를 위해 사용했다.

```javascript
   programData?.map()
```

`?.` 는 참조한 값이 `null`이나 `undefined` 인 경우 실행을 멈추고 undefined를 리턴하는 연산자이다. programData는 빈 배열이라 map을 사용할 수 없다. 첫 번째 렌더링 시점에서 예외처리를 해주고, 리렌더링 된 두 번째 렌더링부터는 데이터를 참조해 가져와 사용할 수 있게 된다.
