본문 바로가기
JavaScript/React

리덕스(Redux)의 이해 및 활용해 보기

by junvely 2023. 4. 19.

리덕스(Redux)

 

1. 리덕스란 무엇인가? = 전역 상태관리 패키지 라이브러리

'중앙 State 관리소'를 사용할 수 있게 도와주는 라이브러리다.

- 리덕스가 필요한 이유 

1) useState의 불편함 = props 드릴링 : 부모 -> 자식 컴포넌트로 props를 계속해서 내려주면, 불필요한 컴포넌트에도 전달되어야 하고, 부모 컴포넌트가 리렌더링 시 자식 컴포넌트도 리렌더링 된다. 어디서 props을 내려주는지, 디버깅이 힘들다 등의 단점이 있다.

2) useContext 등 context API의 위험성 : 전역 State의 provider가 한개라도 변경되면, props을 받는 모든 하위 컴포넌트 전체가 리렌더링 된다. => context API vs 리덕스 공부

3) 따라서 Global state와 Local state의 개념으로 나누어 관리해야 한다.

- Global state : '중앙 state관리소' = 전역상태, 어디선든 접근 가능, 컴포넌트 외부 특별한 곳에서 전역상태를 관리

- Local state : 컴포넌트 내에서 관리

 

 

2. 리덕스 설정방법

1. 설치

$ yarn add redux react-redux

2. 폴더 구조 src 내부에 redux 폴더 생성

config => 리덕스 설정 관련 파일 전부

configStore.js => '중앙 state 관리소'라는 뜻, 설정 관련 코드들 전부(.js)

modules : state의 그룹, ex) todo-list => todos.js 에서 사용하는 모든 state들이 들어있을 

3. configStore.js에서 store 설정

import { createStore } from "redux"; // store를 만드는 api
import { combineReducers } from "redux"; // reducers를 하나로 묶는 api

const rootReducer = combineReducers({}); //reducers는 묶어서 하나로 만든 reducer => modules를 모아놓음
const store = createStore(rootReducer);

export default store;

modules의 state들을 전부 묶어서 하나로 만들어 store를 생성해 외부에서 사용가능 하도록 export 한다.

4. counter.js에서 사용할 state를 생성해 준다.

// 초기 상태값(state) => const [sate, setState] = useState(0)
const initialState = {
  number: 0,
};

// counter 리듀서 : 'state에 변화를 일으키는' 함수
// =>  state를 action의 type에 따라 변경하는 함수

// input : state와 action(type,value = state를 어떻게 사용할건지 action을 정함)
const counter = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state; //변경 사항 state가 있으면 변경된 값 return
  }
};

export default counter;

5. configStore.js에서 combineReducer에 counter를 추가해준다.

const rootReducer = combineReducers({
  //reducers는 묶어서 하나로 만든 reducer => modules를 모아놓음
  counter,
});

 

 

3. App에서 state 가져오기

1. index.js에서 provider로 감싸주면 redux 사용가능 범위로 지정됨, store 내려주기

import { Provider } from "react-redux";
import store from "redux/config/configStore";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  // store로 만든 중앙관리소를 사용가능하게 한다.
  <Provider store={store}>
    <App />
  </Provider>
);

2.  useSeletor => store에 접근해 특정 state를 return하여 사용한다.

1) 여러개의  state 생성 

2) reducer에 추가한 state 포함해 주기

const rootReducer = combineReducers({
  //reducers는 묶어서 하나로 만든 reducer => modules를 모아놓음
  counter,
  users,
});

3) App에서 가져오기

import "App.css";
import { useSelector } from "react-redux";

function App() {
  // 여기에서 store에 접근하여. counter sate의 값을 읽어오고 싶다
  // useSelector =>
  const data = useSelector((state) => {
    // store의 state 전부를 객체로 받아옴
    return state;
  });
  console.log(data.users);
  return <div>{data.users.userId}</div>;
}

export default App;

data에 모든 state들이 들어오는 것을 확인할 수 있다.

 

 

 

4. state 변경하기

- useDispatch() : store에 action 객체를 전달한다.

event 발생 => dispatch실행(action전달) => store에 action 전달

 

1) state의 type 만들기

=> dispatch에게 action객체를 전달 받으면, reducer는 action이 있는 state를 찾아 => action.type에 따라 결과를 return하도록 한다. => 이 때 *action의 이름이 같은 state가 여러개 존재한다면, 해당하는 action을 모두 실행한다. 

counter state 에서는 초기값(현재 객체상태)과 action을 받아 state값을 action.type에 맞게 새로 return한다. 

새로 return할 state는 초기값이 객체였기 때문에 다시 객체형식으로 반환되는 값을 return한다.

const counter = (state = initialState, action) => {
  switch (action.type) {
    case "PLUS_ONE": //type 생성
      return {
        number: state.number + 1, //state를 객체로 받기 때문에 다시 객체를 return 
      };
    default:
      return state;
  }
};

 

2) dispatch생성 후, 호출하면서 action{ type, payload }을 store에 전달한다.

- 실행시킬 action을 type으로 전달하여 state가 어떤 action을 할지 지정해 준다.

  // dispatch를 가져와 보자
  const dispatch = useDispatch();
  
  <button
        onClick={() => {
          // dispatch에 action을 보내 => store에 전달해주기
          dispatch({
            type: "PLUS_ONE",
          });
        }}
      >
        +
      </button>

 

 

 

5. action 리팩토링

1. action.type에서 발생하는 문제

dispatch에게 action객체를 전달받으면, reducer는 action.type이 존재하는 state를 찾아 =>action.type에 따라 결과를 return하도록 한다. => 이 때 *action의 이름이 같은 state가 여러개 존재한다면, 해당하는 action을 모두 실행한다.

 user.js의 State와 counter.js의 State에 "PLUS_ONE"이라는 action.type이 모두 존재하여 클릭 시 둘 다 증가하는 것을 볼 수 있다.

=> 따라서 좀 더 세분화된 네이밍을 하여 조작이 가능하도록 리팩토링이 필요하다.

 

- 리팩토링 1 . module/type명으로 컨벤션 규칙을 정한다고 가정했을 때,

const users = (state = initialState, action) => {
  switch (action.type) {
    case "counter/PLUS_ONE":
      return {
        userId: state.userId + 1,
      };
	dispatch({
            type: "counter/PLUS_ONE",
          });

이와 같이 1. module명/action.type명을 지정하면, 중복되어 실행되는 문제를 줄일 수 있다. 하지만 ❕action.type과 사용부 모두 이름을 수정해 주어야 하는 번거로움 +  'PLUS_ONE'은 오타 등 휴먼 에러를 발생시킬 수 있기 때문에 2. 상수 처리하여 export해서 외부에서 import로 가져올 수 있도록 한다.= action.type에서만 수정하면 됨

// counter.js 모듈
export const PLUS_ONE = "counter/PLUS_ONE";

switch (action.type) {
    case PLUS_ONE:
      return {
        number: state.number + 1,
      };
// import하여 사용
import { PLUS_ONE,, MINUS_ONE } from "redux/modules/counter";

 dispatch({
            type: PLUS_ONE,
          });

 

- 리팩토링 2 . action객체 또한 하드코딩으로 작성되는 부분이기 때문에 휴먼에러 발생할 수 있어 action creator 함수를 만들어 사용한다.

	dispatch({
            type: PLUS_ONE,
          });

1. counter.js 모듈에서 type : PLUS_ONE인 action객체를 return하는 action creator 함수 생성 후 export 

const PLUS_ONE = "counter/PLUS_ONE";
const MINUS_ONE = "counter/MINUS_ONE";

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

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

2. 사용부에서 더 이상 PLUS_ONE 변수를 import할 필요 없이, plusOne 함수를 import해 바로 사용 가능

import { plusOne, minusOne } from "redux/modules/counter";

   onClick={() => {
          dispatch(plusOne());
        }}

 

 

 

6. action payload

n만큼 state에 더해주기 => 변경할 값을 payload로 action객체에 전달해야 한다.

payload값을 받아 action객체를 return하는 action creator 함수를 생성, export

const PLUS_N = "counter/PAYLOAD";

// 사용부에서 action객체를 return하여 counter에 보내기 위한 함수
export const plusN = (payload) => {
  return {
    type: PLUS_N,
    payload,
  };
};

case PLUS_N:
      return {
        number: state.number + action.payload,
      };
import { plusN, minusN } from "redux/modules/counter";

          dispatch(plusN(number));

+팁 : 객체 구조분해 할당 + 형 변환(간단하게 문자에 +붙이면 숫자로 변환됨)

onChange={(e) => {
          const {value} = e.target.value
          setNumber(+value)}}

 

 

7. dux pattern 

개발자들 간의 리덕스 사용 pattern으로 규칙을 정해 사용을 권장한다.

Erik Rasmussen 이 제안한 Ducks 패턴은 아래의 내용을 지켜 모듈을 작성하는 것 이다.

Reducer 함수를 export default 한다.
Action creator 함수들을 export 한다.
Action type은 app/reducer/ACTION_TYPE 형태로 작성한다.
(외부 라이브러리로서 사용될 경우 또는 외부 라이브러리가 필요로 할 경우에는 UPPER_SNAKE_CASE 로만 작성해도 괜찮다.)

그래서 모듈 파일 1개에 Action Type, Action Creator, Reducer 가 모두 존재하는 작성방식입니다.

 

 

본 내용은 스파르타 리액트 최원장님 강의를 참고하였습니다.