글쓰는 개발자

[React/react-native] Redux 쉽게 사용하기 (1/3) 본문

Development/React

[React/react-native] Redux 쉽게 사용하기 (1/3)

세가사 2024. 5. 22. 19:37
반응형

React의 상태관리는 useState를 사용해 이루어 진다.

import { useState } from 'react';


function Home() {

	const [text, setText] = useState('hello');
	return (<div>{text}</div>)
}

하지만 useState를 사용한 상태값의 경우 해당 페이지를 상속하는 페이지까지만 해당 변수를 공유할수 있으며 모든걸 컴포넌트화 시켜서 관리하는 React의 특성상 이러한 변수를 여러개 만들어 아래로 계속해서 변수를 넘기게 되면 개발 복잡도가 기하급수적으로 늘어날 수 밖에 없다.

 

따라서 페이지 최상단에 몇개의 변수를 등록해 두고 이를 사용하는 곳에서 값이 변경될 때마다 페이지를 리로딩 할수 있도록 redux등의 상태 관리 방식을 개발했다.

 

유일한 상태공유 방식이라 한동안 redux를 많이 사용해왔지만 사용의 복잡성 때문에 recoil, zustand, jotai등의 더욱 심플한 상태관리 라이브러리가 개발되었고 무엇을 사용할지는 개발자의 취향문제가 되었다.

 

필자또한 redux의 복잡성 때문에 다른 상태관리 방식을 원했으나 필자가 react를 배운 초창기에는 다른 좋은 대안이 존재하지 않았고 현재에 와서도 커뮤니티의 크기때문에 redux를 쉽게 포기하기 힘들다.

 

이에 redux를 좀더 쉽게 사용할수 있는 자체 라이브러리를 개발해 꾸준히 사용하고있고 이를 활용해 간단히 redux를 사용하는 방법을 공유하고자 한다.

 

1. redux 의 기본사용방법

먼저 redux의 기본형부터 다루어 보자.

 

redux를 사용하려면 react-redux 라이브러리를 설치해야 한다. store를 만들기 위한 @reduxjs/toolkit도 설치해주자.

npm install --save react-redux @reduxjs/toolkit

 

App.js에 Provider를 생성해 내가 사용할 하나의 store를 등록한다.

index.js

import React from 'react';
import { combineReducers } from 'redux';
import { Provider } from 'react-redux'
import { configureStore } from '@reduxjs/toolkit'
import counter from './counter';
import App from './App';

const rootReducer = combineReducers({
  counter
});

const store = configureStore({reducer: rootReducer});

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
);

export default App;

counter.js

export const INCRESE = "COUNT/INCRESE";

export const increseCount = count => ({ type: INCRESE, count });

const initalState = {
  count: 0
};

const counter = (state = initalState, action) => {
  switch (action.type) {
    case INCRESE:
      return {
        ...state,
        count: action.count
      };

    default:
      return state;
  }
};

export default counter;

App.js

import { useSelector, useDispatch } from 'react-redux';
import { increseCount } from './count';

const App = () => {
  // dispatch를 사용하기 위한 준비
  const dispatch = useDispatch();

  // store에 접근하여 state 가져오기
  const { count } = useSelector(state => state.counter);

  const increse = () => {
    // store에 있는 state 바꾸는 함수 실행
    dispatch(increseCount(count+1));
  };

  return (
    <div>
      {count}
      <button onClick={increse}>Add</button>
    </div>
  );
};

export default App;

 

redux에는 Provider 내에 존재하는 모든 컴포넌트가 접근 가능한 저장소를 가지고 해당 저장소의 상태값을 변경하기 위해 dispatch 함수를 상태값에 접근하기 위해서는 useSelector 함수를 각각 사용한다.

 

dispatch의 사용법을 보면 increseCount 함수를 내부에서 호출하고 increaseCount 함수는 최종적으로 다음으로 이루어진다.

{ type: INCRESE, count }

여기서 INCREASE는 "COUNT/INCRESE" 라는 값을 담은 변수이다. 즉 어떤 변수를 수정할것인가에 대한 정보를 담은 불변의 타입명과 값을 변경하는데 필요한 변수가 들어간다.

 

해당 타입과 변수가 포함된 key value 오브젝트가 dispatch에 의해 호출되면 등록된 reducer에서 이를 처리한다.

const counter = (state = initalState, action) => {
  switch (action.type) {
    case INCRESE:
      return {
        ...state,
        count: action.count
      };

    default:
      return state;
  }
};

 

 

위 리듀서는 combineReducers 함수를 사용할때 counter라는 이름으로 등록이 되고 내부에 initialState라는 변수들을 기본값으로 가진 state 저장고가 된다.

 

combineReducers로는 이름으로 나뉘어진 여러개의 reducer를 한번에 등록해서 사용한다. 따라서 A 리듀서 B 리듀서 등등 여러개의 리듀서를 등록해두면 한번 dispatch를 호출할때 각각의 reducer가 순차적으로 로딩되는데 어떠한 리듀서에 상태값을 변경할지를 구분해야 하기 때문에 type값을 가지고 이를 구분한다.

 

dispatch로 넘겨진 값들은 reducer에 두번째 파라미터인 action에 파라미터로 들어가고 이중 type을 읽어서 return 하는 값으로 state값을 변경한다.

 

위에 ...state의 경우 기존의 변수를 모두 그대로 유지하면서 아래 count 값만 들어온 action 파라미너에 넘어온 count 값으로 대체하겠다는 의미다.

 

등록된 스토어의 상태값은 useSelector에 combineReducer에 등록된 이름의 state를 호출함으로써 어떤 컴포넌트에서도 호출할수 있다.

 

2. redux의 문제점

척봐도 구조가 복잡하고 이해하기 어렵다. 하지만 한번 이해하면 사용하지 못할만큼은 아니다. 문제는 reducer와 reducer에 담기는 상태값이 늘어나면서 발생한다.

 

만약 counter reducer에 카운터 값뿐만이 아니라 사용자명, timer 등의 변수가 등록된다면 다음 코드들이 reducer에 추가되어야 한다.

export const INCRESE = "COUNT/INCRESE";
export const USERNAME = "COUNT/USERNAME";
export const TIMER = "COUNT/TIMER";

export const increseCount = count => ({ type: INCRESE, count });

const initalState = {
  count: 0,
  username: '미등록',
  timer: '00:00:00'
};

const counter = (state = initalState, action) => {
  switch (action.type) {
    case INCRESE:
      return {
        ...state,
        count: action.count
      };
    case USERNAME:
      return {
        ...state,
        count: action.username
      };
    case TIMER:
      return {
        ...state,
        count: action.timer
      };

    default:
      return state;
  }
};

export default counter;

 

새로운 변수를 등록하기 위해서는 초기화 값(intialState)과 type 그리고 이를 구분해서 값을 세팅해줄 switch문도 추가해야 한다.

 

redux에는 이를 연계해서 사용할 redux-thunk나 redux-saga등의 미들웨어도 존재하는데 위 코드와 함께 이들을 관리하고 reducer와 변수가 늘어날때마다 redux의 코드 길이와 함께 복잡도가 기하급수적으로 늘어나 휴면에러가 항상 발생한다.

 

redux-saga또한 이러한 등록방식을 사용하기 때문에 하나의 변수나 함수를 추가할때 등록해야할 일부 코드를 빼먹어서 이를 찾는데 사용된 디버깅 시간이 수시간에 이른다. 

 

하지만 자세히 보면 자동으로 처리가능한 몇가지 규칙이 보인다. 이를 사용해 redux를 zustand 급으로 편하게 사용할수 있게 라이브러리화 해서 사용하자. 자세한 내용은 다음포 스팅에서 다루겠다.

반응형