Redux

1Hoit·2023년 2월 27일
0

Redux?

  • Redux(리덕스)란 JavaScript(자바스트립트) 상태관리 라이브러리다.

왜 필요할까?

  • 자식 컴포넌트들 간의 다이렉트 데이터 전달은 불가능 하다.
  • 자식 컴포넌트들 간의 데이터를 주고 받을 때는 상태를 관리하는 부모 컴포넌트를 - 통해서 주고 받는다.
  • 그런데 자식이 많아진다면 상태 관리가 매우 복잡해진다.
  • 상태를 관리하는 상위 컴포넌트에서 계속 내려 받아야한다. => Props drilling 이슈

위와 같은 문제들을 해결하기 위해 상태 관리를 도와주는 도구들이 있다.

  • React Context
  • Redux
  • MobX
    이 중 가장 인기있는 상태 관리 라이브러리 리덕스를 알아보자.

Redux 기본 개념 : 3가지 원칙

1. Single source of truth
동일한 데이터는 항상 같은 곳에서 가지고 온다.
즉, 스토어라는 하나뿐인 데이터 공간이 있다는 의미이다.

2. State is read-only
리액트에서는 setState 메소드를 활용해야만 상태 변경이 가능하다.
리덕스에서도 Action(액션)이라는 객체를 통해서만 상태를 변경할 수 있다.

3. Changes are made with pure functions
변경은 순수함수로만 가능하다.
상태가 엉뚱한 값으로 변경되는 일이 없도록 순수함수로 작성되어야하는 Reducer와 연결되는 원칙이다.

Redux 구조

Redux에서는 Action → Dispatch → Reducer → Store 순서로 데이터가 단방향으로 흐른다

  1. Store (스토어)
  • Store(스토어)는 상태가 관리되는 오직 하나의 공간이다.
  • 컴포넌트와는 별개로 스토어라는 공간이 있어서 그 스토어 안에 앱에서 필요한 상태를 담는다.
  • 컴포넌트에서 상태 정보가 필요할 때 스토어에 접근한다.
import { createStore } from 'redux';
const store = createStore(rootReducer); // 리듀서가 연결 된 스토어 생성
  1. Action (액션)
  • 어떤 액션을 취할 것인지 정의해 놓은 자바스크립트 객체로 dispatch() 메서드의 인자로 간다.
  • Action(액션)은 앱에서 스토어에 운반할 데이터를 말한다.
  • type은 필수이며 대문자와 Snake Case로 작성
    // payload가 필요 없는 경우
    { type: 'INCREASE' }
    // payload가 필요한 경우
    {
    type: 'ACTION_CHANGE_USER', // 필수
    payload: { // 옵션
      name: 'wonho',
      age: 30
    }
    }
  • 보통 Action을 직접 작성하기보다는 Action 객체를 생성하는 함수를 만들어 사용한다. (액션 생성자(Action Creator))
// payload가 필요 없는 경우
const increase = () => {
  return {
    type: 'INCREASE'
  }
}

// payload가 필요한 경우
const setNumber = (num) => {
  return {
    type: 'SET_NUMBER',
    payload: num
  }
}
  1. Reducer (리듀서)
  • Action(액션)을 Store(스토어)에 바로 전달하는 것이 아니라
    Action(액션)을 Reducer(리듀서)에 전달해야한다.
  • 즉, Reducer는 Dispatch에게서 전달받은 Action 객체의 type 값에 따라서 Store의 상태를 변경시키는 함수이다.
  • Action(액션)을 Reducer(리듀서)에 전달하기 위해서는 dispatch() 메서드를 사용해야한다.
  • Action(액션) 객체가 dispatch() 메서드에 전달된다.
  • dispatch(액션)를 통해 Reducer를 호출한다.
  • Reducer는 새로운 Store 를 생성한다.
  • 외부 요인으로 인해 기대한 값이 아닌 엉뚱한 값으로 상태가 변경되는 일이 없어야하기 때문에 순수함수여야 한다.
const count = 1

// Reducer를 생성할 때에는 초기 상태를 인자로 요구합니다.
const counterReducer = (state = count, action) => {

  // Action 객체의 type 값에 따라 분기하는 switch 조건문입니다.
  switch (action.type) {

    //action === 'INCREASE'일 경우
    case 'INCREASE':
			return state + 1

    // action === 'DECREASE'일 경우
    case 'DECREASE':
			return state - 1

    // action === 'SET_NUMBER'일 경우
    case 'SET_NUMBER':
			return action.payload

    // 해당 되는 경우가 없을 땐 기존 상태를 그대로 리턴
    default:
      return state;
	}
}
  • 여러 개의 리듀서를 사용할 경우
import { combineReducers } from 'redux';

const rootReducer = combineReducers({
  counterReducer,
  anyReducer,
  ...
});
  1. Dispatch
  • Reducer로 Action을 전달해주는 함수
  • Dispatch의 전달인자로 Action 객체가 전달된다.
  • Action 객체를 전달받은 Dispatch 함수는 Reducer를 호출
// Action 객체를 직접 작성하는 경우
dispatch( { type: 'INCREASE' } );
dispatch( { type: 'SET_NUMBER', payload: 5 } );

// 액션 생성자(Action Creator)를 사용하는 경우
dispatch( increase() );
dispatch( setNumber(5) );

위와 같이 Redux의 기본 개념을 알아보았다.

Redux Hooks

React-Redux에서 Redux를 사용할 때 활용할 수 있는 Hooks 메서드를 제공

  1. Provider 컴포넌트
  • Provider는 store를 손쉽게 사용할 수 있게 하는 컴포넌트 이며
    해당 컴포넌트를 불러온다음에, Store를 사용할 컴포넌트를 감싸준 후
    Provider 컴포넌트의 props로 store를 설정해주면 된다.
  • 보통 최상위에서 설정한다.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store/store'; // 스토어 불러오기
import { Provider } from 'react-redux'; // store 사용설정을 위해 불러오기
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
  1. useDispatch()
    useDispatch() 는 Action 객체를 Reducer로 전달해 주는 Dispatch 함수를 반환하는 메서드
    위에서 Dispatch를 설명할 때 사용한 dispatch 함수도 useDispatch()를 사용해서 만든 것이다.
import { useDispatch } from 'react-redux'

const dispatch = useDispatch()
dispatch( increase() )
console.log(counter) // 2

dispatch( setNumber(5) )
console.log(counter) // 5
  1. useSelector()
    useSelector()는 컴포넌트와 state를 연결하여 Redux의 state에 접근할 수 있게 해주는 메서드.
// Redux Hooks 메서드는 'redux'가 아니라 'react-redux'에서 불러옴
import { useSelector } from 'react-redux'
const counter = useSelector(state => state)
console.log(counter) // 1

Redux의 장점

  • 상태를 예측 가능하게 만든다. (순수함수를 사용하기 때문)
  • 유지보수에 용이 (복잡한 상태 관리와 비교)
  • 디버깅에 유리 (action과 state log 기록 시) → redux dev tool (크롬 확장)
  • 테스트를 붙이기 용이 (순수함수를 사용하기 때문)

리액트에서 리덕스를 이용한 상태 관리하기

들어가기전 오해가 생길법한 내용을 알아보겠다.
Redux는 React의 관련 라이브러리, 혹은 하위 라이브러리라는 대표적인 오해가 있는데, 전혀 그렇지 않다.
Redux는 React 없이도 사용할 수 있는 상태 관리 라이브러리인 것이다.

  • create-react-app 으로 만든 리액트 프로젝트에서 리덕스를 사용할 경우에는
    npm install redux react-redux 두 모듈을 다운 받는다.
    package.json 을 보면 프로젝트가 두 모듈에 의존하는 것을 확인 할 수 있다.

  • Code Splitting(코드 분할)
    코드 분할은 스크립트를 하나의 큰 파일로 로드하는 대신 더 작은 부분으로 나누고 해당 페이지에 필요한 것만 로드하는 기술이다.
    많은 양의 JavaScript가 있는 프로젝트의 경우 성능이 크게 향상될 수 있다.

    • src 폴더 안에 actions, reducers, store, components, pages 폴더로 분할한다.
    • component 폴더는 프리젠테이션 컴포넌트로, page 폴더는 컨테이너 컴포넌트로 나눈다.
    • Store는 하나의 리듀서만 관리할 수 있지만 리듀서를 여러개 나눠서 하나로 합칠 수 있다.

reducer폴더의 reducers.js

// reducers 폴더 안에 reducers.js 
import { combineReducers } from 'redux'; // 모든 리듀서 합치기 위함
import itemReducer from './itemReducer';
import notificationReducer from './notificationReducer';
const rootReducer = combineReducers({ // 모든 리듀서 합치기
  itemReducer,
  notificationReducer
});
export default rootReducer;

reducer폴더의 itemReducer.js

이곳에서 각각의 리듀서의 코드를 구현하면 된다.
예를들어 하나만 보자면 아래와 같다.

// reducers 폴더 안에 itemReducer.js 
import { REMOVE_FROM_CART, ADD_TO_CART, SET_QUANTITY } from '../actions/index';
import { initialState } from './initialState';

const itemReducer = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TO_CART:
      //TODO
      return Object.assign({}, state, {
        cartItems: [...state.cartItems, action.payload],
      });

    case REMOVE_FROM_CART:
      //TODO
      let currentItem = state.cartItems.filter((el) => el.itemId !== action.payload.itemId);
      return Object.assign({}, state, {
        cartItems: currentItem,
      });

    case SET_QUANTITY:
      let idx = state.cartItems.findIndex((el) => el.itemId === action.payload.itemId);
      //TODO
      return {
        ...state,
        cartItems: [...state.cartItems.slice(0, idx), action.payload, ...state.cartItems.slice(idx + 1)],
      };

    default:
      return state;
  }
};

export default itemReducer;

이처럼 Reducer에서 순수함수로 상태 업데이트를 해야한다.
방법으로는

  • Object.assign()을 이용 (depth 1 까지 깊은 복사)
//단 상태 state가 객체일 경우
return Object.assign( {}, state, {새로 업데이트 할 것} );

참고 : Object.assign() – JavaScript | MDN

  • Spread 펼쳐 문법을 이용
return { ...state, {새로 업데이트 할 것} };

// state 상태가 객체 형태라면 리듀서에서 특정 액션에서 리턴할 때
return Object.assign( {}, state, {새로 업데이트 할 것} );
👉 Spread 펼쳐 문법을 이용한다. [ ...state, {} ]
// state 상태가 객체 형태라면 리듀서에서 특정 액션에서 리턴할 때
return { ...state, {새로 업데이트 할 것} };

store.js

// store 폴더 안에 store.js => 스토어를 생성한 후 리듀서를 등록한다.
import { createStore } from "redux";
import rootReducer from '../reducers/index'; // 합친 리듀서 불러오기
const store = createStore(rootReducer); // 스토어 생성
export default store;

최상위 index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store/store'; // 스토어 불러오기
import { Provider } from 'react-redux'; // store 사용설정을 위해 불러오기
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
profile
프론트엔드 개발자를 꿈꾸는 원호잇!

0개의 댓글