“상태관리? 왜 할까?”
“어떤 상태관리 라이브러리가 있을까?”
“왜 redux-toolkit을 선택했나?”
// Redux
const ADD_TODO = 'ADD_TODO'
const TODO_TOGGLED = 'TODO_TOGGLED'
export const addTodo = (text) => ({
type: ADD_TODO,
payload: { text, id: nanoid() },
})
export const todoToggled = (id) => ({
type: TODO_TOGGLED,
payload: { id },
})
export const todosReducer = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return state.concat({
id: action.payload.id,
text: action.payload.text,
completed: false,
})
case TODO_TOGGLED:
return state.map((todo) => {
if (todo.id !== action.payload.id) return todo
return {
...todo,
completed: !todo.completed,
}
})
default:
return state
}
}
// 사용할때
<button onClick={() => { dispatch({type='ADD_TODO', paylod: data }) }
// Redux ToolKit
import { createSlice } from '@reduxjs/toolkit'
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
todoAdded(state, action) {
state.push({
id: action.payload.id,
text: action.payload.text,
completed: false,
})
},
todoToggled(state, action) {
const todo = state.find((todo) => todo.id === action.payload)
todo.completed = !todo.completed ///#3
},
},
})
export const { todoAdded, todoToggled } = todosSlice.actions
export default todosSlice.reducer
중앙에서 관리하는 store 안에 작은 하나하나인 slice를 가진다. 즉, store는 작은 slice들의 모임이 된다.
// _redux/store.ts
import heartsSlice from './slices/heartsSlice';
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
hearts: heartsSlice,
},
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootStateType = ReturnType<typeof store.getState>;
//-> useSelector 사용시 state의 타입으로 사용됨 !!!
export type AppDispatchType = typeof store.dispatch;
// useDispatch를 좀 더 명확하게 사용하기 위함
테스트 용으로, 버튼을 눌러서 특정 메세지를 hearts라는 slice에 저장해보자!
// _redux/slices/heartsSlice.ts
import { createSlice } from '@reduxjs/toolkit';
interface IHeartsSlice {
hearts: string[];
}
const initialState: IHeartsSlice = {
hearts: [],
};
const heartsSlice = createSlice({
name: 'heartsSlice',
initialState,
reducers: {
getHearts: (state, action) => {
if (typeof action.payload === 'string') {
state.hearts = [...state.hearts, action.payload];
}
},
},
});
export const { getHearts } = heartsSlice.actions; //action을 export
export default heartsSlice.reducer; //리듀서를 export
slice의 reducer의 action을 실행시키기 위해선 dispatch !
store의 slice들, 즉 각 상태를 사용하기 위해서 useSelector !
→ state.슬라이스명.이름 으로 접근
// src/Test.tsx
import { useDispatch, useSelector } from 'react-redux';
import { getHearts } from './_redux/slices/heartsSlice';
import { AppDispatchType, RootStateType } from './_redux/stores';
export const Test = () => {
const useAppDispatch: () => AppDispatchType = useDispatch;
const dispatch = useAppDispatch();
const hearts = useSelector((state: RootStateType) => state.hearts.hearts);
// console.log('hearts state', hearts)
return (
<>
<button
onClick={() => {
dispatch(getHearts('hi'));
}}>
hi 보내기
</button>
</>
);
};
—> dispatch할때 리덕스에서처럼 type을 보내지 않아도 됨! heartsSlice.ts 에서 export const { getHearts } = heartsSlice.actions;
를 했기 때문에 바로 action에 접근할 수 있음
// App.tsx
import { ThemeProvider } from '@emotion/react';
import { Provider } from 'react-redux';
import { Outlet } from 'react-router-dom';
import { store } from './_redux/stores';
import { GlobalReset } from './style/GlobalReset';
import { theme } from './style/theme';
export const App = () => {
return (
<Provider store={store}> // 이 React에 store를 등록해주어야한다.
<ThemeProvider theme={theme}>
<GlobalReset />
<Outlet />
</ThemeProvider>
</Provider>
);
};
버튼 클릭시 결과 →
데이터 패칭 등 비동기 처리 작업을 여러번 발생시킬 때, 매번 패칭하고 그 결과를 dispatch 하는 액션 로직을 만드는 것은 불필요하다!
이 때, 패칭하고 dispatch하는 액션 로직을 따로 함수로 만들고 이를 슬라이스에 반영시킬 수 있다!
→ action creator 이다! 액션을 만든다!
createAsyncThunk(type, payloadCreator callback, [options])
type
은 액션타입을 적어준다. 이후에 .pending, .fulfilled, .rejected를 생성해준다.payloadCreator
는 Promise를 반환하는 콜백함수이다. 리듀서와 동일한 형태 (state,action) ⇒ { }Promise 반환의 상태(pending, fullfilled, rejeected) 각각에 따라 리듀서가 필요하다.
reducers는 액션 creator를 만들어주는데, 비동기작업에 대해서는 만들어주지 못하기 때문에 extraReducers에 정의한다.
//_redux/slices/channelsSlice.ts
import { IChannel } from '@/api/_types/apiModels';
import { getApi } from '@/api/apis';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
interface IChannelData {
channels: IChannel[];
isLoading: boolean;
}
const initialState: IChannelData = {
channels: [],
iLoading: false,
};
// 채널데이터를 받아오는 패칭 액션로직을 thunk로 만든다.
export const getChannelsData = **createAsyncThunk**('getChannels', async () => {
const response = await getApi('/channels');
return response?.data as IChannel[];
});
const channelsSlice = createSlice({
name: 'channelsSlice',
initialState,
reducers: {},
extraReducers: (builder) => { // 👈
builder.addCase(getChannelsData.pending, (state, action) => {
state.isLoading = true;
});
builder.addCase(getChannelsData.fulfilled, (state, action) => {
state.isLoading = false;
state.channels = action.payload;
});
builder.addCase(getChannelsData.fulfilled, (state, action) => {
state.isLoading = false;
});
},
});
export default channelsSlice.reducer;
// src/Test.tsx
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatchType, RootStateType } from './_redux/stores';
import { getChannelsData } from './_redux/slices/channelsSlice';
export const Test = () => {
const useAppDispatch: () => AppDispatchType = useDispatch;
const dispatch = useAppDispatch();
return (
<>
<button
onClick={() => {
**dispatch(getChannelsData())**
.then((res) => console.log(res))
.catch((err) => console.error(err));
}}>
GET <CHANNEL
</button>
);
};
작동과정을 정리하자면,
button Onclick으로 thunk 액션 함수가 실행
→ 액션 로직내에서 비동기 처리
→ (성공시)fullfilled 상태의 리듀서에 그 반환값이 action.payload로 들어옴
→ 상태값에 반영 state.channelds = action.payload
참고자료
https://ko.redux.js.org/redux-toolkit/overview/
https://redux-toolkit.js.org/introduction/getting-started
https://redux-toolkit.js.org/api/createAsyncThunk