리액트도 제대로 못 쓰면서 무슨 리덕스냐...?!

그러기엔 이미 생활코딩 리덕스도 보았고...책 목차도 리덕스다...

한 번 봐두고 나면, 나중에 실습(?)할 때 도움이 될 것이다! ⛱


🍙 React + Redux = ?

리덕스는 JS를 위한 상태 관리 프레임워크이다.

왜 리액트와 리덕스는 같이 사용할까?

  • 컴포넌트로부터 상태 관리 코드를 분리한다.
  • 서버 렌더링 시 데이터 전달이 편하다.
  • LocalStorage에 데이터 저장 및 호출이 편하다.
  • 같은 상탯값을 다수의 컴포넌트에 적용할 때 좋다.
  • 전역 컴포넌트의 상탯값을 관리할 때 좋다.
  • 페이지가 전환되어도 데이터가 살아있어야 할 때 좋다.

이것저것 나열했지만, 사용하면서 알아보는게 더 빠르다.

🍙 리덕스 사용 시 3원칙

  1. 전체 상탯값을 하나의 객체에 저장한다.
    최근 상탯값을 버리지 않으면 실행 취소 기능을 쉽게 구현할 수도 있다.
  2. 상탯값은 불변 객체다.
    리덕스에서 상탯값을 수정하는 유일한 방법은 dispatch메소드를 호출하는 것이다.
  3. 상탯값은 순수 함수에 의해서만 변경되어야 한다.
    리덕스에서 상탯값을 변경하는 함수를 리듀서라고 한다.
    리듀서는 이전 상탯값과 액션객체를 입력으로 받아 새로운 상탯값을 만드는 순수 함수다.
    리듀서(Reducer) : (state, action) => nextState

🍘 리덕스의 개념

1. 액션

액션은 type 속성값을 가지는 JavaScript객체이다.
각 액션은 고유한 type 속성값을 사용해야 한다.

function addTodo({ title, priority }) {
  return { type: 'todo/Add', title, priority };
}
function removeTodo({ id }) {
  return { type: 'todo/REMOVE', id };
}
function removeAllTodo() {
  return { type: 'todo/REMOVE_ALL' };
}
store.dispatch(addTodo({ title: '영화 보기', priority: 'high' }));
store.dispatch(removeTodo({ id: 123 }));
store.dispatch(removeAllTodo());

이때, type속성값은 상수 변수로 만드는게 좋다.

export const ADD = 'todo/ADD';
export const REMOVE = 'todo/REMOVE';
export const REMOVE_ALL = 'todo/REMOVE_ALL';
export function addTodo({ title, priority }) {
  return { type: ADD, title, priority };
}
// ...생략...

2. 미들웨어

미들웨어는 리듀서가 액션을 처리하기 전에 실행되는 함수이다.
디버깅 목적으로 상태값 변경 시 로그를 출력하거나, 리듀서에서 발생한 예외를 서버로 전송한다.

// 미들웨어 기본 구조
const myMiddleware = store => next => action => next(action);

// 미들웨어 설정 방법
import { createStore, applyMiddleware } from 'redux';
const middleware1 = store => next => action => {
  console.log('middleware1 start');
  const result = next(action);
  console.log('middleware1 end');
  return result;
}
const middleware2 = store => next => aciton {
  console.log('middleware2 start');
  const result = next(action);
  console.log('middleware2 end');
  return result;
}
const myReducer = (state, action) => {
  console.log('myReducer');
  return state;
};
const store = createStore(myReducer, applyMiddleware(middleware1, middleware2));
store.dispatch({ type: 'someAction' });
// middleware1 start
// middleware2 start
// myReducer
// middleware1 end
// middleware2 end

로그를 출력하는 미들웨어

액션이 발생할 때마다 이전 상탯값과 이후 상탯값을 로그로 출력한다.

const pringLog = store => next => action => {
  console.log(`prev state = ${store.getState()}`);
  const result = next(action);
  console.log(`next state = ${store.getState()}`);
  return result;
}

에러 정보를 전송하는 미들웨어

예외 발생 시 자동으로 서버에 에러 정보를 전송한다.

const reportCrash = store => next => action => {
  try {
    return next(action);
  } catch (err) {
    // 서버로 예외 전송
  }
};

로컬 스토리지에 값을 저장하는 미들웨어

SET_NAME이 발생할 때마다 로컬스토리지에 값을 저장한다.

const saveToLS = store => next => action => {
  if (action.type === 'SET_NAME') {
    localStorage.setItem('name', action.name);
  }
  return next(action);
};

3. 리듀서

리듀서는 액션이 발생했을 때 새로운 상탯값을 만드는 함수이다.
아래 코드를 보자.
리덕스는 스토어를 생성할 때 상탯값이 없는 상태로 리듀서를 호출한다.
따라서 매개변수 기본값에 상탯값을 준다.
또, 액션은 타입별로 case 구문을 만들어 준다.

function reducer(state = INITIAL_STATE, action) {
  switch (action.type) {
    // ...
    case REMOVE_ALL:
      return {
        ...state, 
        todos: [],
      };
    case REMOVE:
      return {
        ...state,
        todos: state.todos.filter(todo => todo.id !== action.id),
      };
    default:
      return state;
  }
}
const INITIAL_STATE = { todos: [] };

위 코드보다 더 깊은 값을 수정하려면 가독성이 떨어진다.
불변 객체를 위한 패키지 중 immer를 사용하면 아래와 같다.

import produce from 'immer';

function reducer(state = INITIAL_STATE, action) {
  return produce(state, draft => {
    switch (action.type) {
      case ADD:
        draft.todos.push(action.todo);
        break;
      case REMOVE_ALL:
        draft.todos = [];
        break;
      case REMOVE:
        draft.todos = draft.todos.filter(todo => todo.id !== action.id);
        break;
      default:
        breakl
    }
  });
}
createReducer함수를 사용하면 더 간단하게 작성할 수 있다!
const reducer = createReduce(INITIAL_STATE, {
  [ADD]: (state, action) => state.todos.push(action.todo),
  [REMOVE_ALL]: state => (state.todos = []),
  [REMOVE]: (state, action) =>
      (state.todos = state.todos.filter(todo => todo.id !== action.id)),
});

4. 스토어

스토어는 리덕스의 상탯값을 갖는 객체이다.
스토어는 액션이 발생하면 미들웨어를 실행하고, 리듀서를 실행하여 새로운 상탯값으로 변경한다.
그리고 사전에 등록된 모든 이벤트 처리 함수에게 액션처리가 끝났음을 알린다.

아래 코드를 보자.
subscribe메서드로 이벤트 처리 함수를 등록한다.
스토어에 등록된 함수는 액션이 처리될 때마다 호출된다.
그리고 dispatch메서드로 각 액션에 따른 동작이 실행된다.

const INITIAL_STATE = { value: 0 };
const reducer = createReducer(INITIAL_STATE, {
  INCREMENT: state => (state.value += 1),
});
const store = createReducer(reducer);

let prevState;
store.subscribe(() => {
  const state = store.getState();
  if (state === prevState) {
    cosole.log('상탯값 동일');
  } else {
    cosole.log('상탯값 변경');
  }
  prevState = state;
});

store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'OTHER_ACTION' });
store.dispatch({ type: 'INCREMENT' });