본문 바로가기
강의/스파르타코딩클럽

[리액트 숙련주차] 2-12~18 Redux

by gardenii 2023. 6. 30.

2-12 Redux 소개

1. 리덕스가 필요한 이유

- 리덕스란? 상태 관리 라이브러리

- useState의 불편함 : prop-driling

- state를 공유하고자 할 때, 부모 관계가 아니여도 되고 중간 컴포넌트를 거치지 않아도 됨

- 자식에서 만든 state를 부모에서도 사용할 수 있게 해 줌 

=> 즉, 중앙 데이터 관리소

(context API vs Redux context는 상위 컴포넌트의 값 하나만 바뀌어도 하위가 모두 렌더링되어야 하는 단점 有)

2. Global state, Local state

- Local state 지역 상태란? 컴포넌트에서 useState를 이용해서 생성한 state

- Global state 전역 상태란? 중앙화 된 특별한 곳에서 생성됨. 중앙 state 관리소. 어디서든 접근 가능

- 중앙 state 관리소에서 state를 생성하고, 만약 어떤 컴포넌트에서 state가 필요하면 어디서든지 state를 불러와서 사용할 수 있음

=>전역 상태 관리

3. 리덕스란?

- 중앙 state 관리소를 사용할 수 있게 도와주는 패키지(라이브러리)

- 전역 상태관리 라이브러리 

2-13 Redux 설정

1. 리덕스 설치

- 2개의 패키지 설치 필요 : redux, react-redux

- react-redux는 리덕스를 리액트에서 사용할 수 있도록 서로 연결시켜주는 패키지

yarn add redux react-redux

# 아래와 같은 의미
yarn add redux
yarn add react-redux

2. 폴더 구조 생성하기

 

- 📁 redux : 리덕스와 관련된 코드를 모두 모아 놓을 폴더 입니다.

- 📁 config : 리덕스 설정과 관련된 파일들을 놓을 폴더 입니다.

- 📁 configStore : “중앙 state 관리소" 인 Store를 만드는 설정 코드들이 있는 파일 입니다.

- 📁 modules : 우리가 만들 State들의 그룹이라고 생각하면 됩니다. 예를 들어 투두리스트를 만든다고 한다면, 투두리스트에 필요한 state들이 모두 모여있을 todos.js를 생성하게 되텐데요, 이 todos.js 파일이 곧 하나의 모듈이 됩니다.

 

 

 

 

 

 

 

 

3. 설정 코드 작성하기 

ser/configStore.js

- 리덕스를 저장할 스토어를 만들 createStore과 리듀서를 하나의 상태 객체로 합쳐주는 conbineReducers를 import

- rootReducer라는 상수에 combineReducers({}) 를 이용해 상태 객체로 합쳐주고, 이를 createStore(rootReducer) 하여 store 상수에 넣어줌

- 관리소를 export 해 줌 

// 중앙 데이터 관리소(store)를 설정하는 부분

import { createStore } from "redux"; // 스토어 생성
import { combineReducers } from "redux"; //리듀서를 묶는 역할

const rootReducer = combineReducers({});
const store = createStore(rootReducer);

/*
1. createStore()
리덕스의 가장 핵심이 되는 스토어를 만드는 메소드(함수) 입니다. 
리덕스는 단일 스토어로 모든 상태 트리를 관리한다고 설명해 드렸죠? 
리덕스를 사용할 시 creatorStore를 호출할 일은 한 번밖에 없을 거예요.
*/

/*
2. combineReducers()
리덕스는 action —> dispatch —> reducer 순으로 동작한다고 말씀드렸죠? 
이때 애플리케이션이 복잡해지게 되면 reducer 부분을 여러 개로 나눠야 하는 경우가 발생합니다. 
combineReducers은 여러 개의 독립적인 reducer의 반환 값을 하나의 상태 객체로 만들어줍니다.
*/

export default store;

index.js

- export 한 관리소와 Provider(react-redux)를 import 해 줌

- Provider 컴포넌트로 App 컴포넌트를 감싸주고, props로 store를 전달

-> 즉 store를 App 전체에서 쓸 수 있도록 해주는 것

// 원래부터 있던 코드
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

// 우리가 추가할 코드
import store from "./redux/config/configStore";
import { Provider } from "react-redux";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(

	//App을 Provider로 감싸주고, configStore에서 export default 한 store를 넣어줍니다.
  <Provider store={store}> 
    <App />
  </Provider>
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

* export default vs export의 차이? import 할 때, 중괄호 유무의 차이

2-14 Redux Counter (useSelector)

1. 모듈 만들기

- 모듈이란? state의 그룹!

- modules 폴더 내부에 counter.js 생성

- initialState 초기 상태값 생성, counter 리듀서 생성하여 초기값과 액션 넣어주기

- 리듀서란? state를 action의 타입에 따라 변경해주는 함수

- 타입에 따라 변경해주므로 switch문을 사용하며, 현재는 default로 초기값이 들어있는 state를 반환한다. 

// 초기 상태값 (state) - 객체, 배열, 원시데이터 모두 가능
const initialState = {
  number: 0,
};

//useState : const [number, setNumber] = useState(0)

// 리듀서 : 'state의 변화를 일으키는' 함수
// => state를 action의 type에 따라 변경하는 함수!
// => input값으로 state와 action을 받음

const counter = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

export default counter;

 

2. 생성한 모듈을 스토어에 연결하기

- 지금까지는 모듈과 스토어가 따로 분리되어 있어 생성한 state를 스토어에서 꺼낼 수 없음 

- configStore.js 코드에 counter 모듈을 import 해 줌

- rootReducer = combineReducers({ ~ }); 내부에 counter: counter 모듈을 넣어줌

- 모듈을 추가할 때 마다 연결시켜주면 됨!

3. 스토어와 모듈의 연결 확인하기

- 연결 확인 방법 : 컴포넌트에서 스토어를 조회해보기

- useSelector : 스토어를 조회하는 리덕스 훅

- 사용 방법

// 1. store에서 꺼낸 값을 할당 할 변수를 선언합니다.
const number = 

// 2. useSelector()를 변수에 할당해줍니다.
const number = useSelector() 

// 3. useSelector의 인자에 화살표 함수를 넣어줍니다.
const number = useSelector( ()=>{} )

// 4. 화살표 함수의 인자에서 값을 꺼내 return 합니다. 
// 우리가 useSelector를 처음 사용해보는 것이니, state가 어떤 것인지 콘솔로 확인해볼까요?
const number = useSelector((state) => {
    console.log(state)
    return state
});

- 적용 코드

// App.js

import React from "react";
import { useSelector } from "react-redux";

function App() {
  const data = useSelector((state) => {
    return state;
  });

  console.log(data);

  return <></>;
}

export default App;

우리가 만든 counter 라는 모듈의 state가 보임

=> 이렇게 화살표 함수에서 꺼낸 state라는 인자는 현재 프로젝트에 존재하는 모든 리덕스 모듈의 state 인 것 입니다.

4. 스토어에 저장된 모듈의 state를 사용하고 싶을 때

- 만약 counter 모듈의 number 값을 사용하고자 하면

const number = useSelector(state => state.counter.number); // 0

2-15 Redux Counter (useDispatch)

1. 리덕스의 흐름 도식화

출처 : 리덕스 공식 홈페이지

1. store에는 state와 이를 변경하는 reducer가 있음 (루트리듀서에 각 모듈의 리듀서들이 모여있음)

2. UI에서 이벤트가 일어나서 state 값을 바꿔야 한다는 요청이 들어오면, Dispatch가 action 객체를 Store로 던져줌

3. store는 던짐받은 action의 type에 따라 state 값을 변경해 줌

4. 변경된 state값을 UI에 렌더링

2. 도식화에 따라 코드 작성

1. UI에서 이벤트를 만들기

2. useDispatch 훅을 imort 해준 후, type 키와 명령으로 액션 객체를 생성하여 리듀서로 Dispatch한다.

// App.jsx

import React from "react";
import { useSelector, useDispatch } from "react-redux";

function App() {
  //dispatch를 가져와보기 - redux 훅
  const dispatch = useDispatch();

  return (
    <>
      <button
        onClick={() => {
          dispatch({
            type: "PLUS_ONE",
          });
        }}
      >
        +
      </button>
    </>
  );
}

export default App;

3. 리듀서에서 값의 수정이 일어나므로, 리듀서가 받은 명령 타입에 맞게 로을 구현해준다.

// counter.js

const initialState = {
  number: 0,
};

// 리듀서 : 'state의 변화를 일으키는' 함수
// => state를 action의 type에 따라 변경하는 함수!
// => input값으로 state와 action을 받음

const counter = (state = initialState, action) => {
  switch (action.type) {
  // 1을 더해주는 액션을 생성
    case "PLUS_ONE":
      return {
        number: state.number + 1,
      };
    default:
      return state;
  }
};

export default counter;

4.  state 값이 잘 변경되는지 확인하기 위해 state를 가져와서  UI에 렌더링한다.

// App.js

import React from "react";
import { useSelector, useDispatch } from "react-redux";

function App() {
  // 여기에서 store에 접근하여 counter의 값을 읽어오고 싶다면
  // useSelector!
  const counter = useSelector((state) => {
    return state.counter;
  });

  const dispatch = useDispatch();

  return (
    <>
      <div>현재 카운트 : {counter.number}</div>
      <button
        onClick={() => {
          dispatch({
            type: "PLUS_ONE",
          });
        }}
      >
        +
      </button>
    </>
  );
}

export default App;

3. 뺴기 기능 추가 구현 예제

// App.jsx

import React from "react";
import { useSelector, useDispatch } from "react-redux";

function App() {
  const counter = useSelector((state) => {
    return state.counter;
  });
  
  const dispatch = useDispatch();

  return (
    <>
      <div>현재 카운트 : {counter.number}</div>
      <button
        onClick={() => {
          dispatch({
            type: "PLUS_ONE",
          });
        }}
      >
        +
      </button>
      <button
        onClick={() => {
          dispatch({
            type: "MINUS_ONE",
          });
        }}
      >
        -
      </button>
    </>
  );
}

export default App;
// counter.js

const initialState = {
  number: 0,
};

const counter = (state = initialState, action) => {
  switch (action.type) {
    case "PLUS_ONE":
      return {
        number: state.number + 1,
      };
    case "MINUS_ONE":
      return {
        number: state.number - 1,
      };
    default:
      return state;
  }
};

export default counter;

4. 정리

- 액션객체란 반드시 type이라는 key를 가져야 하는 객체이다. 또한 리듀서로 보낼 명령이다.

- 디스패치란 액션객체를 리듀서로 보내는 전달자 함수이다.

- 리듀서란 디스패치를 통해 전달받은 액션객체를 검사하고, 조건이 일치했을 때 새로운 상태값을 만들어내는 함수 

- 디스패치를 사용하기 위해서는 useDispatch()훅을 사용해야 함 -> 액션객체를 파라미터로 전달

- 액션객체 type 값은 대문자로 작성 ex) "PLUS_ONE"

2-16 Redux Action Value Creator (카운터 앱 리팩토링)

1. Action Creator

- action 객체를 디스패치하고 리듀서로 수행할 때, 값이 문자열이기 때문에 오류 발생할 가능성이 높음  

- 따라서 action 객체를 한 곳에서 관리할 수 있도록 함수를 만들고, 액션 값 문자열을 상수로 만들어 상수로 사용하도록 바꿈

- Action Creator : 액션 객체를 만드는 함수

Action Creator 만들기 

// src/modules/counter.js

// 추가된 코드 👇 - 액션 value를 상수들로 만들어 줍니다. 보통 이렇게 한곳에 모여있습니다.
const PLUS_ONE = "PLUS_ONE";
const MINUS_ONE = "MINUS_ONE";


// 추가된 코드 👇 - Action Creator를 만들어 줍니다. 
export const plusOne = () => {
  return {
    type: PLUS_ONE,
  };
};

export const minusOne = () => {
  return {
    type: MINUS_ONE,
  };
};


// 초기 상태값
const initialState = {
  number: 0,
};

// 리듀서
const counter = (state = initialState, action) => {
  switch (action.type) {
    case PLUS_ONE: // case에서도 문자열이 아닌, 위에서 선언한 상수를 넣어줍니다. 
      return {
        number: state.number + 1,
      };
    case MINUS_ONE: // case에서도 문자열이 아닌, 위에서 선언한 상수를 넣어줍니다. 
      return {
        number: state.number - 1,
      };
    default:
      return state;
  }
};


export default counter;

- 이렇게 액션의 value는 상수로 따로 만들어주고, 그것을 이용해서 액션객체를 반환하는 함수를 작성합니다. 

컴포넌트에서 Action Creator 사용하기

- export 된 Action Creator를 import하고, dispatch를 통해 Action Creator를 넣어줌

// src/App.js

import React from "react";
import { useDispatch, useSelector } from "react-redux";

// 사용할 Action creator를 import 합니다.
import { minusOne, plusOne } from "./redux/modules/counter";

const App = () => {
  const dispatch = useDispatch();
  const number = useSelector((state) => state.counter.number);

  return (
    <div>
      {number}
      <button
        onClick={() => {
          dispatch(plusOne()); // 액션객체를 Action creator로 변경합니다.
        }}
      >
        + 1
      </button>
      {/* 빼기 버튼 추가 */}
      <button
        onClick={() => {
          dispatch(minusOne()); // 액션객체를 Action creator로 변경합니다.
        }}
      >
        - 1
      </button>
    </div>
  );
};

export default App;

2. Action Creator 사용하는 이유

- 휴먼에러 방지

- 유지보수 효율성 증가

- 코드 가독성

- 리덕스에서도 권장함

2-17 Redux Payload

1. Payload란?

- 뜻 : 탑제 화물, 전달되는 실체

- action 객체는 action type을 payload 만큼 처리하는 것!

- 즉 action 객체에 처리하고 싶은 값을 payload에 같이 넘기는 것

2. Payload 사용하여 기능 구현하기 - 입력받은 숫자를 더해주기

사용자가 입력 값을 받을 input 구현하기 

// src/App.js


import React from "react";
import { useState } from "react";

const App = () => {
  const [number, setNumber] = useState(0);

  const onChangeHandler = (event) => {
    const { value } = event.target;
		// event.target.value는 문자열 입니다.
		// 이것을 숫자형으로 형변환해주기 위해서 +를 붙여 주었습니다.
    setNumber(+value);
  };

	// 콘솔로 onChangeHandler가 잘 연결되었는지 확인해봅니다.
	// input에 값을 넣을 때마다 콘솔에 그 값이 찍히면 연결 성공!
  console.log(number);

  return (
    <div>
      <input type="number" onChange={onChangeHandler} />
      <button>더하기</button>
      <button>빼기</button>
    </div>
  );
};

export default App;

Payload를 넣은 Action Creator 작성하기

// src/redux/modules/counter.js

// Action Value
const ADD_NUMBER = "ADD_NUMBER";

// Action Creator
export const addNumber = (payload) => {
  return {
    type: ADD_NUMBER,
    payload: payload,
  };
};

// Initial State

// Reducer

// export default reducer

 

Payload 값을 사용하는 리듀서 작성하기

// src/redux/modules/counter.js

// Action Value
const ADD_NUMBER = "ADD_NUMBER";

// Action Creator
export const addNumber = (payload) => {
  return {
    type: ADD_NUMBER,
    payload: payload,
  };
};

// Initial State
const initialState = {
  number: 0,
};

// 리듀서
const counter = (state = initialState, action) => {
  switch (action.type) {
    case ADD_NUMBER:
      return {
		// state.number (기존의 nubmer)에 action.paylaod(유저가 더하길 원하는 값)을 더한다.
        number: state.number + action.payload,
      };
    default:
      return state;
  }
};

export default counter;

구현 기능 테스트

import React from "react";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";

// 4. Action Creator를 import 합니다.
import { addNumber } from "./redux/modules/counter";

const App = () => {
	// 1. dispatch를 사용하기 위해 선언해줍니다.
  const dispatch = useDispatch();
  const [number, setNumber] = useState(0);
  const globalNumber = useSelector((state) => state.counter.number);

  const onChangeHandler = (event) => {
    const { value } = event.target;
    setNumber(+value);
  };

	// 2. 더하기 버튼을 눌렀을 때 실행할 이벤트핸들러를 만들어줍니다.
  const onClickAddNumberHandler = () => {
		// 5. Action creator를 dispatch 해주고, 그때 Action creator의 인자에 number를 넣어줍니다.
    dispatch(addNumber(number));
  };

  return (
    <div>
      <div>{globalNumber}</div>
      <input type="number" onChange={onChangeHandler} />
			{/* 3. 더하기 버튼 이벤트핸들러를 연결해줍니다. */}
      <button onClick={onClickAddNumberHandler}>더하기</button>
      <button>빼기</button>
    </div>
  );
};

export default App;

3. 빼기 기능 추가

// counter.js

// Action Value
const ADD_NUMBER = "ADD_NUMBER";
const MINUS_NUMBER = "MINUS_NUMBER";

// Action Creator
export const addNumber = (payload) => {
  return {
    type: ADD_NUMBER,
    payload: payload,
  };
};

export const minusNumber = (payload) => {
  return {
    type: MINUS_NUMBER,
    payload: payload,
  };
};

// Initial State
const initialState = {
  number: 0,
};

// 리듀서
const counter = (state = initialState, action) => {
  switch (action.type) {
    case ADD_NUMBER:
      return {
        // state.number (기존의 nubmer)에 action.paylaod(유저가 더하길 원하는 값)을 더한다.
        number: state.number + action.payload,
      };
    case MINUS_NUMBER:
      return {
        number: state.number - action.payload,
      };
    default:
      return state;
  }
};

export default counter;
// App.jsx

import React from "react";
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";

import { addNumber } from "./redux/modules/counter";
import { minusNumber } from "./redux/modules/counter";

const App = () => {
  const dispatch = useDispatch();
  const [number, setNumber] = useState(0);
  const globalNumber = useSelector((state) => state.counter.number);

  const onChangeHandler = (event) => {
    const { value } = event.target;
    setNumber(+value);
  };

  const onClickAddNumberHandler = () => {
    dispatch(addNumber(number));
  };

  const onClickMinusNumberHandler = () => {
    dispatch(minusNumber(number));
  };

  return (
    <div>
      <div>{globalNumber}</div>
      <input type="number" onChange={onChangeHandler} />
      <button onClick={onClickAddNumberHandler}>더하기</button>
      <button onClick={onClickMinusNumberHandler}>빼기</button>
    </div>
  );
};

export default App;

 

2-18 Ducks 패턴

1. Ducks 패턴이 만들어진 이유

- 리덕스를 사용하기 위해서는 결국 구성요소를 모두 만들어야만 사용이 가능함

- 모듈이 개발자마다 다르면 협업에 어려움이 생김

- Erik Rasmussn 이라는 개발자가 패턴화하여 작성하는 것을 제안 -> Ducks 패턴

2. Ducks 패턴으로 작성하기

- Reducer 함수를 export default한다

- Action creator 함수를 export 한다.

- Action type은 app/reducer/ACTION_TYPE 형태로 작성한다.

=> 따라서 모듈 파일 1개에 type, creator, reducer가 모두 존재하는 작성 방식