[React] redux, payload, ducks 패턴

Redux

: 전역상태 라이브러리

 

필요성

어떤 컴포넌트에서 생성한 state를 다른 컴포넌트로 보내고자할 때 props를 사용함

하지만 prop drilling이 일어나면서 그 state가 필요없는 컴포넌트도 props를 전달받고, 전달해야 함

또한 자식 컴포넌트에서 부모 컴포넌트로 값을 보낼 수 없다

 

Redux를 사용하면 자식에서 만든 state를 부모에서 사용할 수도 있고, 불필요한 전달이 필요없어진다

 

Global state 와 Local state

1) Local state (지역상태)

- 컴포넌트에서 useState를 이용해 생성한 state

 

2) Global state (전역상태)

- 컴포넌트에서 생성되지 않음!

- 중앙화된 특별한 곳에서 생성된 state들

- 컴포넌트가 어디 위치해있던 상관없이 state를 불러와 사용할 수 있음

- 또한 이런 값들을 관리하는 것을 전역 상태 관리라고 함

 

context API 사용시 제한사항

1) 성능 최적화

- context는 provider 하위의 모든 컴포넌트를 리렌더링

- 상태가 변경될 때마다 관련된 모든 컴포넌트가 불필요한 리렌더링이 발생하는 것을 막을 수 없음

- 반면 리덕스는 상태 변경과 관련된 컴포넌트만 선택적으로 업데이트할 수 있음

 

2) 상태 로직의 중앙화와 일관성

- 리덕스는 상태를 하나의 저장소(store)에 저장

- 이로인해 상태 로직이 중앙에서 관리되어 더 일관성 있고 예측 가능한 상태변경이 가능해짐

- 모든 상태변경 로직이 리듀서(reducers)에 의해 처리되기 때문에 디버깅과 테스팅이 용이함

 

3) 강력한 미들웨어와 개발도구

- 다양한 미들웨어를 지원하여 비동기 작업, 로깅, 상태 변경에 대한 추가 처리 등 복잡한 기능을 구현할 수 있음

- 또한 Redux DevTools같은 강력한 개발도구를 통해 상태 변화를 시각적으로 모니터링하고 이전 상태로 롤백하는 등의 기능을 제공

 

 

설치

yarn add redux react-redux

 

폴더구조 설정

src > redux > config, modules 두 폴더 생성

config > configStore.js 파일 생성 -> 중앙 저장소 store를 설정할 것

 

configStore.js

* reducer: state 관리를 위한 로직들, configStore에서 combineReducers로 모아둘 것

* createStore는 더이상 권장되지 않음 -> 추후 redux toolkit 다루면서 수정할 것

import { combineReducers, createStore } from "redux";

// 1) root reducer 만들기
const rootReducer = combineReducers({}); // 추후 modules에 들어갈 reducer들을 추가할 예정

// 2) store 생성
const store = createStore(rootReducer);

// 3) 만든 store를 내보내기
export default store;

 

main.jsx

- react-redux의 provider로 App을 감싼다

- Provider는 configStore에서 내보낸 store를 props로 받는다

- 그럼 App 의 모든 컴포넌트들은 그 저장소를 사용할 수 있다!

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>
);

 

모듈 만들기

- 예시로 counter.js 를 제작

- reducer라는 함수를 제작해야 함, 이 함수는 state를 반환

- reducer 함수는 두개의 인자를 매개변수로 받음 (state, action)

- state: state 초기값

- action: type이라는 Key를 가지는 객체 (지금은 default만 지정, 나중에 type에따라 분기)

const initialState = { number: 0 };

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

export default counter;

 

만들어준 리듀서를 configStore.js의 combineReducer에 전달해줌

const rootReducer = combineReducers({
  counter: counter,
});

// 단축 가능
const rootReducer = combineReducers({
  counter
});

key: value 형태로 import 한 리듀서를 넣어주면 되는데, key value가 같으니 단축해줌

 

컴포넌트에서 사용해보기

const App = () => {
  const counterReducer = useSelector(state => state.counter);
  console.log("state: ", counterReducer);
}

설정해둔 state를 확인할 수 있음

 

 

dispatch action 객체

- store의 변화는 dispatch()에 의해서만 발생하고, dispatch는 action 객체를 리듀서에 전달한다!

- 리듀서는 전달받은 action의 type에 따라 해당하는 처리를 진행하고, 그래서 store에 변화가 일어남

 

redux 흐름

  1. View 에서 액션이 일어난다.
  2. dispatch 에서 action이 일어나게 된다.
  3. action에 의한 reducer 함수가 실행되기 전에 middleware가 작동한다.
  4. middleware 에서 명령내린 일을 수행하고 난뒤, reducer 함수를 실행한다.
  5. reducer 의 실행결과 store에 새로운 값을 저장한다.
  6. store의 state에 subscribe 하고 있던 UI에 변경된 값을 준다.

 

state 수정 기능 만들어보기

state +1 기능을 만들어보자~

1) useDispatch()를 이용해 dispatch 객체를 만들어줌

2) onClick 이벤트로 dispatch를 실행하게 함

3) dispatch를 실행할 때 객체(action)에 type을 지정해 매개변수로 보내줌

4) reducer에 타입에 해당하는 동작을 설정해줌

import { useDispatch, useSelector } from "react-redux";
import counter from "./redux/modules/counter";

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

  return (
    <>
      <h1>{counterReducer.number}</h1>
      <button
        onClick={() => {
          dispatch({ type: "PLUS_ONE" });
        }}
      >
        +1
      </button>
      <button
        onClick={() => {
          dispatch({ type: "MINUS_ONE" });
        }}
      >
        -1
      </button>
    </>
  );
};

export default App;
const initialState = { number: 0 };

// reducer == 함수
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;

 

 

payLoad, ducks

action.type을 상수로 관리하기

- type은 지금 문자열로 쓰여있음

- 만약 이를 변경하고 싶다면 reducer의 switch문, 사용하고있는 컴포넌트들에서 하나하나 수정을 해줘야 함

- 따라서 별도의 상수로 관리해보자!

 

1) counter.js (reducer 모듈) 에서 필요한 상수와 그 상수를 반환하는 함수 만들기 + switch문에서도 문자열이 아닌 상수로 case 나누기

const PLUS_ONE = "PLUS_ONE";
export const plusOne = () => {
  return { type: PLUS_ONE };
};

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;
  }
};

 

2) app.jsx (리듀서를 사용하는 컴포넌트) 에서 객체와 그 안의 문자열을 직접 작성하는 것이 아니라 상수를 반환하는 함수를 사용

<button
  onClick={() => {
    dispatch(plusOne());
  }}
>
  +1
</button>
<button
  onClick={() => {
    dispatch(minusOne());
  }}
>
  -1
</button>

 

- 이렇게 action을 생성해주는 동작을 action creator라고 함

 

payload

- type과 함께 action 객체를 구성하는 요소

- 예를들어 위 예시들처럼 +1, -1이 아니라 +n, -n 을 사용자가 직접 지정할 수 있도록 한다면 이 때 리듀서로 전달되는 n을 payload라고 한다!

 

const initialState = { number: 0 };

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

const REMOVE_NUMBER = "REMOVE_NUMBER";
export const removeNumber = (payload) => {
  return { type: REMOVE_NUMBER, payload: payload };
};

// reducer == 함수
const counter = (state = initialState, action) => {
  switch (action.type) {
    case ADD_NUMBER:
      return { number: state.number + action.payload };
    case REMOVE_NUMBER:
      return { number: state.number - action.payload };
    default:
      return state;
  }
};

export default counter;
const App = () => {
  const [count, setCount] = useState(0);
  const counterReducer = useSelector((state) => state.counter);
  const dispatch = useDispatch();

  return (
    <>
      <h1>{counterReducer.number}</h1>
      <input
        type="number"
        value={count}
        onChange={(e) => {
          setCount(+e.target.value);
        }}
      ></input>
      <button
        onClick={() => {
          dispatch(addNumber(count));
        }}
      >
        더하기
      </button>
      <button
        onClick={() => {
          dispatch(removeNumber(count));
        }}
      >
        빼기
      </button>
    </>
  );
};

export default App;

 

 

Ducks

Ducks 패턴

- 리덕스 앱을 구성할 때 사용하는 방법론 중 하나

- 일반적으로 분산돼있던 액션 타입, 액션 생성자, 리듀서를 하나의 파일로 구성하는 방식

- Redux 관련 코드의 관리를 보다 간결하고 모듈화하여 관리하도록 도움

 

Ducks 패턴으로 작성하기

위 내용이 이미 Ducks 패턴으로 작성되고 있었다!

 

1) Reducer 함수를 export default

2) Action creator 함수들을 export

3) Action type은 app/reducer/ACTION_TYPE 형태로 작성