Next.js + ReduxToolkit + TypeScript

OkGyoung·2023년 6월 23일
0

2023.11 이전 자료

목록 보기
19/30

기존 JS + Jquery로 진행한 프로젝트를 리뉴얼하면서 새로운 언어스킬을 늘리려고 한다.
그래서 Next.js + ReduxToolkit + TypeScript의 환경을 구성하게 되었는데 생각해보면

Next.js : SSR, Redux : CSR 형태이기에 설정이 필요했다.
물론 Next.js는 CSR사용이 가능하다.

먼저 Next.js는 서버 프레임워크이다. 사전에 서버측에서 렌더링을 진행해 바로 사용자에게 보여준다.
하지만 Redux는 클라이언트 전역상태 관리 라이브러리다. 즉 클라이언트쪽에서 전역상태를 활용한다.

그렇다면 다른 브라우저의 전역상태에 영향없이 하나의 사용자 전역상태 상태만 가져와 수정해 다시 해당 사용자에게 동기화 해줘야한다.

이를 가능하도록 한 라이브러리가 next-redux-wrapper이다.

그렇다면 처음부터 따라가면 해보자
먼저 cna를 이용해 초기설정을 하자 (TS, ESLint, Taillwind, Approuter)

npx create-next-app@latest

당장은 next-redux-wrapper을 이용한 서버측과의 통신이 필요하지 않다.

yarn add redux react-redux @types/react-redux @reduxjs/toolkit next-redux-wrapper

먼저 store를 구성해야한다.
아래 예제를 보자 먼저 configureStore를 통해 store를 구성한다. 특히 devTools를 통해 후에 devTools를 통해 확인 가능 하도록 설정한다.

그 밑에는 RootState타입과 Dispatch타입을 선언한다.

/* /redux/store.ts */
import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
  reducer: {},
  devTools: process.env.NODE_ENV !== "production",
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

아래 예제는 Redux의 hooks를 모아두는 파일이다. 이렇게 hooks를 모아서 사용함으로써 이후에
바로 가져와 쓰도록한다. (잠재적 종속성 문제를 사전에 해결)

/* /redux/hooks.ts */
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

아래 예제는 next13버젼 이후 정형화된 AppRouter방식에서 Provider로 감싸는 방법이다.
이렇게 만든 Provider를 이후에 layout에 적용한다.

/* /redux/provider.tsx */
"use client";

import { store } from "./store";
import { Provider } from "react-redux";

export function Providers({ children }: { children: React.ReactNode }) {
  return <Provider store={store}>{children}</Provider>;
}

아래는 최상위 layout파일에 provider을 적용해 redux를 사용가능 하도록 만든 것이다.

/* /layout.tsx */
import { Providers } from "@/redux/provider";

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

아래는 대망의 슬라이스이다. 보통featuers폴더를 만들어 관리한다. 아래 슬라이스는 숫자를 늘려주고 줄여주는 액션을 취한다.

/* /redux/featuers/counterSlice.ts */
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

type CounterState = {
  value: number;
};

const initialState = {
  value: 0,
} as CounterState;

export const counter = createSlice({
  name: "counter",
  initialState,
  reducers: {
    reset: () => initialState,
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
    decrementByAmount: (state, action: PayloadAction<number>) => {
      state.value -= action.payload;
    },
  },
});

export const {
  increment,
  incrementByAmount,
  decrement,
  decrementByAmount,
  reset,
} = counter.actions;
export default counter.reducer;

그럼 위로 올라가서 store에 새로운 슬라이스를 등록한다.

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counterSlice";

export const store = configureStore({
  reducer: {
    counterReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

마지막으로 인터페이스를 구성하면 끝이다.

/* /page.tsx */
"use client";

import { decrement, increment, reset } from "./redux/features/counterSlice";
import { useAppDispatch, useAppSelector } from "./redux/hooks";

export default function Home() {
  const count = useAppSelector((state) => state.counterReducer.value);
  const dispatch = useAppDispatch();

  return (
    <main style={{ maxWidth: 1200, marginInline: "auto", padding: 20 }}>
      <div style={{ marginBottom: "4rem", textAlign: "center" }}>
        <h4 style={{ marginBottom: 16 }}>{count}</h4>
        <button onClick={() => dispatch(increment())}>increment</button>
        <button
          onClick={() => dispatch(decrement())}
          style={{ marginInline: 16 }}
        >
          decrement
        </button>
        <button onClick={() => dispatch(reset())}>reset</button>
      </div>
    </main>
  );
}

RTK Query사용하기

위에 과정을 이용하면 기본적인 Redux를 이용한 전역 상태관리를 이용할 수 있습니다. 하지만 서버와 통신하고 적적한 데이터를 API를 이용해 받아오려면 RTK Query를 사용해야합니다.

그렇다면 https://jsonplaceholder.typicode.com/ 사이트를 이용해 유저데이터를 받고 이를 활용해서 진행해보겠습니다.

createApi() : RTK Query 핵심 기능입니다. 데이터를 패치하고 변환하는 설정을 포함해서 엔드포인트들에서 어떻게 데이터를 패치하는지 정의할 수 있습니다. 대부분의 케이스에서는 베이스 URL당 하나의 API 슬라이스를 사용해야 합니다.

fetchBaseQuery() : 간단한 요청을 위한 fetch의 래퍼입니다. 대부분의 사용자에게 createApi의 baseQuery로 권장합니다.

아래 예제를 보겠습니다. 먼전 받아올 User타입을 선언 해 줍니다. 그리고 createApi를 통해서 기본 설정을 합니다.

/* /redux/services/userApi.ts */
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";

type User = {
  id: number;
  name: string;
  email: number;
};

export const userApi = createApi({
  reducerPath: "userApi",
  refetchOnFocus: true,
  baseQuery: fetchBaseQuery({
    baseUrl: "https://jsonplaceholder.typicode.com/",
  }),
  endpoints: (builder) => ({
    getUsers: builder.query<User[], null>({
      query: () => "users",
    }),
    getUserById: builder.query<User, { id: string }>({
      query: ({ id }) => `users/${id}`,
    }),
  }),
});

export const { useGetUsersQuery, useGetUserByIdQuery } = userApi;

이후 store에 저장합니다. 그리고 미들웨어를 만들어서 통신하도록 합니다.

/* /redux/store.ts */
import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counterSlice";
import { userApi } from "./services/userApi";
import { setupListeners } from "@reduxjs/toolkit/dist/query";

export const store = configureStore({
  reducer: {
    counterReducer,
    [userApi.reducerPath]: userApi.reducer,
  },
  devTools: process.env.NODE_ENV !== "production",
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({}).concat([userApi.middleware]),
});

setupListeners(store.dispatch);

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

page 설정은 아래처럼 추가합니다.

"use client";
import { useGetUsersQuery } from "./redux/services/userApi";
import { useState } from "react";
import {
  decrement,
  increment,
  reset,
  decrementByAmount,
  incrementByAmount,
} from "./redux/features/counterSlice";
import { useAppDispatch, useAppSelector } from "./redux/hooks";

export default function Home() {
  const count = useAppSelector((state) => state.counterReducer.value);
  const [amount, setAmount] = useState(0);
  const dispatch = useAppDispatch();

  const { isLoading, isFetching, data, error } = useGetUsersQuery(null);

  return (
    <main style={{ maxWidth: 1200, marginInline: "auto", padding: 20 }}>
      <div style={{ marginBottom: "4rem", textAlign: "center" }}>
        <h4 style={{ marginBottom: 16 }}>{count}</h4>
        <input
          onChange={(e) => setAmount(parseInt(e.target.value))}
          value={amount}
          style={{ marginBottom: 16 }}
        />
        <button onClick={() => dispatch(increment())}>increment</button>
        <button
          onClick={() => dispatch(decrement())}
          style={{ marginInline: 16 }}
        >
          decrement
        </button>
        <button onClick={() => dispatch(incrementByAmount(amount))}>
          incrementAmount
        </button>
        <button
          onClick={() => dispatch(decrementByAmount(amount))}
          style={{ marginInline: 16 }}
        >
          decrementAmount
        </button>
        <button onClick={() => dispatch(reset())}>reset</button>
      </div>
      {error ? (
        <p>Oh no, there was an error</p>
      ) : isLoading || isFetching ? (
        <p>Loading...</p>
      ) : data ? (
        <div
          style={{
            display: "grid",
            gridTemplateColumns: "1fr 1fr 1fr 1fr",
            gap: 20,
          }}
        >
          {data.map((user) => (
            <div
              key={user.id}
              style={{ border: "1px solid #ccc", textAlign: "center" }}
            >
              <img
                src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
                alt={user.name}
                style={{ height: 180, width: 180 }}
              />
              <h3>{user.name}</h3>
            </div>
          ))}
        </div>
      ) : null}
    </main>
  );
}

도움링크

profile
이유를 생각하는 개발자

0개의 댓글