[React] - Redux

Lee Jeong Min·2022년 1월 5일
0
post-thumbnail

React Redux

상태 관리의 필요성

JS 앱의 상태 관리가 복잡해진만큼 버그를 최소화 하고, 잘 만들어진 사용자 경험(UX)을 제공하는 데 있어 상태를 효과적으로 관리할 필요성이 생겨났다.
여기서 말하는 상태란? 서버 응답, 캐시 데이터, 로컬 상태(아직 서버에 저장되지 않은 데이터) 등을 의미한다. 뿐만 아니라 활성화 된 라우트, 선택된 탭 핸들, 로딩 표시 여부, 페이지네이션 컨트롤 등 다양한 UI View 상태도 해당된다.이러한 복잡성은 오늘 날 앱 제작에 있어 가장 어려운 부분 중 하나이다.

참고 사이트: https://redux.js.org/understanding/thinking-in-redux/motivation

Redux의 특징

  • 예측 가능
  • 중앙 관리
  • 디버깅 용이
  • 유연한 확장성

Redux는 언제 사용?

  • 지속적으로 업데이트 되는 상당한 양의 상태가 있다.
  • 상태 업데이트를 위한 단 1개의 스토어가 필요하다.
  • 최상위 루트 컴포넌트가 모든 상태를 관리하는 것은 적절하지 않다.

앱의 규모가 작은 경우 React만으로 상태 관리하는 방법을 사용하는 것이 더 효과적일 수 있다.

Redux의 3원칙

  1. 애플리케이션 상태는 모두 한 곳에서 관리된다. (동기화 필요 X, 디버깅 용이)

  2. 상태는 불변(읽기 전용) 데이터이며 오직 액션을 전달해 디스패치하는 것만이 상태를 업데이트 할 수 있다.(예측 가능)

  3. 리듀서(순수 함수)를 사용해 상태를 다음 상태로 업데이트 한다.(순수함울 보장)

Redux 아키텍처

Redux는 Store, State, Reducer, Action, Subscription 등을 제공해 효율적으로 상태를 관리한다.

Redux js 실습(꼭 react에서만 사용하는 것은 아님)

index.html

<!DOCTYPE html>
<html lang="ko-KR">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Redux 아키텍처</title>
  <link rel="stylesheet" href="./css/main.css" />
  <script type="module" src="./js/main.js"></script>
</head>
<body>

  <pre></pre>
  
  <div class="buttonGroup">
    <button type="button" class="button" onclick="moveLeft(-5)">move Left</button>
    <button type="button" class="button" onclick="moveRight(5)">move Right</button>
    <button type="button" class="button" onclick="randomBallColor()">Random Change Ball Color</button>
  </div>

  <div class="circle"></div>

</body>
</html>

js/main.js

// css variable get/set
// import like redux lib.
import { cssVars } from '../lib/cssvar.js';
import { createStore } from '../lib/likeRedux.js';

// state 매개변수는 초깃값을 전달 받아 설정
const initialState = {
  color: cssVars('--color'),
  x: 50, // %,
  y: 50,
};

// 리듀서 (순수)함수: 상태 업데이트
// 매개변수: state, action
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'MOVE_DOWM':
    case 'MOVE_UP':
      return {
        ...state,
        y: state.y + action.payload,
      };
    case 'MOVE_RIGHT':
    case 'MOVE_LEFT':
      return {
        ...state,
        x: state.x + action.payload,
      };
    case 'CHANGE_RANDOM_BALL_COLOR':
      return {
        ...state,
        color: action.payload,
      };

    default:
      return state;
  }
};

// 스토어 생성하기 (<- 리듀서 함수)
const store = createStore(reducer);

// 스토어를 구독할 함수
function render() {
  const state = store.getState();
}

// 스토어의 구독 메서드를 사용해 구독할 함수를 구독 설정
store.subscribe(render);

// 액션 (정보 객체 : 무엇을 수행해서 상태를 업데이트 할 것인가?)
// 스토어를 통해 사용자가 요구하는 액션을 디스패치 하기
// store.dispatch({
//   type: 'MOVE_UP',
// });
// store.dispatch({
//   type: 'MOVE_DOWM',
// });
// store.dispatch({
//   type: 'MOVE_LEFT',
// });
// store.dispatch({
//   type: 'MOVE_RIGHT',
// });

// 컨트롤 할 DOM 요소노드
const ball = document.querySelector('.circle');
const output = document.querySelector('pre');

// 구독할 함수 작성
function printState() {
  const state = store.getState();
  output.textContent = JSON.stringify(state, null, 2);
}

printState();

function moveBall() {
  // 상태 가져오기
  const { x, y } = store.getState();
  ball.style.cssText = `left: ${x}%; top: ${y}%`;
}

function updateBallColor() {
  // 상태 가져오기
  const { color } = store.getState();
  ball.style.background = color;
}

function changeRootNodeCssVar() {
  const { color } = store.getState();
  cssVars('--color', color);
}

// 함수에서 스토어 상태 업데이트를 구독
store.subscribe(moveBall);
store.subscribe(updateBallColor);
store.subscribe(changeRootNodeCssVar);
store.subscribe(printState);

// moveLeft, moveRight 함수 작성
window.moveLeft = function moveLeft(disX) {
  // 상태 업데이트 요청
  store.dispatch({ type: 'MOVE_LEFT', payload: disX });
};

window.moveRight = function moveRight(disX) {
  // 상태 업데이트 요청
  store.dispatch({ type: 'MOVE_RIGHT', payload: disX });
};

window.randomBallColor = function randomBallColor() {
  //RGB -> Hexcode
  let red = Math.ceil(Math.random() * 255).toString(16);
  let green = Math.ceil(Math.random() * 255).toString(16);
  let blue = Math.ceil(Math.random() * 255).toString(16);

  let colorValue = `#${red}${green}${blue}`;
  // 새로운 상태로 업데이트 요청
  store.dispatch({
    type: 'CHANGE_RANDOM_BALL_COLOR',
    payload: colorValue,
  });
};

lib/cssvar.js

// 루트 요소
const rootNode = document.documentElement;

// 글로벌 css 변수 가져오기
function getCssVar(varName) {
  return window, getComputedStyle(rootNode, null).getPropertyValue(varName);
}
// 글로벌 css 변수 설정하기
function setCssVar(varName, value) {
  rootNode.style.setProperty(varName, value);
}

// 가져오거나, 설정하는 유틸리티 내보내기
export const cssVars = (varName, value) => {
  if (!value) {
    return getCssVar(varName);
  } else {
    setCssVar(varName, value);
  }
};

lib/likeRedux.js

export const createStore = reducer => {
  if (typeof reducer !== 'function') {
    throw new Error('createStore 함수는 reducer 함수를 전달 받아야 한다.');
  }
  // 외부에서 접근이 불가능한 상태
  let state = reducer(undefined, {});

  // 외부에서 스토어를 구독하는 함수의 집합 관리
  let listeners = [];

  // 외부에서 상태를 가져오고자 할 때 사용하는 멧더ㅡ
  const getState = () => state;

  // 외부에서 사용자가 액션을 받아, 리듀서 함수에 전달한다.
  const dispatch = action => {
    // 리듀서 함수를 실행해 새로운 상태를 반환
    state = reducer(state, action);

    // 스토어의 상태를 구독중인 리스너 실행
    listeners.forEach(listener => listener?.());
  };

  // 외부의 함수가 스토어의 상태 업데이트를 구독
  // 구독 = 상태 업데이트 감지되면, 함수 실행
  const subscribe = addListener => {
    // 구독
    listeners.push(addListener);

    // 구독 해지 (cleanup)
    return () => {
      listeners = listeners.filter(listener => listener !== addListener);
    };
  };

  // 스토어 객체 반환
  return {
    getState,
    dispatch,
    subscribe,
  };
};

리액트에서 리덕스 사용

패키지 설치

yarn add redux
yarn add react-redux

redux devtool 설치

https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd/related?hl=ko

리듀서에 아래의 코드 추가

참고사이트: https://github.com/zalmoxisus/redux-devtools-extension#11-basic-store

window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()

또는 패키지를 설치

yarn add -D redux-devtools-extension

이후

store/index.js

import { composeWithDevTools } from 'redux-devtools-extension';

...
// 스토어 생성
export const store = createStore(rootReducer, composeWithDevTools());

constate 라이브러리의 경우 하나의 상태를 관리할 때 유용하게 쓰이지만 여러개를 관리하는 경우 provider를 여러개 만들어야하기 때문에 Redux와 같은 라이브러리들을 사용하게 된다.

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글