0. 들어가며,
Redux에 대해서 몇가지 공부를 했었다. 이번에는 타입스크립트와 함께 사용해볼 계획이다.
[React] Reduxt - toolkit 사용해보기편
기본적인 타입스크립트 세팅은 끝났다고 생각하고, Redux를 사용하는 부분만 작성할 예정이다.
세팅하기
yarn add @reduxjs/toolkit react-redux
리액트에서 리덕스를 사용하기 위해서 필요한 패키지를 먼저 설치한다.
@types/react-redux
타입스크립트를 위해서 해당 패키지가 필요하지 않은가 생각할 수 있지만
react-redux 7.2.3버전부터 해당 패키지를 포함하고 있기 때문에 이후 버전은 설치할 필요가 없다.
- src
-- store
--- index.ts
-- reducer
--- 필요에 따라 사용할 reducer.ts
-- hooks
--- index.ts
형식으로 폴더 구성을 했다.
Store
전체 저장소( 상태 ) 가 들어가 있다. 하나의 프로젝트는 하나의 스토어를 권장하지만
필요하다면 여러개도 가능은 하다.
Reducer
변화를 일으키는, 그리고 초기값을 지정하는 공간
필요에 따라 여라가지 Reducer가 들어간다.
Hooks
Dispatch나 Selecter를 쉽게 사용하기 위한 커스텀 훅을 작성할 것이다.
이쪽은 Redux관련 Hook만 들어가는 것이 아닌 필요에 따라 다양하게 활용한다.
이제 본격적으로 Redux를 사용해보자!
store/index.ts
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
아직 리듀서를 만들지 않았기 때문에 들어가는 것은 없고,
RootState와 AppDispatch는 Hooks에서 사용될 Dispatch와 Selector에 사용할 타입이다.
hooks/index.ts
import { useDispatch, useSelector } from "react-redux";
import type { TypedUseSelectorHook } from "react-redux";
import type { RootState, AppDispatch } from "@/store";
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
바로 나왔다.
useAppDispatch
완전 생소한 문법이다.
const x : (a:number) => string = (x) => String(x)
const x : (a:number) => string
함수 스스로 타입을 string, 매개변수 a는 number라고 추론하는 것이다.
즉, (x) => String(x) 함수는 x의 타입을 number로 return 값을 string으로 해야한다는 것이다.
이렇게 설정한 이유는 useDispatch는 기본적으로 thunks에 대해서 알지 못한다.
그렇기 때문에 우리가 thunk middleware를 사용하는 비동기 처리가 필요할 때 매번 AppDispatch를
import하면 낭비이기 때문에 미리 설정해준 것이다.
useAppSelector
useSelector에 state 타입을 지정해주지 않으면
const count = useSelector((state: RootState) => state.counter.value)
사용할 때마다 매번 state의 타입을 지정해야 한다.
const count = useAppSelector((state) => state.counter.value)
하지만 미리 설정해둔다면 따로 state 타입을 지정할 필요가 없다.
reducer/counter.ts
// reducer/counter.ts
import { createSlice } from "@reduxjs/toolkit";
import type { PayloadAction } from "@reduxjs/toolkit";
export interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
createSlice를 사용해서 초기값, reducer 등을 정의할 수 있다.
그 후 action으로 사용하기 위해서 counderSlice.actions를 export 하고
스토어에서 사용하기 위한 counterSlice.reducer도 export 한다.
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../reducer/test";
export const store = configureStore({
reducer: {
// 추가
counter: counterReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
그걸 store에 추가해주면 된다.
React에 적용하기
설정이 끝나면 이제 React에서 해당 스토어를 사용할 수 있게 해야한다.
import { Provider } from "react-redux";
import Router from "./pages/Router";
import { store } from "./store";
interface Props {}
const App = ({}: Props) => {
return (
<Provider store={store}>
<Router />
</Provider>
);
};
export default App;
Provider를 사용해서 store에 우리가 만든 스토어를 넣어주면 된다.
비동기 처리하기
// 통신 에러 시 보여줄 에러 메세지의 타입
interface MyKnownError {
errorMessage: string
}
// 통신 성공 시 가져오게 될 데이터의 타입
interface TodosAttributes {
id: number;
text: string;
completed: boolean
}
// 비동기 통신 구현
const fetchTodos = createAsyncThunk<
// 성공 시 리턴 타입
TodosAttributes[],
// input type. 아래 콜백함수에서 userId 인자가 input에 해당
number,
// ThunkApi 정의({dispatch?, state?, extra?, rejectValue?})
{ rejectValue: MyKnownError }
>('todos/fetchTodos', async(userId, thunkAPI) => {
try {
const {data} = await axios.get(`https://localhost:3000/todos/${userId}`);
return data;
} catch(e){
// rejectWithValue를 사용하여 에러 핸들링이 가능하다
return thunkAPI.rejectWithValue({ errorMessage: '알 수 없는 에러가 발생했습니다.' });
}
})
비동기 작업을 위해서 사용하는 createASyncThunk이다.
<> 의 첫 번째 타입의 경우, 서버 통신이 원활하게 완료된 경우의 타입을 말한다.
두 번째 타입의 경우, 매개변수, 코드에선 userId의 타입을 말한다.
세 번째의 경우 오류가 발생했을 때 rejectValue가 return되는데, 그때의 타입을 말한다.
// ... //
const todosSlice = createSlice({
// ...
extraReducers: (builder) => {
builder
// 통신 중
.addCase(fetchTodos.pending, (state) => {
state.error = null;
state.loading = true;
})
// 통신 성공
.addCase(fetchTodos.fulfilled, (state, { payload }) => {
state.error = null;
state.loading = false;
state.todos = payload;
})
// 통신 에러
.addCase(fetchTodos.rejected, (state, { payload }) => {
state.error = payload;
state.loading = false;
});
},
})
다음으로 reducer 설정이다.
addCase를 사용해서 pending, fulfilled, rejected 상태를 설정한다.
각각 통신 중, 통신 성공, 통신 에러를 나타낸다.
import {unwrapResult} from '@reduxjs/toolkit';
// ... //
try{
const resultAction = await dispatch(fetchTodos(1));
const todos = unwrapResult(resultAction);
setTodos(todos);
} catch(e){
console.log(e)
}
// ... //
마지막으로 사용할 때, Thunk는 기본적으로 프로미스를 반환한다.
하지만 unwrapResult를 사용하면 바로 결과값을 핸들링을 할 수 있다.
'React > 실험실' 카테고리의 다른 글
[React] React와 Canvas를 사용해서 움직이는 공 만들기 (1) | 2022.10.20 |
---|---|
[React] React Query - 기존 상태 관리 라이브러리의 단점 (0) | 2022.10.17 |
[React] 뒤로가기 버튼 감지 (1) | 2022.10.11 |
[React] Webpack과 Babel과 TypeScript와 React (1) | 2022.10.03 |
[React] 나만의 리액트 만들기 - 2 - 컴포넌트 (0) | 2022.09.30 |