Redux는 Context API를 기반으로 만들어진 상태 관리 라이브러리로 reducer와 action이라는 개념 또한 존재해 상당히 유사하다. 프로젝트의 규모에 따라 Context API만으로도 충분하다.
Context API와의 차이점
Context API와의 주된 차이점은 미들웨어이다. 미들웨어는 Action 객체가 Reducer에서 처리되기 전에 원하는 작업을 수행할 수 있다. 액션이 무시되게 하거나 액션 발생시 다른 액션을 발생, 서버에 로깅 등을 하는 등 비동기 처리를 할 수 있게 한다.
그리고 useSelector, useDispatch, useStore와 같은 Hooks를 사용할 수 있다. 마지막으로 Context API와는 다르게 모든 글로벌 상태를 하나의 객체에서 관리할 수 있다.
구성
전체적인 구성은 사실 Context API와 크게 다르지 않고, Context API에서는 분산되어 있던 리듀서들을 하나로 묶고 앞서 설명한 것 처럼 미들웨어가 포함된 조금 더 확장된 방식이라고 생각하면 쉽겠다.
리듀서 (Reducer)
function account(state, action){
switch(action.type){
case 'Login':
return {...state, LoggedIn : true}
case 'Logout':
return {...state, LoggedIn : false}
default:
return state; //반드시 default는 state return
}
}
상태에 변화를 일으키는 함수이다. 리듀서는 state와 action을 파라미터로 받는다. action.type에 해당하는 동작을 수행하여 상태를 변화시킨다.
액션 (Action)
export function login(userData) {
return {
type: "Login", //리듀서의 switch case에 해당
userData //리듀서에서 action.userData로 참조
};
}
리듀서의 파라미터로 삽입되는 액션이다. 리덕스에서는 일반적으로 액션 함수를 사용하며 필수적이진 않고, 액션을 발생시킬 때 직접 생성해도 된다.
스토어 (Store)
한 애플리케이션 당 하나의 스토어를 만들고, 현재 앱 상태와 루트 리듀서 및 내장 함수들이 들어있다.
디스패치 (dispatch)
스토어의 내장 함수로 dispatch(action)로 리듀서 함수를 실행 시킬 수 있다.
구독(subscribe)
액션이 dispatch 될 때마다 파라미터로 받은 함수를 실행한다. 일반적으로 useSelector나 connect 함수로 리덕스의 스토어 상태에 구독한다.
규칙
- 하나의 애플리케이션에는 하나의 스토어가 존재한다.
- 상태는 불변성이 유지되어야 한다.
- 똑같은 파라미터로 호출된 리듀서는 항상 똑같은 결과를 반환해야한다. 네트워크를 통한 요청, 날짜 및 시간, 랜덤 숫자 생성 등 값이 변할 수 있는 동작은 반드시 리덕스 미들웨어를 사용해야한다.
Rudux 사용하기
액션과 리듀서를 서로 다른 파일에 정의하는 패턴과 하나에 파일에 몰아서 작성하는 방식이 있다. 후자를 Ducks 패턴이라고 하고 가장 트렌디한 패턴이다.
리듀서 작성
//액션 타입 정의
//DUCKS 패턴 사용시에는 리듀서 이름을 액션에 삽입
const LOGIN = 'account/LOGIN';
const LOGOUT = 'account/LOGOUT';
//액션 생성 함수
export const login = userAccount => ({type:LOGIN, userAccount}); //앞서 선언한 액션명
export const logout = userAccount => ({type:LOGOUT, userAccount)};
//초기 상태, 객체가 아니어도 된다.
const initialState = {
userName : '',
loggedIn : false
}
//리듀서
export default function account(state = initialState, action){
switch(action.type){
case LOGIN:
return {...state, LoggedIn : true}
case LOGOUT:
return {...state, LoggedIn : false}
default:
return state; //반드시 default는 state return
}
}
루트 리듀서 생성
import { combineReducers } from 'redux';
...
const rootReducer = combineReducers({
account,
researcher
});
export default rootReducer;
위의 account라는 리듀서 외에도 researcher라는 리듀서를 다른 파일에 작성했다고 가정한다. 모든 리듀서들을 병합해 루트 리듀서를 생성하고 이를 export 한다.
Provider 생성
//index.js
import {createStore} from 'redux';
..
const store = createStore(rootReducer);
ReactDOM.render(
<Provider store = {store}>
<App />
</Provider>
document.getElementById('root')
);
store를 삽입한 Provider로 APP 전체를 감싸게되면 하위에서 리덕스 스토어에 접근이 가능해진다. 이 또한 Context API 랑 흡사하다.
컴포넌트에서 사용
이제 리덕스의 상태와 액션 함수들을 컴포넌트에서 사용해야하는데, 정석적인 리덕스 사용에는 컴포넌트를 Presentational, Container 컴포넌트 두 가지로 나누곤한다. 19년도에 리덕스 개발자가 반드시 이러한 형태로 할 필요가 없다고 했지만 아직은 정석이라고 하니 숙지해두자. 모든 액션과 상태를 Container 컴포넌트에서 store를 통해 불러온 후 Presentational 컴포넌트의 파라미터로 넘겨주는 패턴이다.
Presentational 컴포넌트
const LoginModal({ userName, loggedIn, login, logout }){
...
}
Container 컴포넌트
const loginModalContainer = () => {
const { userName, loggedIn } = useSelector(state => ({ //반드시 객체를 반환할 필요는 없다
userName : state.account.userName,
loggedIn : state.account.loggedIn
}));
//store의 dispatch를 사용하게 하는 Hook이다.
const dispatch = useDispatch();
const onLogin = () => dispatch(login()); //앞서 리듀서 파일에서 작성했던 액션 함수이다
const onLogout = () => dispatch(logout());
return (
<LoginModal
login={onLogin}
logout={onLogout}
userName={userName}
loggedIn={loggedIn}/>
);
}
이와 같은 패턴으로 컴포넌트를 나누게 되면Presentationl 컴포넌트의 재사용성이 올라간다.
개발자도구
리덕스의 강력한 점은 개발자 도구이다. 스토어의 상태를 개발자 도구에서 조회할 수 있고, 액션들의 디스패치 현황과 어떻게 상태가 변했는지 확인이 가능하다. 여기서 직접 액션을 디스패치 할 수도있다.
$ yarn add redux-devtools-extension
//스토어 생성시 두 번째 파라미터에 다음과 같이 삽입해준다.
const store = createStore(rootReducer, composeWithDevTools());
크롬에서 익스텐션 설치, 프로젝트에 라이브러리 설치, store 생성 구문 변경을 수행하고 나면 개발자 도구 Redux 탭에서 자세한 정보를 볼 수 있다.
useSelector에서 주의할 점
const { userName, loggedIn } = useSelector(state => ({
userName : state.account.userName,
loggedIn : state.account.loggedIn
}));
위와 같이 useSelector 사용시 객체로 가져올 수 있다. 하지만, 상태의 다른 요소가 변경되었을 때 해당 selector가 변경되었는지 알 수 없기때문에 리렌더링이 이루어진다. 이를 해결하기 위해 다음과 같이 할 수 있다.
분할하기
const userName = useSelector(state => state.account.userName);
const loggedIn = useSelector(state => state.account.loggedIn);
shallowEqual
const { userName, loggedIn } = useSelector(state => ({
userName : state.account.userName,
loggedIn : state.account.loggedIn
}), shallowEqual);
객체 안의 가장 겉에 있는 값을 비교하고 true가 나오면 리렌더링을 하지 않고 false가 나오면 리렌더링을 한다.
'React > React Tech' 카테고리의 다른 글
[React] 다국어 변환 라이브러리 i18n (0) | 2022.06.02 |
---|---|
[React] Redux Thunk (미들웨어) (0) | 2022.05.03 |
웹사이트 성능 최적화하기 with LightHouse (0) | 2022.04.13 |
[React] React.Lazy() (Code Splitting) (0) | 2022.04.11 |