개요
리덕스는 상태 관리를 위한 라이브러리이다. 리액트에서만 사용하는 것이 아니라 순수 자바스크립트 환경 & 여러 프론트엔트 프레임워크에서도 사용이 가능하다. 일례로 Vue프로젝트에서 상태 관리를 사용하기 위해 사용했던 flux
패턴도 리덕스에서 참고 하였다. (당시 작성한 게시글)
공식문서에서 제안하는 리덕스의 필요성은 다음과 같다.
- 계속해서 바뀌는 상당한 양의 데이터가 있다.
- 상태를 위한 단 하나의 근원이 필요하다.
- 최상위 컴포넌트가 모든 상태를 가지고 있는 것은 더 이상 적절하지 않다.
설치하기
공식 문서를 살펴보면 Redux Toolkit
을 강력하게 추천해주고 있다. 기존 Redux에서의 단점을 개선한 라이브러리인 것 같은데, 아직 리덕스를 사용해보지 않은 입장에서 어떻게 사용하는지는 알아야 할 것 같아 리덕스 코어 라이브러리를 설치하였다.
npm install redux
구성요소
Store
애플리케이션 상태(state)와 리듀서가 들어가 있으며 여러 내장 함수를 지니고있다.
- getState: 현재 상태 트리를 반환한다.
- dispatch: 액션 객체를 전달해 상태 변경을 일으킨다.
아래 코드가 기본적인 리덕스의 코드이다. (Node환경에서 테스트)
const { createStore } = require('redux');
//reducer 함수
const todos = (prev, action) => {
switch (action.type) {
case 'ADD_TODO':
return [...prev, action.text];
default:
return prev;
}
};
//store 생성 (reducer, initial data, enhancer)
const store = createStore(todos, ['씻기']);
//action store 함수
function addTodo(text) {
return {
type: 'ADD_TODO',
text
};
}
//dispatch
store.dispatch(addTodo('공부하기'));
store.dispatch(addTodo('밥먹기'));
//state get
console.log(store.getState()); //[ '씻기', '공부하기', '밥먹기' ]
Action
상태에 어떠한 변화가 필요하면 발생한다. 객체로 표현되며 type
속성을 가지고 있다. (액션의 고유 이름)
{
type: 'ACTION_NAME',
data: 'data'
}
동적으로 상태 값을 핸들링하려면 매번 액션 객체를 만들어주는 것은 비효율적이다. 액션 생성 함수(action creator)를 사용하면 함수로 관리할 수 있다.
//action creator
const addItem = (data) => {
return {
type: 'ADD_ITEM',
data
};
};
Reducer
액션이 발생하면 리듀서 함수가 현재 상태에서 전달받은 객체를 파라미터로 전달 받아 새로운 상태를 반환한다. 두 개의 매개변수를 받는데 첫 번째 매개변수에 이전 상태 트리를 포함하고 있고, 두 번째 매개변수에 액션 객체를 전달 받는다.
const reducer = (prevState=[] , action) => {
switch (action.type) {
case 'ADD_TODO':
return [...prevState, action.data];
default:
return prevState;
}
const addItem = (data) => {
return {
type: 'ADD_TODO',
data
};
};
store.dispatch(addItem("밥먹기"))
Dispatch
스토어의 내장 함수이다. 디스패치는 액션을 발생시키는 동작이다.
store.dispatch(addItem('밥먹기'));
Subscribe
subscribe 메서드 콜백 함수를 전달하면 상태가 변경될 때 마다 호출된다. (Action → Dispatch 상황)
const { createStore } = require('redux');
const store = createStore(reducer, initialState);
const subscribe = store.subscribe(() => console.log('dispatched!!'));
//구독을 취소하고 싶을 때 subscribe 반환 함수를 실행하면 된다.
subscribe();
Middleware
액션을 디스패치하는 과정 사이에 리듀서 함수에서 처리하기 전 미리 지정된 작업들을 수행할 수 있다. (Action, Reducer 사이의 미들웨어) createStore
세번째 인수에 enhancer
함수(미들웨어로 수행할 함수)를 전달해 사용한다.
//Middleware 구조 선언
const middleware = (store) => (dispatch) => (action) => {
console.log('Before', action);
dispatch(action);
console.log('After');
};
const enhancer = applyMiddleware(middleware);
const store = createStore(reducer, apply, enhancer);
미들웨어를 활용한 비동기 처리
리덕스에서 비동기적인 함수를 수행하기 위해 사용하는 라이브러리가 Redux Thunk
이다. 액션은 객체 형태이기 때문에 동기식으로만 동작한다. 액션을 함수 형태로 전달해 함수 타입일 때 전달 받은 dispatch를 실행시키는 형태이다.
const reducer = (prevState, action) => {
switch (action.type) {
.
..
...
case 'GET_TODO':
return {
...prevState,
todos: [action.data]
};
default:
return {...prevState};
}
};
//비동기 동작을 위한 thunk미들웨어
const thunkMiddleware = store => dispatch => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return dispatch(action);
};
//store 생성
const store = createStore(reducer, initialState, applyMiddleware(thunkMiddleware));
//async action creator (비동기 액션은 함수 형태로 전달)
const getTodo = (data) => {
return (dispatch, getState) => {
//api를 호출하였다고 가정 (async)
setTimeout(() => {
dispatch(getTodo(['씻기', '공부하기', '밥먹기']));
}, 2000);
}
};
//액션 dispatch (기존과 다르게 함수를 전달했다.)
store.dispatch(requestTodo());
console.log('async!', store.getState());
주의사항
- 하나의 애플리케이션에서는 단 하나의 스토어만 관리되어야한다. (단일 스토어)
- 리덕스 상태는 읽기 전용이다. 기존의 객체에 변화를 주지 않고 새로운 객체를 생성 해주어야 한다.
- 얕은 복사를 하기 때문에 상태 변경 추적이 가능하게 된다.
- 리듀서 함수는 순수 함수로 작성해야 한다. (전달받는 파라미터에만 의존)
- 순수 함수의 장점을 통해
combineReducers
메서드로 여러 리듀서들을 결합 시킬 수 있다.
- 순수 함수의 장점을 통해
정리
리액트를 공부하면서 사용해본 상태 관리는 context api, recoil 정도이다. 비교적 상태관리가 많아질 프로젝트에서 React-Redux를 도입해보기 전에 리덕스가 어떻게 돌아가는지 흐름을 잡아보았다. 공식 문서에도 잘 나와있어서 추가적인 redux-toolkit과 redux-saga에 대해서도 한번 공부해봐야겠다.
참고 자료
- Redux 공식 문서
- 리액트를 다루는 기술(velopert) - 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기
- 조현영님의 Redux 강의