본문 바로가기
프론트엔드

Next.js + TypeScript + @reduxjs/toolkit 사용하기 < 2 / 2 > 개인기록

by goodchuck 2024. 5. 22.

 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에 대한 사용 정리였습니다. 완전히 효율적인 구성은 아닐수도있다고 보고있고 개선할점이 보여서 후에 좀더 좋은방법이 생기면 고치지 않을까 싶습니다.