-
React관련 싹 훑기(3) - Redux 의 개념Web Dev/3. React 관련 2021. 5. 21. 00:58728x90
이때까지 Redux도 쓰기도하고, context도 쓰기도 했고, 완전 전체로 다 state로 관리하면서 props drilling을 해가면서 실사용되는 프로젝트를 다 굴려봤다. 근데 아주 다 하나같이 속이 터질뻔한 경험을 했어서 이번에는 State쪽을 Deep Dive 해보려고 한다.
Redux
https://frontendmasters.com/courses/redux-mobx/introduction/
회사에서만 Redux를 썼었고, 개인 프로젝트를 할때는 한동안 안썼어서 이번기회에 좀 복습을 하려고 이 세미나를 봤다.
- 강의 슬라이드
https://static.frontendmasters.com/resources/2019-05-28-react-state/redux-mobx.pdf
Redux without React
The whole state tree of your application is kept in one store
Redux는 state가 한곳에 저장되어있는 것이다. 그리고 얘는 JavaScript 객체라 JSON.stringify() 같은 것을 통해서 저장도 해놓고, user쪽에서 에러가 났을때, 해당 상태를 저장했다가 debugging 단계해서 다시 JSON.parse()를 해서 복구를 시킬수도 있다.
그런데 Redux에서는 이 JavaScript Object를 그냥 modify할 수 있는 것은 아니고, action을 dispatch해서 상태를 업데이트한다. 이때 어떻게 상태를 업데이트할지를 reducer에 정의한다(reducersms action과 새로운 상태값을 받고 결과를 반환하는 pure function)
Redux의 용어 개념 잡기
Redux자체는 어떤 JavaScript Apps에서도 사용하다. Angular도 좋고. react-redux가 단지 redux를 react를 쓰기 좋게 만들어 둔것. 자주 나오는 용어/개념을 아래와 같이 간략하게 정리했다.
- redux의 state는 JavaScript object
앱에 아래와 같은 state가 있다고 해보자.
{ todos: [{ text: 'Eat food', completed: true }, { text: 'Exercise', completed: false }], visibilityFilter: 'SHOW_COMPLETED' }
- action과 dispatch, actionHelper
이런 앱의 상태를 변경하기 위해서 action이 발생한다. 이때 action도 JavaScript Object이고 아래와 같이 생겨있다.
{ type: 'ADD_TODO', payload: { text: 'Do the laundry'}}
이런 action을 발생시키는걸 dispatch라고 한다. (dispatch가 한국어로 보내다라는 뜻을 가지고 있다)
그래서 무슨 버튼 같은 것을 눌렀을때 저렇게 생겨먹은 객체를 dispatch를 해줘야하는데, 보다시피 누가봐도 에러내기 딱 좋게 생겼다. 그래서 helper함수처럼 만들어서
function addTodo(value){ store.dispatch({ type: 'ADD_TODO', payload: { text: value } }) }
이렇게 함수로 다루게 하는것이다.
- Reducer란
reducer는 대강 단어 느낌이 뭐를 해소해주는 그런 느낌인데, 저런 action이 발생했을 때 뭘하란건지 정의해두는 걸 말한다. reducer는 상태 하나를 해결하는 단위로 만들고, 얘를 combine해서 쓴다. (combineReducer)
Redux API
https://redux.js.org/api/api-reference
API를 상세하게 좀더 살펴보자면 createStore, combineReducers, applyMiddleware, bindActionCreators, compose 정도가 된다.
- compose
compose의 역할은 주어진 함수를 오른쪽에서 부터 왼쪽으로 합성해주는 역할이다. 오른쪽에서 왼쪽으로 합성하기 때문에 실제로는 왼쪽의 함수가 가장 먼저 적용된다.
// compose const makeLouder = (string) => string.toUpperCase(); const repeatThreeTimes = (string) => string.repeat(3); const embolden = (string) => string.bold(); const f = compose(embolden, repeatThreeTimes, makeLouder); console.log(f("hello")); // 출력 결과: "<b>HELLOHELLOHELLO</b>"
Middleware를 합성할 때 쓰인다.
- store
store는 앱의 state 트리를 가지고 있는 곳이다. 이 store의 state를 바꾸기 위해서는 action을 dispatch해야한다. 이 store는 class도 아니고, object에 몇개의 method가 있는것 뿐이다. 생성하는 방법은 createStore에 reducer함수를넘겨주는 것이다.
const reducer = (state = {value: 1}, action) => { if(action.type === "ADD"){ const value = state.value; const amount = action.payload.amount; return { value: value + amount } } return state; } // createStore const store = createStore(reducer);
이렇게 store를 생성할 수 있다. 이때 생성된 store에는 내가지 메소드가 존재한다.
- getState()
- 상태를 보여주는 reducer
- dispatch(action)
- action을 넘겨서 dispatch를 할수 있게 해준다. 얘를 통해서만 state 업데이트를 하루 시있다.
- subscribe(listener)
- state에 업데이트가 생기면 실행된다. 이 subscribe method는 unsubscribe를 할 수 있는 함수를 반환해준다.
const unsubscribe = store.subscribe(() => {console.log("obj", store.getState())}); // unsubscribe를 반환 // dispatch를 하면 subscribe에 넘겨준 콜백함수가 실행된다. // 이 이벤트 리스너를 해제할 수 있는 함수를 subscribe에서 반환해주기때문에 얘로 이벤트를 지울 수 있다.
- replaceReducer(nextReducer)
- Reducer를 업데이트 하는데 쓰인다.
- combineReducers
const calcReducer = (state = {value: 1}, action) => { console.log("dispatched", action) if(action.type === "ADD"){ const value = state.value; const amount = action.payload.amount; return { value: value + amount } } return state; } const reducer2 = combineReducers({calculator: calcReducer});
reducer가 여러개인 경우, combineReducers에 객체 형태로 reducer를 넘겨 줄 수 있다.
- bindActionCreators
const handrolledDispatch = store.dispatch(createAddAction(4)); const dispatchAdd = bindActionCreator(createAddAction, store.dispatch); //
이 두 함수가 같다. dispatch 를 wrap 해주는 용도이다.
- applyMiddleware
MiddleWare는 비동기처리등을 하거나, 로깅같은것을 할때 쓰이는데 여기서는 간략하게 사용법 위주로 봤다. ㅊ
const logger = ({getState} => { return next => action => { console.log("MIDDLEWARE", getState(), action); const value = next(action); // 다음으로 넘겨주는것 return value; } } }) const secondStore = createStore(reducer, applyMiddleware(logger));
next, action 관련해서는 아직 와닿지가 않아서 이 글을 참고 했다.
https://www.codementor.io/@vkarpov/beginner-s-guide-to-redux-middleware-du107uyud
The redux middleware syntax is a mouthful: a middleware function is a function that returns a function that returns a function. The first function takes the store as a parameter, the second takes a next function as a parameter, and the third takes the action dispatched as a parameter. The store and action parameters are the current redux store and the action dispatched, respectively. The real magic is the next() function. The next() function is what you call to say "this middleware is done executing, pass this action to the next middleware". In other words, middleware can be asynchronous.
위의 글에서 따온 부분인데, 미들웨어는 함수를 반환하는 함수를 반환하는 함수이다.
1. 첫번째 함수는 store를 받는다.
2. 두번째 함수는 next함수를 받고
3. 세번재 함수는 action을 받는다.
store랑 action은 현재 redux store의 파라미터이다. 놀라운건 next()인데, "미들웨어 동작이 끝났으니, 액션을 다음 미들웨어로 보내세요" 라는 동작을 처리해준다. 이걸 이용해서 비동기 처리를 할 수 있게 되는 것.
Redux에서 reducer에서 왜 state를 직접 변경하면 안되는지..
// 이전 state랑 다른 새로운 객체를 아예 만들어서 반환 const reducer1 = (state = {value: 1}, action) => { if(action.type === "ADD"){ const value = state.value; const amount = action.payload.amount; return { value: value + amount } } return state; } // state자체를 변경 const reducer2 = (state = {value: 1}, action) => { if(action.type === "ADD"){ const value = state.value; const amount = action.payload.amount; state.value = value + amount } return state; } // 스토어 생성하고 각각에 대해서 action처리 const prev = store.getStore() // action을 dispatch const next = store.getStore() // reducer1으로 한경우 console.log(prev === next) // false 반환 // reducer2으로 한경우 console.log(prev === next) // true 반환
reducer내에서 state를 변경할때는 반드시 새로운 객체를 만들어내서 deepCopy를 해야한다. reducer2처럼 reducer를 정의하면 state자체는 바뀌지만 기존이랑 동일한 state이기 때문에, 앱에서 값이 변했는지 뭐어쨌는지 알 방법이 없다. 그렇기때문에 항상 값을 제대로 복사해서 만들어야하는 것.
공부하면서 막따라한 코드. 100프로의 확률로 오타가 있다.
// 공부하면서 마구 작성한 코드, 아마 오타 있음 const { createStore, combineReducers, compose, bindActionCreators, applyMiddleware } = Redux; // compose const makeLouder = (string) => string.toUpperCase(); const repeatThreeTimes = (string) => string.repeat(3); const embolden = (string) => string.bold(); const f = compose(embolden, repeatThreeTimes, makeLouder); console.log(f("hello")); const reducer = (state = {value: 1}, action) => { console.log("dispatched", action) if(action.type === "ADD"){ const value = state.value; const amount = action.payload.amount; // return { value: value + amount } -> 이전 state랑 다른 새로운 객체를 아예 만듬 return { value: value + amount } // state.value = value + amount } return state; } // createStore const store = createStore(reducer); // ["dispatch", "subscribe", "getState", "replaceReducer"] console.log(Object.keys(store)); const unsubscribe = store.subscribe(() => {console.log("obj", store.getState())}); // unsubscribe를 반환 const first = store.getState(); store.dispatch({ type: "ADD", payload: { amount: 2 }}) store.dispatch({ type: "ADD", payload: { amount: 2 }}) unsubscribe(); store.dispatch({ type: "ADD", payload: { amount: 2 }}) const second = store.getState(); console.log(first === second) // 달라야함 // combine reducer: reducer합치는것. const calcReducer = (state = {value: 1}, action) => { console.log("dispatched", action) if(action.type === "ADD"){ const value = state.value; const amount = action.payload.amount; // return { value: value + amount } -> 이전 state랑 다른 새로운 객체를 아예 만듬 return { value: value + amount } // state.value = value + amount } return state; } const reducer2 = combineReducers({calculator: calcReducer}); console.log("reducer2", reducer2); const initial2 = { calculator: 0, error: 'No Error' } const addAction = { type: 'ADD', amount: 4 } const createAddAction = amount => { return { type: 'ADD', payload: { amount } } } const handrolledDispatch = store.dispatch(createAddAction(4)); const dispatchAdd = bindActionCreator(createAddAction, store.dispatch); // console.log(store.getState()); // 만들면 이렇게 생김 const bindActionCreators = (actions, dispatch) => { return Object.keys(actions).reduce((boundActions, key) =>{ boundActions[key] = bindActionCreator(actions[key], dispatch) }, {}) } const logger = ({getState} => { return next => action => { console.log("MIDDLEWARE", getState(), action); const value = next(action); // 다음으로 넘겨주는것 return value; } } }) const secondStore = createStore(reducer, applyMiddleware(logger)); // const initialState = { result: 0 }; // const addAction = { // type: 'ADD', // value: 4 // }; // const calculatorReducer = ( // state = initialState, // action // ) => { // if (action.type === 'ADD') { // return { // ...state, // result: state.result + action.value // } // } // return state; // } // const subscriber = () => { // console.log('SUBSCRIPTION!!!!', store.getState()) // }; // const logger = ({ getState }) => { // return next => action => { // console.log( // 'MIDDLEWARE', // getState(), // action // ); // const value = next(action); // console.log({value}); // return value; // } // } // const initialError = { message: '' }; // let errorMessageReducer = ( // state = initialError, // action // ) => { // if (action.type === 'SET_ERROR_MESSAGE') // return { message: action.message }; // if (action.type === 'CLEAR_ERROR_MESSAGE') // return { message: '' }; // return state; // }; // const store = createStore(combineReducers({ // calculator: calculatorReducer, // error: errorMessageReducer // }), {}, applyMiddleware(logger)); // const unsubscribe = store.subscribe(subscriber); // const add = value => ({ type: 'ADD', value }); // const setError = (message) => ( // { type: 'SET_ERROR_MESSAGE', message} // ); // const clearError = () => ( // { type: 'CLEAR_ERROR_MESSAGE' } // ); // const bindActionCreator = // (action, dispatch) => // (...args) => dispatch(action(...args)); // const addValue = bindActionCreator(add, store.dispatch); // const bindActionCreatorz = (actions, dispatch) => { // return Object.keys(actions).reduce((boundActions, key) => { // boundActions[key] = bindActionCreator(actions[key], dispatch); // return boundActions; // }, {}); // } // const errors = bindActionCreatorz({ // set: setError, // clear: clearError // }, store.dispatch);
후기
React랑 따로 Redux자체만을 한번 봤는데, 정말 Redux가 기능이 정말 최소한이라더니 진짜 그렇다. 문서도 굉장히 잘되있는것 같아서 다음에 다시한번 읽어봐야겠다.
'Web Dev > 3. React 관련' 카테고리의 다른 글
React관련 싹 훑기(4) - Redux Subscriber는 어떻게 동작하는가 (0) 2021.05.25 Next.js Link컴포넌트에 className 적용하기 (0) 2021.05.23 React관련 싹 훑기(2) - Intermediate react 세미나 듣고.. (0) 2021.05.20 hydration (0) 2021.05.19 lazy, Suspense (0) 2021.05.19 - getState()