사용교재 : 벨로퍼트와 함께하는 모던 리액트
범위 : 6.1 ~ 6.5
노션 용량문제로 Migration
리액트에서 가장 사용률이 높은 상태관리 라이브러리
리덕스 ≒ Context API
+ useReducer
→ 리덕스도 리듀서, 액션 개념 有
리덕스는 미들웨어를 사용하여 액션객체가 리듀서에서 처리되기 전, 원하는 작업 수행
+) 주로 비동기 작업 처리에 사용
Context의 Provider
, 커스텀 Hook
과 비슷한 기능이 내재
connect
함수 : 리덕스 상태, 액션 생성 함수를 컴포넌트의 props
로 받을 수 있음
→ useSelector
useDispatch
useStore
등 과 함께 쓰면 상태조회, 액션 디스패치 가능
→ 실제 상태가 바뀔 때만 리렌더링
↔ Context API는 상태가 바뀌면 Provider 내부 모든 컴포넌트 리렌더링
context는 기능별로 따로 만드는 것이 일반적
↔ 리덕스는 하나의 커다란 상태를 전역적으로 다룸
사용 추천
상태에 변화가 필요할 때 발생시키는 객체
{
type: "TOGGLE_VALUE", // 필수
text: "나머지는 값은 개발자 마음대로"
}
액션을 만드는 함수. 파라미터를 받아서 액션 객체 형태로 만듦.
→ 컴포넌트에서 쉽게 액션을 발생시키기 위함 (필수는 아님)
export function changeInput (text) {
return {
type: "ADD_TODO",
text
};
}
export const changeInput = text => ({
type: "CHANGE_INPUT",
text
});
변화를 일으키는 함수. 상태와 액션을 파라미터로 받음
function counter(state, action) {
switch (action.type) {
case 'INCREASE':
return state + 1;
case 'DECREASE':
return state - 1;
default:
return state;
}
}
(!) useReducer
와 차이 : default
로 state
를 반환. useRedcer
는 에러 처리
리덕스는 여러개의 서브 리듀서와 이를 합친 루트 리듀서의 형태로도 만들 수 있다.
현재 앱의 상태, 리듀서가 내제. 몇가지 내장 함수도 존재.
⬆️스토어의 내장 함수. 액션을 발생시킴 dispatch(액션)
⬆️스토어의 내장 함수. 함수 형태의 값을 파라미터로 받아옴.
→ 액션이 디스패치될 때마다 전달해준 함수 호출
여러 개의 스토어가 가능은 하지만, 권장 X
→ 특정 업데이트가 너무 빈번함 or 특정 부분 완전 분리하기 위해 사용
→ 개발 도구 사용 불가
불변성 유지를 위해
→ shallow equality 검사를 하기 때문
→ 내부 데이터 변경을 감지
→ 얕은 비교로 퍼포먼스 up
(?) 순수함수
$ yarn add redux
import { createStore } from 'redux';
// 리덕스에서 관리 할 상태 정의
const initialState = {
counter: 0,
text: '',
list: []
};
// 액션 타입 정의. 주로 대문자로 작성합니다.
const INCREASE = 'INCREASE';
const DECREASE = 'DECREASE';
const CHANGE_TEXT = 'CHANGE_TEXT';
const ADD_TO_LIST = 'ADD_TO_LIST';
// 액션 생성함수 정의. 주로 camelCase 로 작성합니다.
function increase() {
return {
type: INCREASE // 액션 객체에는 type 값이 필수입니다.
};
}
const decrease = () => ({
type: DECREASE
});
const changeText = text => ({
type: CHANGE_TEXT,
text // 액션안에는 type 외에 추가적인 필드를 마음대로 넣을 수 있습니다.
});
const addToList = item => ({
type: ADD_TO_LIST,
item
});
// 리듀서 만들기
// 액션 생성함수로 만든 객체들을 새로운 상태를 만드는 함수 (불변성 필수)
function reducer(state = initialState, action) {
switch (action.type) {
case INCREASE:
return {
...state,
counter: state.counter + 1
};
case DECREASE:
return {
...state,
counter: state.counter - 1
};
case CHANGE_TEXT:
return {
...state,
text: action.text
};
case ADD_TO_LIST:
return {
...state,
list: state.list.concat(action.item)
};
default:
return state;
}
}
// 스토어 만들기
const store = createStore(reducer);
console.log(store.getState());
// getState() : 현재 store 상태를 조회하는 store 내장함수
// 스토어안에 들어있는 상태가 바뀔 때 마다 호출되는 listener 함수
const listener = () => {
const state = store.getState();
console.log(state);
};
const unsubscribe = store.subscribe(listener);
// 구독을 해제하고 싶을 때는 unsubscribe() 를 호출하면 됩니다.
(?) 리덕스 모듈
가 포함된 JS파일
→ 각각 다른 파일에 분리가능(액션 / 리듀서)
↔ 하나로 뭉쳐서 쓰는 스타일 : Ducks
패턴
/* 액션 타입 만들기 */
// Ducks 패턴을 따를땐 액션의 이름에 접두사를 넣어주세요.
// 이렇게 하면 다른 모듈과 액션 이름이 중복되는 것을 방지 할 수 있습니다.
const SET_DIFF = 'counter/SET_DIFF';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';
/* 액션 생성함수 만들기 */
// 액션 생성함수를 만들고 export 키워드를 사용해서 내보내주세요.
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 로 내보내주세요.
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;
}
}
/* 액션 타입 선언 */
const ADD_TODO = 'todos/ADD_TODO';
const TOGGLE_TODO = 'todos/TOGGLE_TODO';
/* 액션 생성함수 선언 */
let nextId = 1; // todo 데이터에서 사용 할 고유 id
export const addTodo = text => ({
type: ADD_TODO,
todo: {
id: nextId++, // 새 항목을 추가하고 nextId 값에 1을 더해줍니다.
text
}
});
export const toggleTodo = id => ({
type: TOGGLE_TODO,
id
});
/* 초기 상태 선언 */
// 리듀서의 초기 상태는 꼭 객체타입일 필요 없습니다. 원시 타입도 상관 없습니다.
const initialState = [
/* 우리는 다음과 같이 구성된 객체를 이 배열 안에 넣을 것입니다.
{
id: 1,
text: '예시',
done: false
}
*/
];
export default function todos(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return state.concat(action.todo);
case TOGGLE_TODO:
return state.map(
todo =>
todo.id === action.id // id 가 일치하면
? { ...todo, done: !todo.done } // done 값을 반전시키고
: todo // 아니라면 그대로 둠
);
default:
return state;
}
}
한 프로젝트의 여러개의 리듀서를 합친 것
combineReducer
내장 함수 사용
import { combineReducers } from 'redux';
import counter from './counter';
import todos from './todos';
const rootReducer = combineReducers({
counter,
todos
});
export default rootReducer;
// index.js
const store = createStore(rootReducer); // 스토어를 만듭니다.
react-redux
라이브러리 사용
→ index.js
에서 Provider
컴포넌트로 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')
);
리덕스 스토어에 직접 접근X
→ 필요한 값, 함수를 props
로만 받아와서 사용하는 컴포넌트
function Counter({ number, diff, onIncrease, onDecrease, onSetDiff }) {
const onChange = e => {
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;
리덕스 스토어의 상태 조회, 액션 디스패치를 할 수 있는 컴포넌트
프리젠테이셔널 컴포넌트를 사용 ( HTML 태그 사용X )
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
// 상태
number={number}
diff={diff}
// 액션 디스패치 함수
onIncrease={onIncrease}
onDecrease={onDecrease}
onSetDiff={onSetDiff}
/>
);
}
export default CounterContainer;
// App.js
import CounterContainer from './containers/CounterContainer';
function App() {
return (
<div>
<CounterContainer />
</div>
);
}
export default App;
프리젠테이셔널 / 컨테이너 컴포넌트 구분 짓는 것이 전통적인 대세
→ 꼭 따를 필요는 없음
useSelector
로 리덕스 스토어의 상태를 조회하면, 상태가 바뀌지 않았을 때는 리렌더링 X
→ 해당 값이 바뀌었을 때만 컴포넌트 리렌더링
// 리렌더링X
const { number, diff } = useSelector(state => ({
number: state.counter.number,
diff: state.counter.diff
}));
// 리렌더링
const number = useSelector(state => state.counter.number);
const diff = useSelector(state => state.counter.diff);
shallowEqul
useSelector
의 두번째 인자(equalityFn
)로 react-redux함수인 shallowEqual
전달
→ equalityFn?: (left: any, right: any) => boolean
→ 이전 값과 다음 값을 비교
→ true
리렌더링X, false
리렌더링
컨테이너 컴포넌트를 만드는 방법 중 하나. (HOC
)
→ 잘 안 쓰임. 클래스 컴포넌트에서 쓰임 (훅을 못 써서)
→ useSelector
, useDispatch
가 더 쉬움
컴포넌트를 특정 함수로 감싸서 특정 값 또는 함수를 props
로 받아와서 사용가능하게 하는 패턴
리액트 컴포넌트 개발 패턴 중 하나
→ 로직 재활용에 유리
→ 특정 함수, 값을 props
로 받아서 사용할 때 쓰임
→ hook을 쓰기 전에 자주 사용
→ recompose 라이브러리 사용 추천
connect(mapStateToProps, mapDispatchToProps)
mapStateToProps(state)
: 컴포넌트에 props로 넣어줄 리덕스 스토어 상태에 관련된 함수state
: 현재 상태mapDispatchToProps(dispatch)
: 컴포넌트에 props로 넣어줄 액션을 디스패치하는 함수들에 관련된 함수mapDispatchToProps(_, ownProps)
의 ownProps
props
를 가리킴. 생략 가능connect(_, _, mergeProps, options)
mergeProps
: 컴포넌트가 실제로 전달 받을 props
를 정의. 생략가능options
: 컨테이너 컴포넌트가 어떻게 동작할 지에 대한 옵션. 생략가능