20210615 Redux01 : Redux의 개념, Store, Action, Reducer, 데이터 구조에 따른 설계, combineReducers, reducers 모듈화

8 분 소요

Redux 01



Redux Basic

  • Component들 간의 정보 전달 과정에서 불편함을 느끼고 contextAPI를 사용하는데
  • Redux는 해당 context에 있는 전역 데이터를 효과적인 관리하는 라이브러리 임
  • Redux는 Store라는 독립적인 부분 딱 하나 존재하는데(단일 스토어), 어느 Component에서 store의 state를 변경하면 store에서 연관된 다른 Component를 rerender 시킴


단일 스토어 사용 준비


  • 스토어 만들기
    • import redux
    • 액션 정의 -> 액션을 사용하는 리듀서 만듦 -> 리듀서들을 합침 -> 최종 합쳐진 리듀서를 인자로 단일 스토어를 만듦


  • 스토어 사용하기
    • import react-redux
    • connect() 함수를 이용해서 컴포넌트에 연결


Actions (액션)


  • 액션은 객체(Object)이다.
  • 두가지 형태의 액션이 존재
    • type만 있는 액션 {type: 'TEST'} (payload 없는 액션, Action에서 전달할 특별한 값이 없는 경우)
    • type과 payload가 있는 액션 {type: 'TEST', params: 'hello'} (Action에서 특별한 값을 전달하여 활용해야 하는 경우)
    • type은 필수 property 이고, String 임
  • 만들어진 Action이 스토어에 전해 스토어의 상태를 변경하는 용도로 사용됨



액션 생성자


  • Action Creator : 액션을 생성하는 함수
  • 함수를 통해 액션을 생성해서, 액션 객체를 return 함
  • 예) createTest('hello') -> return {type: 'TEST', params:'hello'}
function ActionCreator(...args) {
  return ActionObject;
}



액션이 하는 일


  • 액션 생성자를 통해 액션을 만듦 -> 액션 객체를 리덕스 스토어에 보냄 -> 리덕스 스토어가 액션 객체를 받으면 스토어의 상태값이 변경됨 -> 변경된 상태 값에 의해 상태를 이용하고 있는 컴포넌트가 변경됨
  • 액션은 스토어에 보내는 일종의 인풋이라고 볼 수 있음



액션 준비


  • 액션의 타입을 정의하여 변수로 빼는 작업
    • 강제는 아님
    • 그냥 타입을 문자열로 넣기에는 실수를 유발할 가능성이 큼
    • 미리 정의한 변수를 사용하면 스펠링에 주의를 덜 기울여도 됨


  • 액션 객체를 만들어 내는 함수를 만드는 작업
    • 하나의 액션 객체를 만들기 위해 하나의 함수를 만들어 냄
    • 액션의 타입은 미리 정의한 타입 변수로 부터 가져와서 사용함
// action.js
// type string 관리
export const ADD_TODO = "ADD_TODO";
// type의 경우 space는 underbar, 글자는 대문자로 사용함

// addTodo의 매개변수가 todo임으로 todo property를 항상 가짐을 알 수 있음
export function addTodo(todo) {
  return {
    type: ADD_TODO,
    // todo: todo
    todo,
  };
}




Reducers (리듀서)


  • 액션을 주면, 그 액션이 적용되어 수정되거나 아닌 결과를 만들어주는 함수
  • Pure Function : 같은 input을 받으면 같은 결과를 내는 함수
    • reducer 안에서 시간에 따라서 변하는 결과가 들어가면 안됨
  • Immutable : original state와 new State가 별도의 객체로 만들어 져야함 (리덕스는 리듀서를 통해 스테이트가 달라졌음을 Immutable 방식으로 인지함)
function reducerName(preState, action) {
  return newState;
}
  • 액션을 받아서 스테이트를 리턴하는 구조
  • 인자로 들어오는 prevState와 리턴되는 newState는 다른 참조를 가지도록 해야함(Immutable 해야함)
// Type 가져오기
import { ADD_TODO } from "./actions";

// state 구상 -> ['코딩', '점심 먹기']
const initialState = [];

export function todoApp(previousState = initialState, action) {
  // 초기값 설정 방법 01
  // if (previousState === undefined) {
  //   return [];
  // }

  // 초기값 설정 방법 02
  // 매개변수 자체에 initialState 값을 default 설정해 줌

  if (action.type === ADD_TODO) {
    // 배열 형태로 todo 값을 넣음
    return [...previousState, action.todo];
  }

  return previousState;
}




createStore()


  • 스토어를 만드는 함수
    • const store = createStore(리듀서)
// store.js
import { createStore } from "redux";
import { todoApp } from "./reducers";

const store = createStore(todoApp);

export default store;


  • 스토어 함수 return 관련 함수
    • store.getState() : state를 가져오는 함수
    • store.dispatch(액션), store.dispatch(액션생성자()) : 액션을 인자로 넣어서 store의 상태를 변경시킴
    • const unsubscribe = store.subscribe(()=>{})
      • subscribe()는 state에 변경이 생겼을 때 안에 있는 callback을 실행시킴
      • return 이 unsubscribe 함수 임
      • unsubscribe() 하면 subscribe로 실행했던 함수가 이후 부터는 제거됨
    • store.replaceReducer: 원래 가지고 있던 reducer를 다른 reducer로 바꾸는 기능


import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

// store와 action생성자를 가져옴
import store from "./redux/store";
import { addTodo } from "./redux/actions";

// subscribe : 스토어의 상태가 변경되면 함수가 호출됨
// 그리고 unsubscribe의 함수를 return 함
const unsubscribe = store.subscribe(() => {
  console.log(store.getState());
});

// dispatch : state를 변경 함
store.dispatch(addTodo("coding"));
store.dispatch(addTodo("Read Books"));
store.dispatch(addTodo("eat"));
unsubscribe();
// 이후에는 subscribe에 있는 함수가 실행안됨 (store 변경은 그대로 이루어짐)
store.dispatch(addTodo("coding"));
store.dispatch(addTodo("Read Books"));
store.dispatch(addTodo("eat"));

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);


정리


  • Action 만들기
    • Action 생성자 함수 만들기 (Type 결정)
    • Type이 필요함
    • Action에 데이터가 들음


  • Reducer 만들기
    • Reducer 생성 함수 만들기
    • Type을 가져와서 만듦
    • Type을 기준으로 action을 받아 state를 변경할지 결정하는 로직


  • Store 만들기
    • createStore(reducer)로 단일 store를 만듦
    • 어떤 reducer 로직으로 store 만들건지 결정


  • Store 활용하기
    • dispatch(Action) : state 변화 시키기
    • subscribe(callback) : state 변화시에 실행할 callback
    • unsubscribe() (subscribe의 return) : subscribe 의 callback 중지




State 설계01 : state가 객체 형태의 데이터에 boolean 상태를 가지는 경우


  • State 형태 설계
    • ['코딩', '점심 먹기'] -> [{text: '코딩', done: false}, {text: '점심 먹기', done: false}]
  • done 속성의 경우, 변경 가능하게 해야함


Actions 설계


// actions.js

// type 관리
export const ADD_TODO = "ADD_TODO";
export const COMPLETE_TODO = "COMPLETE_TODO";

// Actions Creator
export function addTodo(text) {
  return {
    type: ADD_TODO,
    text,
  };
}
// {type: ADD_TODO, text: '할일'}
// 어차피, boolean 값은 기본값이 false이기 때문에 데이터 추가 및 생성 때는 받을 필요가 없음

// Change Done
export function completeTodo(index) {
  return {
    type: COMPLETE_TODO,
    index,
  };
}
// {type: ADD_TODO, index: 0}
// 어떤 것을 done true로 만들 것인가? -> index를 받아서 해당 action을 true로 만듦



Reducer 설계


import { ADD_TODO, COMPLETE_TODO } from "./actions";

const initialState = [];

export function todoApp(previousState = initialState, action) {
  // state에 데이터 추가 하는 경우
  if (action.type === ADD_TODO) {
    return [...previousState, { text: action.text, done: false }];
  }
  // 객체가 배열로 쌓이는 구조로 바꿈

  // state에 데이터의 done 상태를 변경 하는 경우
  if (action.type === COMPLETE_TODO) {
    // 변경할 객체를 찾아야 함 -> map 돌려서 받아온 index로 객체 찾아 값 변경
    return previousState.map((todo, index) => {
      if (index === action.index) {
        return { ...todo, done: true };
      }
      return todo;
    });
  }

  return previousState;
}



사용


store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(addTodo("할일01"));
// [{text: "할일01", done: false}]
store.dispatch(completeTodo(0));
// [{text: "할일01", done: true}]




State 설계02 : todo의 배열(todos)과, filter 값을 가지는 경우

  • State 형태 설계
    • [{text: '코딩', done: false}, {text: '점심 먹기', done: false}]
    • => {todos: [{text: '코딩', done: false}, {text: '점심 먹기', done: false}], filter: 'ALL'}
  • filter 속성의 경우, 변경 가능하게 해야함


Actions 설계


// actions.js

// type 관리
export const ADD_TODO = "ADD_TODO";
export const COMPLETE_TODO = "COMPLETE_TODO";
export const SHOW_ALL = "SHOW_ALL";
export const SHOW_COMPLETE = "SHOW_COMPLETE";

// Actions Creator
export function addTodo(text) {
  return {
    type: ADD_TODO,
    text,
  };
}

// Change Done
export function completeTodo(index) {
  return {
    type: COMPLETE_TODO,
    index,
  };
}

// Change Filter
export function showAll() {
  return { type: SHOW_ALL };
}
export function showComplete() {
  return { type: SHOW_COMPLETE };
}



Reducer 설계


  • previousState를 전개구문으로 사용하고, 변화 시킬 property만 변경하여 덮어 쓰게 만들어 각각 독립적으로 명령을 수행할 수 있게 함 (서로 영향을 주지 않음)
import { ADD_TODO, COMPLETE_TODO, SHOW_ALL, SHOW_COMPLETE } from "./actions";

// Initial Data structure
const initialState = { todos: [], filter: "ALL" };

// Reducer
export function todoApp(previousState = initialState, action) {
  // Add toDo
  if (action.type === ADD_TODO) {
    return {
      ...previousState,
      todos: [...previousState.todos, { text: action.text, done: false }],
    };
  }

  // Change Done
  if (action.type === COMPLETE_TODO) {
    return {
      ...previousState,
      todos: previousState.todos.map((todo, index) => {
        if (index === action.index) {
          return { ...todo, done: true };
        }
        return todo;
      }),
    };
  }

  // Change Filter
  if (action.type === SHOW_ALL) {
    return {
      ...previousState,
      filter: "ALL",
    };
  }

  if (action.type === SHOW_COMPLETE) {
    return {
      ...previousState,
      filter: "COMPLETE",
    };
  }

  return previousState;
}



사용


store.subscribe(() => {
  console.log(store.getState());
});

store.dispatch(addTodo("할일01"));
// {todos: Array(1), filter: "ALL"}
store.dispatch(completeTodo(0));
// {todos: Array(1), filter: "ALL"} -> done : true
store.dispatch(showComplete());
// {todos: Array(1), filter: "COMPLETE"}
store.dispatch(showAll());
// {todos: Array(1), filter: "ALL"}




Key Point

  • 단일 store이기 때문에, reducer를 어떻게 잘쪼개서 쓰느냐가 관건임
  • reducer를 쪼개서 type 별로 사용하는데 type별로 각각의 행동이 영향을 미치지 않고 독립적으로 이루어 질수 있게 하는 것이 포인트임
    • previousState를 전개하여 사용하여 나머지는 그대로 유지하게 하는 방법을 잘 이용 할것!(...previousState)




combineReducers


  • 규모가 커진 앱을 다루는 경우
  • store는 단일 store이기 때문에 reducer 또한 하나가 들어가게 되는데, reducer를 관리할 때 너무 많은 내용들이 하나의 reducer로 뭉쳐있어 복잡도가 올라감
  • 이를 해결하기 위해서 여러개의 Reducer로 작성하고, 이를 하나의 Reducer로 만들어 주는 것이 combineReducers


CombineReducers 사용


  • combineReducers에는 모두 종합된 Data 구조의 객체가 들어가고, 해당 속성에는 속성별 Reducer 함수가 들어가서 연결 되어 있음
  • 기존의 reducer를 각 속성별 함수로 분해하기 때문에, 속성별 Reducer 함수가 return 하는 결과도 각 속성에 맞게 변경해줘야 함
  • 각 Reducer 함수가 참조하는 initialState 구조도 각 함수에 맞게 변경해서 연결 해줘야 함
import { combineReducers } from "redux";

// Types
import { ADD_TODO, COMPLETE_TODO, SHOW_ALL, SHOW_COMPLETE } from "./actions";

// Initial Data structure
const initialState = { todos: [], filter: "ALL" };
const todosInitialState = initialState.todos;
const filterInitialState = initialState.filter;

// CombineReducers
const reducer = combineReducers({
  todos: todosReducer,
  filter: filterReducer,
});
export default reducer;

// todosReducer
function todosReducer(previousState = todosInitialState, action) {
  // Add toDo
  if (action.type === ADD_TODO) {
    return [...previousState, { text: action.text, done: false }];
  }

  // Change Done
  if (action.type === COMPLETE_TODO) {
    return previousState.map((todo, index) => {
      if (index === action.index) {
        return { ...todo, done: true };
      }
      return todo;
    });
  }

  return previousState;
}

// filterReducer
function filterReducer(previousState = filterInitialState, action) {
  // Change Filter
  if (action.type === SHOW_ALL) {
    return "ALL";
  }

  if (action.type === SHOW_COMPLETE) {
    return "COMPLETE";
  }

  return previousState;
}




combineReducers를 구성하는 각 reducer의 모듈화 (파일로 분리 관리)


  • redux 폴더에서, reducers 폴더를 따로 구성함



Reducers : reducer


  • 개별적인 Reducer들을 묶어 store로 보내는 router 같은 역할을 함
// reducers/reducer.js
import { combineReducers } from "redux";

// reducer
import todos from "./todos";
import filter from "./filter";

// combineReducers
const reducer = combineReducers({
  todos: todos,
  filter: filter,
});
export default reducer;


Reducers : todos


// reducers/todos.js

// Types
import { ADD_TODO, COMPLETE_TODO } from "../actions";

// Initial Data structure
const initialState = [];

// todosReducer
export default function todos(previousState = initialState, action) {
  // Add toDo
  if (action.type === ADD_TODO) {
    return [...previousState, { text: action.text, done: false }];
  }

  // Change Done
  if (action.type === COMPLETE_TODO) {
    return previousState.map((todo, index) => {
      if (index === action.index) {
        return { ...todo, done: true };
      }
      return todo;
    });
  }

  return previousState;
}


Reducers : filter


// reducers/filter.js

// Types
import { SHOW_ALL, SHOW_COMPLETE } from "../actions";

// Initial Data structure
const initialState = "ALL";

// filterReducer
export default function filter(previousState = initialState, action) {
  // Change Filter
  if (action.type === SHOW_ALL) {
    return "ALL";
  }

  if (action.type === SHOW_COMPLETE) {
    return "COMPLETE";
  }

  return previousState;
}
  • store에서 reducer 경로와 각각 type들의 경로에 주의를 하자.

태그:

업데이트: