리덕스에 대해 알아보자!

이상진·2023년 5월 26일
9

React

목록 보기
1/6
post-thumbnail

리액트를 다루는 기술

이번 글은 리액트를 다루는 기술 chapter 16 ~ 17을 기반으로 작성되었다.

리덕스란?

리덕스는 오픈 소스 자바스크립트 라이브러리의 일종으로, state를 이용해 웹 사이트 혹은 애플리케이션의 상태 관리를 해줄 목적으로 사용한다.

리덕스는 리액트에 종속되는 라이브러리가 아니다.(바닐라 자바스크립트, Vue, angular-redux, ember 리덕스에서도 사용)

리덕스의 장점

  1. 예측 가능한 상태 관리
  2. 컴포넌트 간 데이터 흐름 관리
  3. 시간 여행 및 디버깅
  4. 서버 사이드 렌더링 지원
  5. 생태계와 커뮤니티

리덕스의 단점

  1. 복잡성
  2. 많은 코드량
  3. 약간의 성능 영향
  4. 중앙 집중화된 상태 관리의 한계

리덕스 개념 정리

액션
상태에 어떠한 변화가 필요하다면 액션이 발생한다.

액션은 객체 형태이고 type 값을 무조건 가지고 있어야한다.

{
  type: 'ADD_TODO',
  data: {
    id: 1,
    text: '리덕스 키우기'
  }
}

{
  type: 'CHANGE_INPUT',
  text: '안녕하세요'
}

액션 생성 함수
액션 생성 함수는 객체를 만들어 주는 함수이다.

어떤 변화를 일으켜야 할 때마다 액션 객체를 만들어야 하는데 매번 직접 작성하면 번고롭고, 실수가 생길 수 있기 때문에 함수를 만들어서 관리한다.

const addTodo = (data) => {
	return {
    	type: 'ADD_TODO',
        data
    }
}

리듀서
리듀서는 변화를 일으키는 함수이다.

액션을 만들어서 발생시키면 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아온다. 그리고 두 값을 참고하여 새로운 상태를 만들어 변환해 준다.

cosnt initialState = {
 	counter: 1 
};
function reducer(state = initialState, action){
 	switch(action.type){
      case INCREMENT:
        return {
        	counter: state.counter + 1 
        };
      default:
        return state;
    }
}

스토어
한 개의 프로젝트는 단 하나의 스토어만 가질 수 있다.

스토어 안에는 현재 에플리케이션의 상태와 리듀서가 들어 있으며, 그 외에도 몇 가지 중요한 내장 함수를 지닌다.

디스패치
디스패치는 스토어의 내장 함수 중 하나이고, 액션을 발생시키는 것이다.

이 함수는 액션 객체를 파라미터로 넣어서 호출한다.
디스패치가 호출되면 스토어는 리듀서 함수를 실행시켜서 새로운 상태를 만들어 준다.

구독
구독도 스토어의 내장 함수 중 하나이다.

subscribe 함수 안에 리스너 함수를 파라미터로 넣어서 호출해주면, 이 리스너 함수의 액션이 디스페치가되어 상태가 업데이트 될 때마다 호출된다.

const listener = () => {
  console.log('상태가 업데이트됨');
}
const unsubscribe = store.subscribe(listener);

unsubscribe(); // 추후 구독을 비활성화할 때 함수를 호출

리덕스의 세 가지 규칙

구독
하나의 에플리케이션 안에는 하나의 스토어가 들어 있다.

빈번한 업데이트와 특정 부분을 분리 시킬 때 여러 개의 스토어를 만들 수 있지만 복잡하다.

읽기 전용 상태
setState를 사용하여 state를 업데이트할 때도 객체나 배열을 업데이트하는 과정에서 불변성을 지켜 주기 위해 spread 연산자를 사용한다. (새로운 객체 생성)

리듀서는 순수한 함수
리듀서 함수는 이전 상태와 액션 객체를 파라미터로 받는다.
파라미터 외의 값에 의존하면 안 된다.
이전 상태는 건드리지 않고, 변화를 줄 새로운 객체를 생성한다.
같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과 값을 반환한다.

리덕스 활용하기

가장 간단한 카운터 예제를 통해 리덕스의 기능에 대해 학습해 볼 것이다.

Ducks 패턴을 이용하였다.
(액션 타입, 액션 생성 함수, 리듀서 함수를 기능별로 하나의 파일에 몰아서 작성하는 방식)

//액션 타입 정의
const INCREASE = 'counter/INCREASE'; 
const DECREASE = 'counter/DECREASE';
 // 액션 생성 함수
export const increase = () => ({type:INCREASE})
export const decrease = () => ({type:DECREASE})
// 초기 상태 및 리듀서 생성함수 만들기 
const initialState = {
  number: 0
}

export default function counter(state = initialState, action){
  switch(action.type){
    case INCREASE:
      return {
        number: state.number + 1
      }
    case DECREASE:
      return{
        number: state.number - 1
      }
    default:
      return state;
  }
}

이 모듈의 초기상태는 number 값으로 설정하였다.
(초기 상태 및 리듀서 생성함수) modules 디렉터리의 counter.js에서 이루어진다.

💡 Tip
export default는 한 개를 내보낸다.
export는 여러개를 내보낸다.

//루트 리듀서 생성
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';

const rootReducer = combineReducers({
  counter,
});

export default rootReducer;

(루트 리듀서 생성) modules 디렉터리의 index.js 파일에서 이루어진다.

//스토어 만들기
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createStore } from 'redux';
import './index.css';
import App from './App';
import rootReducer from './modules';

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);
//Provider 컴포넌트를 사용하여 프로젝트에 리덕스 적용하기
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import rootReducer from './modules';
import reportWebVitals from './reportWebVitals';

const store = createStore(rootReducer, composeWithDevTools());

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

(스토어 만들기) 리덕스를 적용할때 사용한다. src 디렉터리의 index.js에서 이루어진다.

👀 더 알아보기
리덕스를 사용할때 기존의 상태를 보존하기 위해 spread(...) 연산자를 사용한다.

function todos(state = initialState, action){
  switch(action.type){
    case CHANGE_INPUT:
      return{
        ...state,
        input: action.input
      };
    case INSERT:
      return{
        ...state,
        todos: state.todos.concat(action.todo)
      };
    case TOGGLE:
      return{
        ...state,
        todos: state.todos.map(todo => 
          todo.id === action.id ? {...todo, done: !todo.done} : todo  
        )
      };
    case REMOVE:
      return{
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.id)
      }
    default:
      return state;
  }
}

Redux-action

react-action 을 사용하면 액션 생성 함수를 더 짧은 코드로 작성할 수 있다. 또한
리듀서를 작성할 때도 switch/case 문이 아닌 handleActions라는 함수를 사용하여 각 액션마다 업데이트 함수를 설정하는 형식으로 작성해 줄 수 있다.

💲yarn add redux-actions

예제 코드에 적용하기

//createAction 함수 사용

import { createAction } from 'redux-actions';

const INCREASE = 'counter/INCREASE'; 
const DECREASE = 'counter/DECREASE';

export const increase = () => (INCREASE) 
export const decrease = () => (DECREASE)

createAction을 사용하면 매번 객체를 직접 만들어 줄 필요가 없어진다. (효율적)


const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

export const increase = () => (INCREASE)
export const decrease = () => (DECREASE)

const initialState = {
  number: 0
}

export default function handleActions(
  {
    [INCREASE]: (state, action) => ({ number: state.number + 1 });
	[DECREASE]: (state, action) => ({ number: state.number - 1 });
  }
  initialState,
);

Immer

리덕스에서 상태를 업데이트할 때는 불변성을 지켜야 했었다. (spread 연산자, 배열 내장함수)
그러나 모듈의 상태가 복잡해질수록 불변성을 지키기 까다로워지기 때문에 immer를 사용한다. (컴포넌트에 리액트 연동이 편리)

//사용 예시
const deepObj = {
  modal: {
    open: false,
    content: {
      title: '알림',
      body: '성공적으로 처리되었습니다.',
      buttons: {
       	confirm: '확인',
        cancel: '취소'
      },
    },
    waiting: false,
    setting: {
      theme: 'dark',
      zoomLevel: 5,
    }
  },
}

Hooks를 사용하여 컨테이너 컴포넌트 만들기

useSelector

useSelector을 사용하면 컴포넌트에서 Redux 상태 값을 구독할 수 있으며, 상태 값의 변경에 따라 컴포넌트를 업데이트할 수 있다.

const 결과 = useSelector(상태 선택 함수);
import React from 'react';
import { useSelector } from 'react-redux';

const Counter = () => {
  const count = useSelector(state => state.counter); // 상태 선택자를 사용하여 counter 상태 값 추출

  return (
    <div>
      <h1>Counter: {count}</h1>
    </div>
  );
};

export default Counter;

useDispatch

useDispatch는 컴포넌트 내부에서 스토어의 내장 함수 dispatch를 사용할 수 있게 해 준다.

const dispatch = useDispatch();
dispatch({ type: 'SAMPLE_ACTION' });
import React from 'react';
import { useDispatch } from 'react-redux';
import { increment, decrement } from './actions';

const CounterButtons = () => {
  const dispatch = useDispatch();

  const handleIncrement = () => {
    dispatch(increment()); // increment 액션 디스패치
  };

  const handleDecrement = () => {
    dispatch(decrement()); // decrement 액션 디스패치
  };

  return (
    <div>
      <button onClick={handleIncrement}>Increment</button>
      <button onClick={handleDecrement}>Decrement</button>
    </div>
  );
};

export default CounterButtons;

useStore

useStore를 사용하면 컴포넌트 내부에서 리덕스 스토어 객체를 직접 사용할 수 있게된다.

import React from 'react';
import { useStore } from 'react-redux';

const App = () => {
  const store = useStore();
  const state = store.getState(); // 현재 Redux 스토어의 상태 값 가져오기

  return (
    <div>
      <h1>Counter: {state.counter}</h1>
    </div>
  );
};

export default App;

글을 마치며

나는 2023년 3월 중순쯤에 리덕스 툴킷에 대한 공부를 아주 살짝 해봤었다.
그때는 아무런 생각없이 사용했던거라서 useSelector이 뭔지 Provider이 뭔지 아무것도 모른채 사용했었는데 이번 기회를 통해서 리덕스의 개념을 이해하게되어 각 기능들의 역할을 알 수 있게 되어서 좋았다. 아직 부족하지만 계속 리덕스를 사용해보면서 완벽하게 알게될때까지 공부를 해야겠다고 생각했다.

profile
프론트엔드 공부중

3개의 댓글

comment-user-thumbnail
2023년 6월 1일

신고합니다

1개의 답글