Redux는 React Native 공부를 하면서 채용조건을 찾아볼 때마다 자주 보였던 녀석이다. 하지만 적용해야 할 필요성을 느끼지 못했기에 여태 구현을 해오다가 상태 관리를 조금 더 효율적으로 관리할 필요가 생기면서 찾아보게 되었다. 처음엔 꽤 난잡한데 이해하기 위해서는 큰 그림을 볼 필요가 있었으며, 손 코딩을 하는 것이 큰 도움이 되었다. 그럼 이번 게시글에서는 redux-toolkit을 어떻게 적용하였는지에 대해 간략히 적어보려고 한다.
(참고로 이 게시글에서는 Redux가 아닌 Redux-toolkit에 대해 다룰 것이기 때문에 Redux에 대한 기본 지식이 습득된 상태로 볼 것을 추천한다😉)
공홈 링크는 다음과 같고, 설치 방법은 생략한다.
https://redux-toolkit.js.org/
큰 그림은 이러하다 !
- 앱이 Redux Store에 접근하기 위하여 Provider로 전체를 감싸준다.
- store안에 모든 reducer를 합쳐주는 RootReducer와 추가적인 미들웨어를 넣어준다.
- RootReducer에서 필요한 reducer에 대한 처리를 해준다.
- 각각의 reducer들과 초기값, 이름이 선언될 Slice를 선언해준다.
사실상 이게 다인데.. 거꾸로 다뤄보면 이해가 쉬울 것 같다.
- 상태 관리를 위한 Slice와 내부 리듀서들 세팅하기
- 그렇게 설정된 Slice들을 RootReducer에서 모아주기
- RootReducer와 미들웨어를 store에 넣어주기
- 취합된 store를 앱과의 연결을 위해 Provider로 감싸주고 내부 인자로 넣어주기
Provider 세팅
큰 맥락은 사실상 Redux와 동일하다. Redux-toolkit은 다소 번거로운 부분들을 좀 더 쉽게 다룰 수 있게 개선된 느낌이다. 이제 코드를 살펴보자.
import store from './app/redux/store/index';
import {Provider} from 'react-redux';
const appRedux = () => (
<Provider store={store}>
<App />
</Provider>
);
이 방식은 'react-navigation' 의 NavigationContainer, 'react-query'의 QueryClientProvider, styled-component의 ThemeProvider에서 보던 방식이라 이제 제법 익숙하다.
Store 세팅
reducer와 middleware를 합쳐주기 위해 configureStore 내부에 넣어준다. reducer는 Slicer에 선언된 모든 reducer들을 모아주는 취합해주는 rootReducer를 넣어주고, UI와 네트워크를 보다 쉽게 확인하기 위하여 redux-flipper를 미들웨어로 넣어주었다.
import {configureStore} from '@reduxjs/toolkit';
import {useDispatch} from 'react-redux';
import rootReducer from './reducers';
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => {
if (__DEV__) {
const createDebugger = require('redux-flipper').default;
return getDefaultMiddleware().concat(createDebugger());
}
return getDefaultMiddleware();
},
});
export default store;
//타입스크립트에서 쓰기 위한 wrapper
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
store에 있는 state를 조회하기 위해서는 useSelector를, 갱신을 위해서는 useDispatch를 사용할 것이다. useDispathc의 경우에는 store.dispatch의 타입형을 typeof로 받아서 useDispatch에 넣어준 상태로 useAppDispatch를 export 하여 추후에 사용할 계획이다.
RootReducer 세팅
reducer들을 한 데 모아주기 위해서는 combineReducers 함수를 사용한다. Slice는 뒤에 나오게 될 userSlice, orderSlice 내부의 reducer를 넣어주었다. 이로써 나중에 특정 Slice로의 접근에 대한 분기점이 세팅된 셈이다.
import {combineReducers} from 'redux';
import orderSlice from '../slices/order';
import userSlice from '../slices/user';
const rootReducer = combineReducers({
user: userSlice.reducer,
order: orderSlice.reducer,
});
//RootReducer의 반환값 타입형은 RootState type alias로 지정 가능
export type RootState = ReturnType<typeof rootReducer>;
//ReturnType은 특정 함수의 반환값 타입을 가져오는 유틸 타입형
//이 유팁 타입이 Generic에 <typeof 함수명>을 쓰면 해당 함수의 반환값을 조회할 수 있다
//이렇게까지 반환값을 export 하는 이유는
//추후 useSelector를 사용할 때 이 타입을 참조해야하기 때문.
export default rootReducer;
RootState는 추후 useSelector로 store 내부 상태를 조회할 때, rootReducer의 리던 타입형을 넣어주기 위함이다.
Slice 세팅
대망의 슬라이스. 형태는 아래 코드와 같다.
import {createSlice, PayloadAction} from '@reduxjs/toolkit';
const initialState = {
name: '',
email: '',
accessToken: '',
};
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser(state, action) {
state.email = action.payload.email;
state.name = action.payload.name;
state.accessToken = action.payload.accessToken;
},
setName(state, action) {
state.name = action.payload;
},
setEmail(state, action) {
state.email = action.payload;
},
setAccessToken(state, action) {
state.accessToken = action.payload;
},
},
//extraReducer는 비동기 액션 생성시 필요
// extraReducers: builder => {},
});
export default userSlice;
createSlice를 통하여 생성하며, 내부 인자로는 slice의 이름(문자열), 초기값, 상태 동작을 위한 reducers가 들어간다. reducers 내부에는 특정 동작을 가능하게 해줄 함수들이 들어가는데 이 함수들은 state와 action을 인자로 넣어주어 또다시 그 내부에서 추가 코드를 작성해준다. 이때, action은 payload를 통해 값에 접근하며 타입형이 필요할 시 <PayloadAction>을 넣어줄 수 있다!
import {createSlice, PayloadAction} from '@reduxjs/toolkit';
//
...
//
const orderSlice = createSlice({
name: 'orders',
initialState,
reducers: {
addOrder(state, action: PayloadAction<Order>) {
state.orders.push(action.payload);
}
});
export default orderSlice;
이런 방식 !
조회 및 갱신 (useSelector, useDispatch)
앞서 언급하였듯이 조회 및 갱신은 useSelector와 useDispatch로 해준다. useDispatch는 타입을 넣어주고 useAppDispatch로 export 해주었었다. 아래 코드는 RootStack 코드로 로그인 여부에 따른 분기 처리와 MainTab과 AuthStack을 모아주는 역할의 코드이다.
조회는 useSelector와 state를 통해 접근하고, 이때 state는 RootReducer에서 export 했던 타입형인 RootState을 넣어준다. 그리고 state를 통해 user와 같이 분기 처리해주었던 슬라이스로 접근(state.user.~)하여 상태를 조회할 수 있다.
import {RootState} from '../redux/store/reducers';
import {useSelector} from 'react-redux';
const RootStack = () => {
const isLoggedIn = useSelector((state: RootState) => !!state.user.email);
return isLoggedIn ? (
<Tab.Navigator>
//
...
//
</Tab.Navigator>
) : (
<Stack.Navigator>
//
...
//
</Stack.Navigator>
);
};
export default RootStack;
액션을 통한 갱신은 useDispatch로 할 수 있다고 했다. 원하는 위치에(필자의 경우, 토큰을 refresh 하면서 유저 정보를 다시 서버로 요청하는 코드) 새롭게 선언된 dispatch를 넣어주고 내부에는 원하는 슬라이스와 actions를 통해 원하는 동작 함수를 찾아서 짜면 된다.(Slice.actions.function())
import {useAppDispatch} from '../redux/store';
//Slices
import userSlice from '../redux/slices/user';
import orderSlice from '../redux/slices/order';
const RootStack = () => {
const dispatch = useAppDispatch();
useEffect(() => {
const getTokenAndRefresh = async () => {
try {
// ... 추가 logic
dispatch(
userSlice.actions.setUser({
name: response.data.data.name,
email: response.data.data.email,
accessToken: response.data.data.accessToken,
}),
);
} catch (error) {
// ... 에러 핸들링
} finally {
// ... 추가 코드 작성
}
};
getTokenAndRefresh();
}, [dispatch]);
// ...
};
export default RootStack;
전체 코드.
import React, {useEffect} from 'react';
//Reduc Type
import {RootState} from '../redux/store/reducers';
// ...
import {useSelector} from 'react-redux';
import {useAppDispatch} from '../redux/store';
//Slices
import userSlice from '../redux/slices/user';
import orderSlice from '../redux/slices/order';
const Tab = createBottomTabNavigator();
const Stack = createNativeStackNavigator();
const RootStack = () => {
const dispatch = useAppDispatch();
//!!연산자 : undefined checking : null이나 undefined 면 false 를 반환 !
const isLoggedIn = useSelector((state: RootState) => !!state.user.email);
//앱 실행 시 토큰 존재하면 로그인 활성화!
useEffect(() => {
const getTokenAndRefresh = async () => {
try {
const token = await EncryptedStorage.getItem('refreshToken');
if (!token) {
return; //없으면 탈출
}
//있으면 아래 경로로 토큰 쏴주고, 받아온 값을 리덕스로 보관
const response = await axios.post(
`${Config.API_URL}/refreshToken`,
{},
{
headers: {
authorization: `Bearer ${token}`,
},
},
);
dispatch(
userSlice.actions.setUser({
name: response.data.data.name,
email: response.data.data.email,
accessToken: response.data.data.accessToken,
}),
);
} catch (error) {
console.log('RootStack - useEffect - Error : ', error);
if ((error as AxiosError).response?.data.code === 'expired') {
Alert.alert('알림', '다시 로그인 해주세요.');
}
} finally {
//로딩 화면으로 splash-screen 추가 예정
}
};
getTokenAndRefresh();
}, [dispatch]);
return isLoggedIn ? (
<Tab.Navigator>
<Tab.Screen
name="Home"
component={Home}
/>
<Tab.Screen
name="Going"
component={Going}
/>
<Tab.Screen
name="Profile"
component={Profile}
/>
</Tab.Navigator>
) : (
<Stack.Navigator
screenOptions={{
headerTitle: '',
headerTintColor: 'black',
headerShadowVisible: false,
}}>
<Stack.Screen name="LogIn" component={LogIn} />
<Stack.Screen name="SignUp" component={SignUp} />
</Stack.Navigator>
);
};
export default RootStack;
'Frontend > 상태관리(Redux, Storage, ContextAPI)' 카테고리의 다른 글
[React Native] Hook (1) - useMemo, useCallback (0) | 2022.04.04 |
---|---|
[React Native] Async-Storage / Encrypted-Storage 필요성과 사용법 (0) | 2022.04.03 |
[React Native] Context API with Hook (0) | 2022.02.21 |