Redux
20210615 Redux01 : Redux의 개념, Store, Action, Reducer, 데이터 구조에 따른 설계, combineReducers, reducers 모듈화
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 임
- type만 있는 액션
- 만들어진 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 변화시에 실행할 callbackunsubscribe()
(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들의 경로에 주의를 하자.