- 리덕스(Redux)는 리액트 생태계에서 가장 사용률이 높은 상태관리 라이브러리이다.
- 자바스크립트 앱을 위한 상태 컨테이너라고 생각하면 된다.
- 리액트만을 위한 Library는 아니다. 리액트 뿐만 아니라 Augular, jQuery, vanilla JavaScript 등 다양한 framework와 작동되게 설계되었다.
- 리덕스는 Flux 패턴을 기반으로 생성되었기 때문에 단방향으로 일관적으로 동작하고, 서로 다른 환경(서버, 클라이언트, 네이티브)에서 작동하며, 테스트하기 쉬운 앱을 작성하도록 도와준다.
- Flux는 Facebook에서 만든 client-side web applications을 구축할 때 사용하는 application architecture(앱 구조), design pattern(디자인 패턴)이다.
- MVC (Model–View–Controlle)구조 의 단점을 보완할 목적으로 개발된 Flux는 대규모 프로젝트에서 너무 복잡해지는 MVC구조의 단점을 보완하는 단방향 데이터 흐름(unidirectional data flow)의 구조이다.
복잡한 MVC 패턴
- Flux 패턴은 Model이 View를 반영하고, View가 Model을 변경하는 양방향 데이터 흐름에서 벗어나 단방향으로만 데이터를 변경할 수 있도록 만들었다.
- 액션은 데이터의 상태를 변경할 수 있는 명령어 카드와 같습니다.
- 액션 생성자는 새로 발생한 액션의 타입과 데이터 페이로드를 액션 메시지로 묶어 디스패쳐로 전달합니다.
- 디스패쳐는 액션 메시지를 감지하는 순간 그것을 각 스토어에 전달합니다.
- 전달은 콜백 함수로 이루어지며, 등록되어 있는 모든 스토어로 페이로드를 전달할 수 있습니다.
- 이때 스토어가 서로를 의존하고 있다면 (예를들어, 학생의 개인정보를 담은 스토어와 모든 학생의 수학 점수만을 담은 스토어) 특정 스토어가 업데이트되기를 기다리게 해주는 waitFor()를 사용할 수 있습니다.
- 스토어는 어플리케이션의 상태와, 상태를 변경할 수 있는 메서드를 가지고 있습니다.
- 어떤 타입의 액션이 날아왔느냐에 따라 메서드를 다르게 적용해 상태를 변경하게 됩니다.
- React에 해당되는 부분입니다.
- 컨트롤러 뷰는 스토어에서 변경된 데이터를 가져와 모든 자식 뷰에게 데이터를 분배합니다.
- 데이터를 넘겨받은 뷰는 화면을 새로 렌더링합니다.
- Flux와 달리 Redux는 dispatcher라는 개념이 존재하지 않는다.
- Redux는 다수의 store도 존재하지 않는다. 대신 Redux는 하나의 root에 하나의 store만이 존재한다.
- 순수함수(pure functions)에 의존한다. (state의 불변성)
결국 Redux는 Flux 패턴을 좀 더 쉽고 정돈된 형태로 쓸 수 있게 도와주는 라이브러리라고 볼 수 있다.
- 자식 컴포넌트들 간의 다이렉트 데이터 전달은 불가능 하다.
- 자식 컴포넌트들 간의 데이터를 주고 받을 때는 상태를 관리하는 부모 컴포넌트를 통해서 주고 받는다.
- 자식이 많아진다면 상태 관리가 매우 복잡해진다.
- 상태를 관리하는 상위 컴포넌트에서 계속 내려 받아야한다. = Props drilling 이슈
- 상태 업데이트 로직 분리
- 더 쉬운 상태 관리
- Props drilling 이슈 해결
- 순수함수를 사용함으로써, 상태를 쉽게 예측할 수 있고, 로직이 어떻게 작동하는지 쉽게 이해할수 있으며, 테스트코드를 붙이기에도 용이하다.
- 다른 복잡한 상태관리 방법에 비해 유지보수가 편하다.
- redux dev tool 이라는 크롬 확장을 사용하여 디버깅에 유리하다.
- 다양한 곳에 많은 양의 상태값들이 필요로 할 때,
- 상태가 시간이 지남에따라 자주 업데이트가 될 때,
- 해당 상태를 업데이트하는 로직이 복잡할 때,
- 중간 규모 이상의 소스코드를 포함한 작업을 많은 사람들이 함께 할 때,
- "actions"라는 이벤트를 사용하여 상태를 관리하고 업데이트하는 패턴 및 라이브러리
- 가장 사용률이 높다.
- 리덕스 미들웨어라는 기능을 통하여 비동기 작업, 로깅 등의 확장적인 작업들을 더욱 쉽게 할 수도 있게 해준다.
- 상태 변화가 필요할 때, 액션이란 것을 발생시킨다.
- 하나의 객체로 되어있다.
- 액션 객체는
type
필드를 필수적으로 가지고 있어야하고 그 외의 값들은 마음대로 넣어줄 수 있다.
{
type: "ADD_TODO",
data: {
id: 0,
text: "리덕스 배우기"
}
}
- 말 그대로 액션을 생성하는 함수.
- 단순히 파라미터를 받아와서 액션 객체 형태로 만들어준다.
export function addTodo(data) {
return {
type: "ADD_TODO",
data
};
}
// 화살표 함수로도 가능하다
export cohnst addTodo = data => {
return {
type: "ADD_TODO",
data
};
}
- 액션 생성함수를 만들어서 사용하는 이유는 나중에 컴포넌트에서 더욱 쉽게 액션을 발생시키기 위함이다.
- 그래서 보통 함수 앞에 export 키워드를 붙여서 다른 파일에서 불러와서 사용한다.
- 리덕스를 사용 할 때 액션 생성함수를 사용하는것이 필수적이진 않다.
- 액션을 발생 시킬 때마다 직접 액션 객체를 작성할수도 있다.
- 변화를 일으키는 함수.
- 두가지의 파라미터를 받아온다.
- 현재의 상태와, 전달 받은 액션을 참고하여 새로운 상태를 만들어서 반환한다.
// 숫자를 1올리거나 내리는 counter를 구현한다고 하면
// 아래와 같은 모양의 리듀서를 만들 수 있다.
function counter(state, action) {
switch (action.type) {
case 'INCREASE':
return state + 1;
case 'DECREASE':
return state - 1;
default:
return state;
}
}
- 전반적인 형태는 useReducer 와 유사하지만, useReducer 에선 일반적으로
default:
부분에throw new Error('Unhandled Action')
과 같이 에러를 발생시키도록 처리하는게 일반적인 반면 리덕스의 리듀서에서는기존 state
를 그대로 반환하도록 작성해야한다.
- 여러개의 리듀서를 합쳐서 루트 리듀서 ( rootReducer ) 를 만들 수 있다.
- 리덕스에서는 한 애플리케이션당 하나의 스토어를 만든다.
- 현재의 앱 상태와 리듀서가 들어가있고 추가적으로 몇가지 내장 함수들이 들어있다.
- 스토어의 내장함수 중 하나다.
- 액션을 발생 시키는 것 ( 액션을 파라미터로 전달한다. )
- 호출을 하면, 스토어는 리듀서 함수를 실행시켜서 해당 액션을 처리하는 로직이 있다면 액션을 참고하여 새로운 상태를 만들어줍니다.
- 스토어의 내장함수 중 하나
- 함수 형태의 값을 파라미터로 받아온다.
- subscribe 함수에 특정 함수를 전달해주면, 액션이 디스패치 되었을 때 마다 전달해준 함수가 호출됩니다.
- 리액트에서 리덕스를 사용하게 될 때 보통 이 함수를 직접 사용하는 일은 별로 없다.
- 그 대신에 react-redux 라는 라이브러리에서 제공하는 connect 함수 또는 useSelector Hook 을 사용하여 리덕스 스토어의 상태에 구독한다.
- 동일한 데이터는 항상 같은곳에서 가져온다. ( 하나의 스토어에서 )
- 여러개의 스토어를 사용하는것은 사실 가능하기는 하나, 권장되지는 않는다.
- 특정 업데이트가 너무 빈번하게 일어나거나, 애플리케이션의 특정 부분을 완전히 분리시키게 될 때 여러개의 스토어를 만들 수도 있다.
- 하지만 그렇게 하면, 개발 도구를 활용하지 못한다.
- 리엑트에서 setState 메소드를 통해서만 상태를 바꿀 수 있는것처럼, 리덕스에서도 액션이라는 객체를 통해서만 상태를 변경할 수 있다.
- 추가적으로 Immutable.js 혹은 Immer.js 를 사용하여 불변성을 유지하며 Redux를 사용할 수 있다.
- 새로운 상태를 생성하여 업데이트 해주는 방식으로 해주면, 나중에 개발자 도구를 통해서 뒤로 돌릴 수도 있고 다시 앞으로 돌릴 수도 있다.
- 리덕스에서 불변성을 유지해야 하는 이유는 내부적으로 데이터가 변경 되는 것을 감지하기 위하여 shallow equality 검사를 하기 때문이다.
- 객체의 변화를 감지 할 때 객체의 깊숙한 안쪽까지 비교를 하는 것이 아니라 겉핥기 식으로 비교를 하여 좋은 성능을 유지할 수 있다.
- 변경은 순수함수로만 가능하다.
- 리듀서 함수는 이전 상태와, 액션 객체를 파라미터로 받는다.
- 이전의 상태는 절대로 건들이지 않고, 변화를 일으킨 새로운 상태 객체를 만들어서 반환한다.
- 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결과값을 반환해야만 한다.
- 순수하지 않은 작업 ( ex. new Date() , 랜덤숫자 등 ) 은 리듀서 함수 바깥에서 처리해주어야 하며, 이런 순수하지 않은 작업을 처리하기 위해 리덕스 미들웨어를 사용한다.
/* 액션 타입 만들기 */
// 액션 이름 앞에 접두사를 넣는다 (Ducks 패턴)
// 이렇게 하면 다른 모듈과 액션 이름이 중복되는 것을 방지 할 수 있다
const SET_DIFF = 'counter/SET_DIFF';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
/* 액션 생성함수 만들기 */
// 액션 생성함수를 만들고 export 로 내보낸다 (Ducks 패턴)
export const setDiff = diff => ({ type: SET_DIFF, diff });
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
/* 초기 상태 선언 */
const initialState = {
number: 0,
diff: 1
};
/* 리듀서 선언 */
// 리듀서는 export default 로 내보낸다 (Ducks 패턴)
export default function counter(state = initialState, action) {
switch (action.type) {
case SET_DIFF:
return {
...state,
diff: action.diff
};
case INCREASE:
return {
...state,
number: state.number + state.diff
};
case DECREASE:
return {
...state,
number: state.number - state.diff
};
default:
return state;
}
}
- Redux에서 actions, reducers 등의 디렉토리를 나누어 관리를 하게 되면 하나의 기능을 수정하려고 하면, 이 기능과 관련된 여러개의 파일을 수정해야하는 일이 생기고 이러한 불편함을 개선하고자 나온 것이 Ducks 패턴이다.
- Ducks 패턴은 구조중심이 아니라 기능중심으로 파일을 나눈다. 그래서 단일기능을 작성할때나 기능을 수정할 때 하나의 파일만 다루면 되므로 직관적인 코드작성이 가능하다.
- 즉, action type, action생성자 함수, saga, reducer를 하나의 파일에서 관리하는것이다.
- Ducks패턴에서 각각의 액션/액션함수/리듀서를 모아둔 것을 module이라고 부른다
- 지켜야할 점
- reducer는 export default로 내보낸다.
- action 함수는 export로 내보낸다.
- 액션타입을 정의할 때 reducer/ACTION_TYPE형태로 적어줘야 한다. 이렇게 접두사를 붙여주는 이유는 서로다른 리듀서에서 액션이름이 중첩되는것을 방지하기위해서이다.
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter
});
export default rootReducer;
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './modules';
const store = createStore(rootReducer); // 스토어 생성
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
import React from 'react';
function Counter({ number, diff, onIncrease, onDecrease, onSetDiff }) {
const onChange = e => {
// e.target.value 의 타입은 문자열이기 때문에 숫자로 변환
onSetDiff(parseInt(e.target.value, 10));
};
return (
<div>
<h1>{number}</h1>
<div>
<input type="number" value={diff} min="1" onChange={onChange} />
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease}>-</button>
</div>
</div>
);
}
export default Counter;
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Counter from '../components/Counter';
import { increase, decrease, setDiff } from '../modules/counter';
function CounterContainer() {
// useSelector는 리덕스 스토어의 상태를 조회하는 Hook
// state의 값은 store.getState() 함수를 호출했을 때 나타나는 결과물과 동일
const { number, diff } = useSelector(state => ({
number: state.counter.number,
diff: state.counter.diff
}));
// useDispatch 는 리덕스 스토어의 dispatch 를 함수에서 사용 할 수 있게 해주는 Hook
const dispatch = useDispatch();
// 각 액션들을 디스패치하는 함수들을 만든다.
const onIncrease = () => dispatch(increase());
const onDecrease = () => dispatch(decrease());
const onSetDiff = diff => dispatch(setDiff(diff));
return (
<Counter
// 상태와 액션을 디스패치 하는 함수들을 props로 넣어준다.
number={number}
diff={diff}
onIncrease={onIncrease}
onDecrease={onDecrease}
onSetDiff={onSetDiff}
/>
);
}
export default CounterContainer;
- 리덕스를 사용 할 때 프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트를 분리해서 작업을 하는 패턴을 리덕스의 창시자 Dan Abramov가 소개하게 되면서 이렇게 컴포넌트들을 구분지어서 진행하는게 당연시 됐었다.
- 하지만, 이후에 Dan Abramov 또한 2019년에 자신이 썼던 포스트를 수정하게 되면서 꼭 이런 형태로 할 필요는 없다고 명시하였다.
Dan Abramov 의 포스트
import React from 'react';
import CounterContainer from './containers/CounterContainer';
function App() {
return (
<div>
<CounterContainer />
</div>
);
}
export default App;