리덕스는 자바스크립트를 위한 상태 관리 프레임워크다.
리덕스 사용 시 따라야할 세 가지 원칙이있다.
전체 상태 값을 하나의 자바스크립트 객체로 표현되기 때문에 활용도가 높아진다. 하지만 프로그램의 전체 상태 값을 리덕스로 관리하는 것은 쉬운 일이 아니므로 일부 상태만 리덕스를 활용해도 된다.
상태 값은 오직 액션 객체에 의해서만 변경되어야 한다.
// 액션 객체
const incrementAction = {
type: 'INCREMENT',
amount: 100,
};
store.dispatch(incrementAction);
type
속성 값이 존재하며, type 속성 값으로 액션 객체를 구분한다.리덕스에서 상태 값을 변경하는 함수를 리듀서(reducer)라고 부른다.
(state, action) => nextState
리듀서는 이전 상태 값과 액션 객체를 입력받아 새로운 상태 값을 만드는 순수 함수다. 순수 함수는 부수 효과를 발생시키지 않아야 한다. 순수 함수는 같은 인수에 대해 항상 같은 값을 반환해야 한다.
(이런 특성때문에 순수 함수는 테스크 코드를 작성하기 쉽다.)
즉, 리듀서는 순수 함수이기 때문에 같은 상태 값과 액션 객체를 입력하면 항상 똑같은 상태 값을 반환한다.
action은 type 속성 값을 가진 자바스크립트 객체이다. 액션 객체를 dispatch 메서드에 넣어서 호출하면 리덕스는 상태 값을 변경하기 위해 위 그림의 과정을 수행한다.
// 액션 객체는 액션 생성 함수와 리듀서에서 액션 객체를 구분할 때도 사용되므로
// 상수 변수로 만드는게 좋다.
export const ADD = 'todo/ADD';
export const REMOVE = 'todo/REMOVE';
export const REMOVE_ALL = 'todo/REMOVE_ALL';
export function addTodo({ titile, priority }) {
return { type: ADD, title, priroity };
}
export function removeTodo({ id }) {
return { REMOVE, id };
}
export function removeAll() {
return { REMOVE_ALL };
}
store.dispatch(addTodo({ title: '영화 보기', priority: 'high' }));
store.dispatch(removeTodo(12));
store.dispatch(removeAll());
액션 타입과 액션 생성 함수는 다른 코드나 외부에서 사용하므로 export 해준다.
미들웨어는 리듀서가 액션을 처리하기 전에 실행되는 함수다.
const myMiddleware = store => next => action => next(action);
미들웨어는 함수 세 개가 중첩된 구조로 되어있다.
import { createStore, applyMiddleware } from 'redux';
const midddleware_1 = (store) => (next) => (action) => {
console.log('midddleware_1 start');
const result = next(action); // next 함수 호출 시 리듀서 호출
console.log('midddleware_1 end');
return result;
};
const myReducer = (state, action) => {
console.log('myReducer');
return state;
};
const store = createStore(myReducer, applyMiddleware(midddleware_1));
store.dispatch({ type: 'SOME_ACTION' });
// midddleware_1 start
// myReducer
// midddleware_1 end
미들웨어 활용
const printLog = (store) => (next) => (action) => {
console.log(`prev state = ${store.getState()}`);
const result = next(action);
console.log(`next state = ${store.getState()}`);
return result;
};
next 함수를 호출 시 리듀서가 호출되고 상태 값이 변경된다. next 함수 호출 전후로 로그를 출력하고있다.
const delayAction = (store) => (next) => (action) => {
const delay = aciton.meta && action.meta.delay;
if (!delay) return next(action);
const timeoutId = setTimeout(() => next(action), delay);
// 취소 함수 반환
return function cancel() {
clearTimeout(timeoutId);
};
};
const cancel = store.dispatch({
type: 'SOME_ACTION',
meta: { delay: 1000 },
});
cancel(); //
리듀서는 액션에 발생했을 때 새로운 상태 값을 만드는 순수 함수이다.
리듀서 함수 예시
function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
// ...
case REMOVE_ALL:
// 새로운 객체 생성: 불변성 관리
return {
...state,
todos: [],
};
case REMOVE:
return {
...state,
todos: todos.filter((todo) => todo.id !== action.id),
};
// 처리할 액션이 없으면 상태값 변경 x
default:
return state;
}
}
const INITIAL_STATE = { todos: [] };
중첩된 객체의 데이터 수정
function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case ADD:
return {
...state,
todos: [...state.todos, { id: getNewId(), title: action.title, priority: action.priority }],
// ...
};
}
}
const INITIAL_STATE = { todos: [] };
todo 1개를 추가하기 위해 spread operator를 2번 사용하고있다. 만약 더 깊은 곳의 값을 수정할 때는 코드의 가독성이 많이 떨어진다. immer 패키지를 사용해 불변 객체를 관리할 수 있다.
import produce from 'immer';
const person = { name: 'kang', age: 27 };
const newPerson = produce(person, (draft) => {
draft.age = 28;
});
produce 함수의 첫번째 매개변수로 변경하고자 하는 객체를 입력한다. 두번째 매개변수는 객체를 수정하는 함수다. draft가 person 객체라 생각하고 값을 수정해도 기존이 person 객체를 수정하지 않고 produce 함수가 새로운 객체를 반환해준다.
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:
break;
}
});
}
const INITIAL_STATE = { todos: [] };
createReducer 함수로 리듀서 작성하기
createReducer 함수를 사용하면 간결하게 리듀서 작성이 가능하다.
const reducer = createReducer(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)),
});
const INITIAL_STATE = { todos: [] };
createReducer 의 경우는 immer의 produce를 자체적으로 지원하기 때문에 따로 코드로 immutable 관리를 하지 않아도 되는 큰 장점이 있습니다.
스토어(store)는 리덕스의 상태 값을 가지는 객체다. 액션의 발생은 스토어의 dispatch 메서드로 시작된다. 스토어는 액션이 발생하면 미들웨어 함수를 실행하고 리듀서를 실행해서 상태 값을 새로운 값으로 변경한다.
createReducer 파일 작성
import produce from 'immer';
export default function createReducer(initialState, handlerMap) {
return function (state = initialState, action) {
return produce(state, (draft) => {
const handler = handlerMap[action.type];
if (handler) {
handler(draft, action);
}
});
};
}
친구 목록을 위한 리덕스 코드 작성 (ducks 패턴)
import createReducer from '../Common/createReducer';
// 액션 타입
const ADD = 'friend/ADD';
const REMOVE = 'friend/REMOVE';
const EDIT = 'firend/EDIT';
// 액션 생성자 함수
export const addFriend = (friend) => ({ type: ADD, friend });
export const removeFriend = (friend) => ({ type: REMOVE, friend });
export const editFriend = (friend) => ({ type: EDIT, friend });
// 초기 상태
const INITIAL_STATE = { friends: [] };
// 리듀서
const reducer = createReducer(INITIAL_STATE, {
[ADD]: (state, action) => state.friends.push(action.friend),
[REMOVE]: (state, action) => state.friends.filer((friend) => friend.id !== action.friend.id),
[EDIT]: (state, action) => {
const index = state.friends.findIndex((friend) => friend.id === action.friend.id);
if (index >= 0) {
state.friend[index] = action.friend;
},
});
export default reducer;
타임라인을 위한 리덕스 코드 작성
import createReducer from '../Common/createReducer';
// 액션 타입
const ADD = 'timeline/ADD';
const REMOVE = 'timeline/REMOVE';
const EDIT = 'timeline/EDIT';
const INCREASE_NEXT_PAGE = 'timeline/INCREASE_NEXT_PAGE';
// 액션 생성자 함수
export const addTimeline = (timeline) => ({ type: ADD, timeline });
export const removeTimeline = (timeline) => ({ type: REMOVE, timeline });
export const editTimeline = (timeline) => ({ type: EDIT, timeline });
export const increaseNextPage = () => ({ type: INCREASE_NEXT_PAGE });
const INITIAL_STATE = { timelines: [], nextPage: 0 };
const reducer = createReducer(INITIAL_STATE, {
[ADD]: (state, action) => state.timelines.push(action.timeline),
[REMOVE]: (state, action) =>
(state.timelines = state.timelines.filer((timeline) => timeline.id !== action.timeline.id)),
[EDIT]: (state, action) => {
const index = state.timelines.findIndex((timeline) => timeline.id === action.timeline.id);
if (index >= 0) {
state.timelines[index] = action.timeline;
}
},
[INCREASE_NEXT_PAGE]: (state, action) => (state.nextPage += 1),
});
export default reducer;
src/index.js에 추가
import { createStore, combineReducers } from 'redux';
import timelineReducer, {
addTimeline,
editTimeline,
increaseNextPage,
removeTimeline,
} from './timeline/state';
import friendReducer, { addFriend, editFriend, removeFriend } from './friend/state';
// reducer 합치기
const reducer = combineReducers({
timeline: timelineReducer,
friend: friendReducer,
});
// 스토어 생성
const store = createStore(reducer);
store.subscribe(() => {
const state = store.getState();
console.log(state);
});
store.dispatch(addTimeline({ id: 1, desc: '즐거워' }));
store.dispatch(addTimeline({ id: 2, desc: '리덕스' }));
store.dispatch(increaseNextPage());
store.dispatch(editTimeline({ id: 2, desc: '수정' }));
store.dispatch(removeTimeline({ id: 1 }));
store.dispatch(addFriend({ id: 1, name: 'kang' }));
store.dispatch(addFriend({ id: 2, name: 'kim' }));
store.dispatch(editFriend({ id: 2, name: 'son' }));
store.dispatch(removeFriend({ id: 1, name: 'kang' }));