reduxToolKit 사용해보기
아래 사진은 src/libs폴더입니다. 이중 예제에 사용하기좋은 counter를 이용해서 redux 를 사용해보려고합니다.
counter.actions.ts
// base
import { createAction } from "@reduxjs/toolkit";
// default
export const COUNTER = "counter";
export const COUNTER_SLICE_NAME = `${COUNTER}Slice`;
// action
const INCREMENT = `${COUNTER}/increment`;
const DECREMENT = `${COUNTER}/decrement`;
const INCREMENT_BY_AMOUNT = `${COUNTER}/incrementByAmount`;
const FETCH_COUNT = `${COUNTER}/fetchCount`;
// createAction -> 해당껀 thunk랑은 맞지 않는다.
const incrementAction = createAction(INCREMENT);
const decrementAction = createAction(DECREMENT);
const incrementByAmount = createAction<number>(INCREMENT_BY_AMOUNT);
// const fetchCountAction = createAction(FETCH_COUNT);
export const actions = {
incrementAction,
decrementAction,
incrementByAmount,
FETCH_COUNT,
};
counter의 action을 정의한 파일입니다. 해당 파일을 선언함으로써 협업자가 counter에 대한 action의 내용들을 한눈에 확인이 가능합니다.
counter.api.ts
// A mock function to mimic making an async request for data
export function fetchCount(amount = 1) {
return new Promise<{ data: number }>((resolve) =>
setTimeout(() => resolve({ data: amount }), 500)
);
}
counter의 api를 정의한 파일입니다. 해당 포스트에선 예시로 api호출 대신 딜레이를 걸어서 promise를 반환하게 되어있습니다.
counter.thunk.ts
// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched. Thunks are
import { createAsyncThunk } from "@reduxjs/toolkit";
import { actions } from "../actions/counter.actions";
import { fetchCount } from "../api/counter.api";
// typically used to make async requests.
export const incrementAsync = createAsyncThunk(
actions.FETCH_COUNT,
async (amount: number) => {
const response = await fetchCount(amount);
// The value we return becomes the `fulfilled` action payload
return response.data;
}
);
counter의 thunk를 정의한 파일입니다. 해당 파일을 선언함으로써 비동기작업들을 볼수있고
incrementAsync 함수를 보면
actions.FETCH_COUNT라는 미리 정의된 액션 명을 사용을하며
그다음 인자로 비동기함수를 사용합니다.
fetchCount는 counter.api에 정의된 내용을 사용합니다.
counter.slice.ts
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { RootState, AppThunk } from "../../../store";
import { incrementAsync } from "../thunk/counter.thunk";
import { actions, COUNTER_SLICE_NAME } from "../actions/counter.actions";
export interface CounterState {
value: number;
status: "idle" | "loading" | "failed";
}
const initialState: CounterState = {
value: 0,
status: "idle",
};
export const counterSlice = createSlice({
name: COUNTER_SLICE_NAME,
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
// increment: (state) => {
// // Redux Toolkit allows us to write "mutating" logic in reducers. It
// // doesn't actually mutate the state because it uses the Immer library,
// // which detects changes to a "draft state" and produces a brand new
// // immutable state based off those changes
// state.value += 1;
// },
// decrement: (state) => {
// state.value -= 1;
// },
// Use the PayloadAction type to declare the contents of `action.payload`
// incrementByAmount: (state, action: PayloadAction<number>) => {
// state.value += action.payload;
// },
},
// The `extraReducers` field lets the slice handle actions defined elsewhere,
// including actions generated by createAsyncThunk or in other slices.
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = "loading";
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = "idle";
state.value += action.payload;
})
.addCase(incrementAsync.rejected, (state) => {
state.status = "failed";
})
.addCase(actions.incrementAction, (state) => {
state.value += 1;
})
.addCase(actions.decrementAction, (state) => {
state.value -= 1;
})
.addCase(
actions.incrementByAmount,
(state, action: PayloadAction<number>) => {
state.value += action.payload;
}
);
},
});
// export const { incrementByAmount } = counterSlice.actions;
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state: RootState) => state.counter.value;
// We can also write thunks by hand, which may contain both sync and async logic.
// Here's an example of conditionally dispatching actions based on current state.
export const incrementIfOdd =
(amount: number): AppThunk =>
(dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
// dispatch(incrementByAmount(amount));
dispatch(actions.incrementByAmount(amount));
}
};
// export const counter = counterSlice.name;
// 해당 리듀서는 rootReducer에 전달하기 용
export const counterReducer = counterSlice.reducer;
// export default counterSlice.reducer;
counter의 slice 파일입니다.
해당에서 초기 값과 타입들을 선언해서 사용합니다.
counter에선 value와 status만을 활용할 거기 때문에
CounterState에서 value와 status를 선언해주고 initialState라는 초기값도 선언해 사용합니다.
그다음 createSlice를 통해 name은 사전에 선언한 COUNTER_SLICE_NAME을 가져와 sliceName으로 사용합니다.
만약 counter.actions.ts에서 createAction으로 action을 만들었다면 reducers에서 사용할 수 없는데
예를들면 counter.actions.ts에서 선언한 incrementAction은 createAction으로 만들어진 action인데
해당 액션은 extraReducers에 addCase를 이용해서 활용해야합니다.
extraReducers에서는
.addCase(actions.incrementAction, (state) => {
state.value += 1;
})
해당으로 선언이 되어 사용이 가능합니다.
Counter.tsx
"use client"
import React, { useState } from 'react'
import { useAppSelector, useAppDispatch } from '../../hooks'
import {
actions,
incrementIfOdd,
selectCount,
incrementAsync
} from '../counter'
import styles from './Counter.module.css'
export function Counter() {
const count = useAppSelector(selectCount)
const dispatch = useAppDispatch()
const [incrementAmount, setIncrementAmount] = useState('2')
const incrementValue = Number(incrementAmount) || 0
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(actions.decrementAction())}
>
-
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(actions.incrementAction())}
>
+
</button>
</div>
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
onChange={e => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
onClick={() => dispatch(actions.incrementByAmount(incrementValue))}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(incrementValue))}
>
Add Async
</button>
<button
className={styles.button}
onClick={() => dispatch(incrementIfOdd(incrementValue))}
>
Add If Odd
</button>
</div>
</div>
)
}
위는 src/libs/features/counter/Counter.tsx파일입니다.
해당 부분은 아직 완전한 정리를 하지 못했습니다.
더 효율적인 방법을 모색중입니다.
해당 컴포넌트에서 redux를 사용하기위해서
import { useAppSelector, useAppDispatch } from '../../hooks'
import {
actions,
incrementIfOdd,
selectCount,
incrementAsync
} from '../counter'
import를 해줍니다.
export function Counter() {
const count = useAppSelector(selectCount)
const dispatch = useAppDispatch()
const [incrementAmount, setIncrementAmount] = useState('2')
const incrementValue = Number(incrementAmount) || 0
..... 생략
}
const count는 아래코드인 counter.slice.ts에서 선언했었던 counter의 value입니다.
해당 코드로 사용자에게 값을 가져와서 보여줄수 있습니다.
dispatch는 thunk나 그런 액션들을 실행시키기 위한 함수입니다. useAppSelector, useAppDispatch는 다 hooks.ts에서 선언한 내용입니다.
export const selectCount = (state: RootState) => state.counter.value;
dispatch를 이용한 함수 호출방법
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(actions.decrementAction())}
>
-
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(actions.incrementAction())}
>
+
</button>
Counter.tsx내의 return문 tsx내용중 일부입니다. 해당처럼 dispatch를 통해 counter.actions.ts에서 export한 actions를 가져와 이용할 수 있습니다.
이렇게 Next.js + TS + reduxToolKit에 대한 사용 정리였습니다. 완전히 효율적인 구성은 아닐수도있다고 보고있고 개선할점이 보여서 후에 좀더 좋은방법이 생기면 고치지 않을까 싶습니다.
'프론트엔드' 카테고리의 다른 글
[Next.js, React] TodoList 만들어보기 (0) | 2024.06.04 |
---|---|
Next.js + TypeScript + @reduxjs/toolkit 사용하기 < 1 / 2 > 개인기록 (0) | 2024.05.22 |