[Next.js] Next.js 13버젼 + Redux Toolkit

S_Soo100·2023년 8월 6일
1

web

목록 보기
5/6
post-thumbnail

참고자료

1) 리덕스 툴킷 공식문서: https://ko.redux.js.org/redux-toolkit/overview/
2) 생활코딩 유튜브: https://www.youtube.com/watch?v=9wrHxqI6zuM
3) Pedro Tech 유튜브: https://www.youtube.com/watch?v=Yokjzp91A4o

리덕스 툴킷(Redux toolkit) 이란?

  • 효율적인 Redux 개발을 위한 저희의 견해를 반영한,
    "이것만으로도 작동하는 도구" 모음으로,
    기존에 사용하기 어렵던 리덕스를 '리덕스 툴킷 개발진'의 주관적인 의견으로 사용이 쉽도록 만든 패키지이며,
    store 만들기, 리듀서 정의, 불변 업데이트 로직, 액션 생산자나 액션 타입을 직접 작성하지 않고도 전역 상태인 슬라이스(Slice)를 만들어내는 기능까지 대부분의 Redux 사용 방법에 해당하는 유틸리티 함수를 포함한다.
    그리고 비동기 로직을 위한 Thunk등의 애드온도 함께 포함되어 있다.
    (출처: https://ko.redux.js.org/redux-toolkit/overview/)

  • 즉, 리덕스의 자유도를 어느정도 저해하지만 보일러 플레이트를 줄이고
    정말 자주 사용하는 기능들을 래핑해둔 것이다.


리덕스 툴킷 사용의 목적?

  • 이것 또한 공식문서의 친절한 설명을 참고해서 말하자면
    아래 3가지를 해결하기 위해서이다.
    (1) "Store를 설정하는 것이 너무 복잡하다"
    (2) "쓸만하게 되려면 너무 많은 패키지들을 더 설치해야 한다"
    (3) "보일러플레이트 코드를 너무 많이 필요로 한다"

  • 리덕스를 도입하는 많은 개발자들의 고민을 줄여줄만한 좋은 패키지라고 생각한다.


이걸 Next.js app dir에 적용해보자.

  • 아직 현업에서 app dir 사용은 어려워도 미리 알아두면 좋겠지? 사실 기본 사용법이랑 크게 다르지 않다.
    나는 여기에 추가로 타입스크립트 사용을 전제로 했다.
    또, 실전에서 사용하면 좋게 카운터가 아니라 유저 상태 등 조금더 구체적인 예시를 찾다가, 외국의 좋은 강의 영상을 찾아서 그걸 기준으로 정리해보았다.
  • 리덕스 툴킷의 사용은 아래 순서로 진행된다.

1) 리덕스 툴킷과 리액트 리덕스 설치

  • app디렉토리를 쓰는 create-next-app은 완료했다고 생각하고 진행하자
    우선 두 개의 라이브러리를 설치한다.
  # NPM
  npm i @reduxjs/toolkit react-redux

2) 스토어(store) 만들기

  • 그리고 app디렉토리의 바깥, src디렉토리 안에 redux디렉토리를 만든다.
    (폴더 명은 바꿔도 된다)
    여기에 먼저 저장소를 만들어주자.
    store.ts를 만든다.
import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
  reducer: {
    //여기에 추후에 리듀서를 넣어둔다.
  },
});
  • 수많은 전역변수들을 관리할 수 있는 스토어가 만들어졌다.
    여기다 slice들을 만들고, 리듀서를 넣어주고 하면서 기능을 추가해보자.

3) 슬라이스(slice)와 리듀서

  • 방금 만든 redux디렉토리 안에 slices디렉토리를 만든다.
    slice란, 리코일의 atom 같이 리덕스 툴킷이 관리하는 "전역 상태의 단위'이다.

  • 리덕스 툴킷의 createSlice()PayloadAction타입을 사용하게 되는데,
    createSlice()란 말 그대로 slice를 쉽게 만들게 해주며 상태 이름, 초기 값, 리듀서들을 인자로 받는다.
    아래는 createSlice()의 선언문이다.
    (줄이 길어서 조금 나눴는데 가독이 더럽다, 양해해주기 바란다..)

export declare function createSlice
  <State, CaseReducers extends SliceCaseReducers<State>, 
  Name extends string = string>
  (options: CreateSliceOptions<State, CaseReducers, Name>)
  : Slice<State, CaseReducers, Name>;
  • slice의 이름을 문자열로 받고, CaseReducers를 받고, 초깃값인 State를 받는다. 이 3가지를 정의해주자.

  • slice디렉토리에 auth-slice.ts를 만들자.

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

const initialState = {}; // 아직 초기상태 정의되지 않음!

export const auth = createSlice({
  name: "auth", // slice 이름으로 쓸 문자열!
  initialState, // 아직 정의되지 않음!
  reducers: {
  	// 액션 여기에 추가 예정!!
  },
});
  • 생각보다 간단하다.
    그리고 이전 리덕스 툴킷을 사용하지 않을 때에는
    authState.ts, authActions.ts, rootReducer.ts 등으로 파일 쪼개져서 관리하던걸 생각하면 여기서부터 코드량이 많이 줄어들었다고 느낄 수 있다.

  • 그러면 이제 간단하게 유저 상태를 정의하고 로그인, 로그아웃 기능만 구현하자.
    원래는 비동기로 유저 정보(AuthState타입의)를 받아와야 하지만
    간단하게 로그인 창에 유저 이름을 입력하면 그 유저로 변경된다고 하자.

import { createSlice, PayloadAction } from "@reduxjs/toolkit";


type InitialState = {
  value: AuthState;
};

// 유저 상태를 정의할 타입을 만든다.
type AuthState = {
  isAuth: boolean; // 로그인이 되었는가? 
  username: string; // 유저 닉네임
  uid: string; // id
  isModerator: boolean; // 관리자 계정인가요?
};

// 로그인 되지 않은 상태
const initialState = {
  value: {
    isAuth: false,
    username: "",
    uid: "",
    isModerator: false,
  } as AuthState,
} as InitialState;

export const auth = createSlice({
  name: "auth", // slice name
  initialState, // initial state
  reducers: {
    // 이 아래에 auth slice상태를 관리하기 위한 액션들을 정의하자.
    logOut: () => { 
      return initialState;
    },
    // 로그아웃을 하면 유저가 없는 initialState로 변경
    logIn: (State, action: PayloadAction<string>) => {
      // 여기서 State란 초기상태인데, 사용하지 않으니 회색으로 뜰 것이다.
      // _(언더 바)로 해둬서 사용하지 않음을 표시할 수도 있다.
      return {
        value: {
          isAuth: true, // 로그인 했으니까 true
          username: action.payload,
          uid: "uid",
          isModerator: false,
        },
      };
    },
    // 로그인을 하면 받아온 유저 정보로 변경
  },
});

export const { logIn, logOut, toggleModerator } = auth.actions;
// 정의한 액션들을 export 
export default auth.reducer;
// authReducer를 export
  • 현업에서는 이거보다 복잡하게 관리하겠지만 스터디하며 정리하기엔 딱 좋다.
    이제 store에 auth 슬라이스를 추가하자.
  • 처음 배우며 특이했던 점은, export는 auth.reducer로 하는데
    import 및 사용은 authReducer로 한다는 점이다.

store.ts

import { configureStore } from "@reduxjs/toolkit";
import authReducer from "./slices/auth-slice";
import { useSelector, TypedUseSelectorHook } from "react-redux";

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

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

4) 프로바이더 연결

  • 프로바이더는 상태관리에 정말 자주 사용되는 개념인데,
    리액트 context를 사용한 개념으로 Next Auth때도 다뤘을거다(세션 프로바이더 등).
    프로바이더를 최상위에 씌워놓고 그 아래 있는 자식 컴포넌트라면 언제든 프로바이더에 있는 내용을 꺼내쓸수 있다.
    이건 리덕스 툴킷 내용은 아니고, react-redux에 포함된 내용으로 사실 예전과 동일하다.
    그러므로 빠르게 진행해보자.

  • redux폴더에 provider.tsx를 만든다.
    상태관리 관련된 컴포넌트는 'use client'는 필수!

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

type Props = {
  children: React.ReactNode;
};

export default function ReduxProvider({ children }: Props) {
  return <Provider store={store}>{children}</Provider>;
}
  • 이제 src/app/layout.tsx에 프로바이더를 씌워주자!

layout.tsx

import "./globals.css";
import { Nanum_Gothic } from "next/font/google";
import ReduxProvider from "@/redux/provider";

const nanum_Gothic = Nanum_Gothic({ weight: "400", subsets: ["latin"] });

export const metadata = {
  title: "Redux toolkit study",
  description: "velog 좋아요",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={nanum_Gothic.className}>
        <ReduxProvider>{children}</ReduxProvider>
        // 바로 이곳!
      </body>
    </html>
  );
}

5) selector, dispatch

  • 이제 준비는 마쳤다. selector와 dispatch를 사용해서 본격적으로 상태관리를 해보자.
    상태관리를 사용하는 컴포넌트는 항상 'use client'로 사용하는걸 잊지 말자

  • 로그인 컴포넌트를 하나 만들자.
    src/screens/LogInScreen.tsx

"use client";
import { useState } from "react";
import { useDispatch } from "react-redux";
import { AppDispatch } from "@/redux/store";
import { logOut, logIn } from "@/redux/slices/auth-slice";

type Props = {
  name: string;
};

export default function LogInScreen({ name }: Props) {
  const [username, setUsername] = useState<string>("");
  const dispatch = useDispatch<AppDispatch>();
  const style = {
    buttonStyle:
      "px-4 py-2 m-4 text-white bg-blue-400 rounded-[1.1rem] ",
    inputStyle: "m-4 border-2 border-blue-400 ",
  };

  const onClickLogIn = () => {
    dispatch(logIn(username));
  };
  
  const onClickLogOut = () => {
    dispatch(logOut());
  };

  return (
    <div className="flex">
      <input
        className={style.inputStyle}
        type="text"
        onChange={(e) => setUsername(e.target.value)}
      />
      <button className={style.buttonStyle} onClick={onClickLogIn}>
        Log In
      </button>
      <button className={style.buttonStyle} onClick={onClickLogOut}>
        Log Out
      </button>
    </div>
  );
}

  • src/app.tsx에 셀렉터와 로그인 스크린을 붙이자
"use client";
import { useAppSelector } from "@/redux/store";
import LogInScreen from "@/screens/LogIn";

export default function Home() {
  const { username, uid } = useAppSelector((state) => state.authReducer.value);

  return (
    <section className="m-4">
      <h1 className="text-2xl">UserName : {username}</h1>
      <h1 className="text-2xl">uid : {uid}</h1>
      <LogInScreen name={username} />
    </section>
  );
}
  • 예시로 만든 화면이다. 초기 상태에 유저 이름과 아이디가 모두 ""이기 때문에 비어있는 채로 나온다.

  • 이제 input에 변수를 입력하고 로그인 버튼을 누르면?

  • dispatch와 useSelector가 열일하고 있음을 알 수 있다!

profile
플러터, 리액트

4개의 댓글

comment-user-thumbnail
2023년 10월 10일

useAppSelector 는 어디에 있나용?

1개의 답글
comment-user-thumbnail
2023년 11월 5일

혹시 첫 렌더 때 redux 작동 안되는 이슈 겪어보신 적 있으신가요?

Next.js 14 App routing 환경이고, 뭔가 작동이 이상한 것 같아 자세히 살펴보니 보통 첫 렌더 때 redux 와 devtools 작동이 안되고, 새로고침 해야 작동이 되는 이슈를 겪고 있습니다

Next.js 12/13 Page routing 때 쓰던 코드 가져와서 작업하다가 이슈 발견해서 올려주신 글도 시도해보고, 해외 블로그도 똑같이 시도 해보았는데 문제가 여전해서 댓글 남깁니다 ㅠㅠ

1개의 답글