Redux의 구조를 간략하게 다시 정리해보면,
<3가지 기본 원칙>
❗️ React를 사용하면서 props로 넘겨주어야 하는 값들이 많아지고, 최상단으로 state를 계속 끌어줘야할 때 큰 불편함을 느꼈었다. 이럴 때 redux를 사용하면 전역스토어에 상태 값을 저장하고 가져올 수 있기 때문에 조금 더 유연하게? 편리하게 state를 관리하고 사용할 수 있다.
리덕스를 사용하지 않았을 때,
상태를 계속 최상단으로 끌어주어야만 형제 컴포넌트끼리 상태를 공유할 수 있었음.
리덕스는 전역 스토어가 존재하기 때문에 어느 컴포넌트에서든 스토어를 통해 state를 저장/사용할 수 있다.
store의 대략적인 코드 구조는 아래와 같다.
const initialState = {
value: 0, // state가 저장되는 공간, 모든 데이터는 이 한 곳에 저장된다
}; // 처음에는 이 하나의 객체에 모든 데이터가 저장된다는 것이 뭔가 신기했었다
function addReducer(state = initialState, action) {
switch(action.type) {
case 'INCREMENT': // 다음과 같은 액션이 실행 되어지면 해당 케이스가 실행되고, 이전 state 값을 통해 새로운 값을 반환한다
return { ...state, value: state.value + 1 };
case 'DECREASE':
return { ...state, value: state.value - 1 };
default:
return state;
}
}
export default addReducer;
리덕스는 단방향 데이터 흐름을 가지고 있고,
진행되는 방식은 아래와 같다.
- 액션을 스토어에 전달하고,
- 리듀서를 실행,
- 새로운 State 데이터를 Store에 반영,
- Store Subscriber에게 알림,
- 새로운 정보와 함께 컴포넌트를 다시 리렌더링
액션은 어플리케이션 상태에 영향을 미치는 사건을 표현/묘사하는 객체이다.
Store에 존재하는 dispatch라는 메소드를 이용하여 액션을 실행시킬 수 있다.
( 리듀서에 해당 케이스(action type)를 실행시킬거야~ 하고 표현해주는 느낌이다. )
액션을 실행시키는 코드 예시)
const dispatch = useDispatch(); // dispatch에 대한 설명은 아래에 있다.
// 간략하게 설명하자면, 리액트에도 훅이 있듯이 리덕스에도 훅이 존재하는데 useDispatch가 그 중 하나이다.
dispatch({
type: 'INCREMENT', // "액션의 종류를 한번에 식별할 수 있는 문자열"
payload: 5 // "액션의 실행에 필요한 임의의 데이터"
});
// type은 reducer에서 실행 시킬 케이스를 적어준다.
// 전달 할 data가 있다면 payload로 전달하고,
// 전달 할 data가 없다면 type만 적어주면 된다.
스토어는 어플리케이션의 State를 보관하는 곳이다.
redux는 모든 state가 하나의 store에서 관리된다 -> SSOT(Sinlge source of truth)
위 코드에선 아래 부분에 해당된다.
const initialState = {
value: 0, // state가 저장되는 공간, 모든 데이터는 이 한 곳에 저장된다
};
createStore(reducer)로 Redux store를 생성할 수 있다.
import { createStore } from 'redux'
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([ action.text ]);
default:
return state;
}
}
let store = createStore(todos, [ 'Use Redux' ]);
store.dispatch({
type: 'ADD_TODO',
text: 'Read the docs'
});
<store의 주요 메서드>
<Redux hooks의 2가지>
- useSelector, useDispatch
- 스토어의 메서드 getState와 hooks의 useSelector 차이?
이전 테스트를 진행하면서 아무 생각없이 useSelector가 아닌 getState를 사용한 적이 있는데 이 차이도 정리해보고자 한다.
이 전에 리액트에서 리덕스를 사용하기 위해서는 connect()
로 컴포넌트를 감싸고 mapStateToProps
와 mapToDispatchToProps
를 작성해야 리덕스 스토어에 접근을 할 수 있었지만,
현재는 리덕스에서 hooks를 지원하게 되면서 이러한 작업 없이 간결하고 코드 재사용성에 유리하게 코드를 작성할 수 있게 되었다.
그 훅이 바로 useSelector()
와 useDispatch()
이다.
리덕스 스토어의 데이터를 추출할 수 있는 리덕스의 훅 중 하나이며
문법적으로는 useSelector(함수, === 비교 함수)
로
첫 번째 인자에는 Selector 함수가 들어가는데 리덕스 스토어의 전체 상태를 인자로 받는다.
import React from 'react'
import { useSelector } from 'react-redux'
export const CounterComponent = () => {
const counter = useSelector((state) => state.counter);
return <div>{counter}</div>
};
리덕스 스토어로부터 전달 받은 dispatch 함수를 참조하는 값을 반환한다
const dispatch = useDispatch(); // useDispatch를 dispatch로 선언
dispatch({
type: 'INCREMENT', // "액션의 종류를 한번에 식별할 수 있는 문자열"
payload: 5 // "액션의 실행에 필요한 임의의 데이터"
});
👉 또 store는 4가지의 메서드가 들어있는 객체인데 그 중 하나인 getState()
와 redux hook의 useSelector()
의 차이는,
-> 즉, getState()
는 현재 상태를 반환하는 함수이며, 스토어의 리듀서가 마지막으로 반환한 값과 동일하다. store에 state가 변경되어도 자동으로 업데이트 되지 않는다.
반대로 useSelector()
는 이전 결과와의 비교를 통해 결과가 같을 경우 다시 렌더링하지 않는다는 큰 장점을 가진 훅이다.
외부에서 들어온 액션은 스토어의 상태(state)와 함께 리듀서에 전달되고,
스토어는 리듀서로부터 새로운 상태를 받는다.
그래서 리듀서는 이전 상태와 액션을 받아 새로운 상태를 반환하는 순수함수로 정의한다.
-> 이전 상태와 Action을 합쳐, 새로운 state를 만듦.
-> 반환된 state는 store에 바로 반영됨.
즉, 액션(Dispatch 메소드를 이용하여 Action을 발생시킴) -> 리듀서(currentState) -> 새로운 상태 값(nextState) 반환
function addReducer(state = initialState, action) {
switch(action.type) {
case 'INCREMENT': // 다음과 같은 액션이 실행되어지면 해당 케이스가 실행되고, 새로운 값을 반환한다
return { ...state, value: state.value + 1 };
case 'DECREASE':
return { ...state, value: state.value - 1 };
default:
return state;
}
}
export default addReducer;
그렇다면, 아무 생각 없이 써왔던 Redux의 내부는 어떻게 이루어져 있을까?🤔
말 그대로 앱의 상태 트리 전체를 보관하는 Redux 스토어를 만드는 것이다.
앱 내에는 단 하나의 스토어만 있어야 한다!
Redux는 액션을 실행하면 바로바로 적용되는 동기적인 동작 밖에 못한다.
-> 따라서 바로바로 실행되기 때문에 특정시간, 특정 동작 이후에 액션을 실행할 수 없다.
-> Redux action 사이사이에 비동기 요청이 들어갈 수 있게 확장을 해줘야 한다.
-> 미들웨어를 사용
-> 대표적인 saga, thunk, observer 등으로 이를 확장해 줄 수 있다.
dispatching 과정에서 액션이 리듀서에 도달하기 전 애플리케이션의 로직이 끼어들 수 있는 틈을 만들어주는 것이다.
-> 즉, 외부에서 생성된 액션이 리듀서에 도달하기전에 먼저 수신하여 시스템에 알맞게 특정 작업들을 미리 처리할 수 있도록 해주는 것.
여기서 특정 작업이란, 액션을 검증하거나 필터링, 모니터링, 외부 API와의 연동, 비동기 처리 등을 추가적으로 수행하는 부분들을 말한다.
이러한 특정 작업을 통해서 더 강력한 dispatch를 만들 수 있다고 한다.
그림으로 보면 아래와 같다.
<미들웨어의 위치>
<미들웨어를 통해 강력한 dispatch 생성 과정>
이미지 출처
<2가지의 dispatch>
그리고 이러한 고차 함수는 아래에 compose를 활용해 구현할 수 있따.
getState
, dispatch
}를 인자로 받고,next
라는 체이닝 함수를 전달받는 새로운 wrapDispatch
함수를 반환한다.-> 미들웨어는 3개의 중첩된 함수를 구현하고 있어야 한다.
예시는 아래와 같다..
function middleware({getState, dispatch}}) {
return function wrapDispatch(next) {
return function dispatchToSomething(action) {
// do something...
return next(action);
}
}
}
// arrow-syntax로 더 간략하게
const middleware = ({getState, dispatch}) => next => action => { // getState, dispatch 를 인자로 받고,
// do something...
return next(action);
}
미들웨어가 하나가 아니라면?
스토어에 여러 미들웨어가 중첩되어 있을 때 바로 다음 미들웨어의 dispatching 함수에 진입하기 위해 필요한 함수가 바로 위의 next
함수이고, 그림으로 표현하면 아래와 같다.
createStore
자체를 감싸는 고차 함수다.
applyMiddleware와 middleware는 정확히 구분해야 하는데
applyMiddleware >>> middleware
applyMiddleware
: 미들웨어 자체보다 더 강력한 확장 메커니즘이다.
함수를 오른쪽에서 왼쪽으로 조합합니다.
이것은 함수형 프로그래밍 유틸리티로, Redux에는 편리함을 위해 포함되었습니다.
여러 스토어 인핸서들을 순차적으로 적용하기 위해 사용할 수 있습니다.
인수
(arguments): 조합할 함수들입니다.
각각의 함수는 하나의 인자를 받아야 합니다.
함수의 반환값은 왼쪽에 있는 함수의 인수로 제공되는 식으로 연속됩니다.
예외는 가장 오른쪽에 있는 인수로, 여러 개의 인자를 받을 수 있으며 조합된 함수의 시그니처는 이를 따릅니다.
반환
(Function): 오른쪽에서 왼쪽으로 조합된 최종 함수입니다.
import { createStore, combineReducers, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers/index'
const store = createStore(
reducer,
compose( // ✅
applyMiddleware(thunk),
DevTools.instrument()
)
)
compose가 하는 일은 결국 깊이 중첩된 함수 변환을 길게 늘어진 코드 없이 작성하게 해주는 것이라고 한다.
처음 위에서 리덕스의 구조를 설명하였을 때에 reducer가 하나였지만, reducer가
여러 개가 된다면 어떻게 될까?
-> 여러 개의 reducer를 지정했다면 redux의 combineReducer로 reducer를 통합시킬 수 있다.
아직 더 정리중..