redux란?
리덕스는 react 라이브러리 없이 구현 가능한 라이브러리이다.
그럼 상태란 무엇일까?
리액트에서 상태, 즉 State는 웹 페이지에서 제공되는 기능이다. 다음의 사진을 보자
위의 사진에서 '장바구니에 담기' 버튼을 클릭하면 선택한 물건이 장바구니 페이지에 담기게 되는데 이를 JS 동작 시퀀스로 풀어보면
버튼이 클릭되면 버튼에 구현되어있던 이벤트 함수가 발동되고
이벤트 함수는 선언해놓았던 state 초기값을 변경시키게 되고
이 변경된 state값으로 기본 app화면에 장바구니에 몇 개의 항목이 담겼는지 갯수가 표시되고
다시 장바구니 탭에 선택 항목들이 담기게 된다.
상태 관리란 이런 기능들 ( === state ) 을 개발자 입장에서 직관적이고 수월하게 구현 혹은 수정할 수 있게 작업하는 것을 의미한다.
위에서 언급했듯이 REDUX는 용이한 상태 관리를 위해 만들어진 라이브러리라고 했고 React의 기존 상태 관리를 보완할 수 있다.
redux가 등장한 배경
리액트는 부모 컴포넌트에서 내려준 props를 통해 자식 컴포넌트들의 상태를 변경한다.
위의 예시를 조금 더 풀어보자.
이를 해결하고자 Redux가 등장했다.
redux 특징
하나의 소스를 가진다. 즉, 모든 컴포넌트는 동일한 곳에서만 제어할 수 있다. 이 하나의 소스는 후에 서술할 store가 되겠다.
state는 읽기 전용이다. 이 말은 즉슨 state값은 직접적으로 변경하지 못한다는 말이다.
( state에 값을 다이렉트로 할당해서 변경시키지 못한다 )
React에서는 state 값을 변경하기 위해 SetState를 사용했다. Redux도 비슷하게 action이라는 객체를 통해서만 가능하다.
변화는 순수 함수에 의해서만 일어난다.
즉, Side Effect를 가지고 있는 함수로는 redux를 구현할 수 없게 된다. 리듀서와 연결되는 개념이다
** 순수함수란 ? 함수 내에 인풋을 주었을 때 외부 영향에 따라 값이 변하지 않는 함수
** 부수 효과 ( Side Effect ) --- 함수 내부의 값이 외부에 영향을 주는 것. 혹은 함수 외부의 값이 내부에 영향을 미치는 것.
ex ---- 지역 변수가 전역 변수에 영향을 끼치는 것
redux 구조
{
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId
}
}
액션은 반드시 type 필드를 가지고 있어야 하며, 그 외의 값은 상황에 따라 넣어줄 수 있다.
const addToCart = (itemId) => {
return {
type: ADD_TO_CART,
payload: {
quantity: 1,
itemId
}
}
}
const initialState =
{
"items": [
{
"id": 1,
"name": "노른자 분리기",
"img": "../images/egg.png",
"price": 9900
}]}
const itemReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TO_CART:
//TODO
return Object.assign({}, state, {
cartItems: [...state.cartItems, action.payload]
})
default:
return state;
}
}
리듀서는 현재의 State 와 Action 을 인자로 받아 Store 에 접근해 Action 에 맞게 State 를 변경한다.
import { compose, createStore, applyMiddleware } from "redux";
import rootReducer from '../reducers/index';
import thunk from "redux-thunk";
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
: compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));
export default store;
const dispatch = useDispatch()
const handleClick = (item) => {
if (!cartItems.map((el) => el.itemId).includes(item.id)) {
dispatch(addToCart(item.id))
dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`))
}
else {
dispatch(notify('이미 추가된 상품입니다.'))
}
}
dispatch(action) 식으로 Action 을 인자로 리듀서 함수에게 전달한다.
이렇게 호출을 하면 리듀서 함수가 스토어에 전달된다.
정리하면 다음과 같다.
Action(액션) 객체가 dispatch() 메소드에 전달된다.
dispatch(액션)를 통해 Reducer를 호출한다.
Reducer는 새로운 State를 생성한다.
위의 사진의 work flow를 살펴보자.
먼저 root reducer 함수를 사용하여 만들어진 리덕스 스토어가 있다.
스토어는 root reducer를 한번 호출하고 리턴 값을 초기 상태로 저장한다.
UI가 처음 렌더링될 때, UI 컴포넌트는 리덕스 스토어의 상태에 접근하여 그것을 렌더링에 활용한다.
또한 그것들은 후에 상태의 변화가 업데이트 되는 것을 구독한다.
유저가 버튼을 클릭한다.
앱은 유저의 행동에 맞는 디스패치를 실행해 액션을 일으킨다.
스토어는 이전 상태와 현재 액션으로 리듀서 함수를 실행하고, 그 리턴 값을 새로운 상태로 저장한다.
스토어는 스토어를 구독하고 있던 UI들에게 업데이트 되었다고 알려준다.
스토어의 데이터가 필요한 각각의 UI들은 필요한 상태가 업데이트 되었는지 확인한다.
데이터가 변경된 각 구성요소는 새 데이터로 강제로 다시 렌더링하므로 화면에 표시되는 내용을 업데이트 할 수 있다.