Redux 도입기

leave_a_comment·2024년 9월 11일
0
post-thumbnail

목차

  • 리액트 상태 관리 라이브러리들 (feat. 장단점)
  • 상태 관리에 Redux를 선택하게 된 계기
  • Redux 셋팅하기
  • Next.js에 Redux를 적용하며 마주하게 된 사사로운 오류사항들
  • 번외






🛠️ 리액트 상태 관리 라이브러리들 (feat. 장단점)


리액트 상태 관리 라이브러리에는 다양한 종류가 있다. 익숙한 것들만 간단히 나열해보자면 Recoil, Redux, Jotai, Mobx, zustand 등등 ...

우리가 사용할 기술을 선정할 때 고려해야 할 것이 몇가지 있다. 각 기술의 특성과 장단점을 무조건 파악을 해야 한다는 거다.

제일 주의해야하는 건 무작정 '남들이 다 사용하니까' 선택하는 일이다.

그렇게 되면 내가 만들 프로젝트와 그 기술의 성격이 맞지 않을 수가 있고, 몸집이 커진 프로젝트를 다시 싹 다 갈아 엎어야 하는 대참사가 발생할 수도 있다.



상태관리에 Redux를 선택하게 된 계기


위와 같은 다양한 라이브러리 중 나는 zustand 또는 Redux 사이에서 고민하게 되었다.


나의 상황을 적어보자면

1) 온전히 혼자 작업을 해야 한다.
2) 최대한 빠르게 작업을 마무리 하면 좋다.
3) 두 라이브러리 모두 사용해 본 경험이 없거나 적다.


위 같은 상황에서 빠르고 쉽게 작업할 수 있는 zustand 를 선택하는게 더 효율적이었겠지만, 나는 redux를 선택했다!


왜냐면 redux는 대규모 프로젝트에 더 적합하기 때문이다. (나는 런칭해서 운영까지, 즉 유지보수까지 해보고 싶었기 때문이다.)
그리고 '쉬운걸 먼저 적용해버리면 나중에 더 큰 어려움을 마주하게 되지 않을까? 그러니까 어려운 것부터 먼저 깊게 다뤄보자!' 싶은 마음이었다.


따라서 redux를 next.js(App Route) + typescript 에 적용하게 되는데 ,,,



Redux 셋팅하기


자 차근차근 적용해보도록 하자.


우선, redux와 관련 패키지를 설치해야 한다.



Redux 설치


npm install @reduxjs/toolkit react-redux



Redux 설정


일단, store를 만든다. 이때 store는 redux의 모든 상태들을 관리하게 된다.

// store.ts

import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./rootReducer";

export const makeStore = () => {
  return configureStore({
    reducer: rootReducer,
  });
};


export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];



slice 생성

slice는 액션 및 리듀서를 포함한다.

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

const userAuthSlices = createSlice({
  name: "userAuth", // slice 식별
  initialState: { name: "subeen" },
  reducers: {
    setUserAuth: (state, action) => {
      state.name = action.payload;
    },
  },
});

export const { setUserAuth } = userAuthSlices.actions;

export default userAuthSlices.reducer;

이때, name에 따라 slice가 식별되니 직관적이게 작명하도록 유의한다.



Provider 설정

Provider를 설정해 모든 페이지에서 Redux 상태에 접근할 수 있도록 한다.

Next.js App Router 구조에서는 app/layout.tsx 파일에서 설정한다.


"use client";

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

const store = makeStore(); // makeStore 함수를 호출하여 스토어 객체를 생성합니다.

const ClientLayout: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  return (
    <Provider store={store}>
      {/* store 객체를 Provider에 전달합니다. */}
      <main>{children}</main>
    </Provider>
  );
};

export default ClientLayout;
import Footer from "@/components/Footer";
import Header from "@/components/Header";
import type { Metadata, Viewport } from "next";
import { Inter } from "next/font/google";
import ClientLayout from "./ClientLayout";
import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

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

export const viewport: Viewport = {
  initialScale: 1,
  width: "device-width",
};

const RootLayout = ({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) => {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ClientLayout>
          <Header />
          {children}
        </ClientLayout>
        <Footer />
      </body>
    </html>
  );
};

export default RootLayout;

위의 코드를 보면 바로 Provider로 감싸는 대신 ClientLayout으로 감쌌는데, 이는 Redux가 클라이언트 컴포넌트에서만 작동하는 훅을 사용해야하기 때문이다. (useSelector, useDispatch)


여기서 잠깐


Redux는 클라이언트 사이드 상태 관리 (브라우저 환경에서 동작하는 상태 관리 도구) 라이브러리이기 때문에 "use client" 지시어가 필요하다.

("use client" 지시어는 초기 페이지 로딩 속도나 사용자 경험에 부정적인 영향을 미칠 가능성이 있다. 번들 크기가 커져 페이지 로드 시간이 증가하는 단점도 있다.)



보통 그래서 next-redux-wrapper 라이브러리를 사용한다.




Redux 사용 예시


컴포넌트에서 redux 상태와 액션 사용 방법이다.
useSelector와 useDispatch 훅을 이용하여 상태를 조회하고, 액션을 디스패치 할 수 있다.


// app/login/page.tsx
'use client';

import { useState } from 'react';
import { useDispatch } from 'react-redux';
import { login } from '../../features/userSlice';
import { useRouter } from 'next/navigation';

const LoginPage = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const dispatch = useDispatch();
  const router = useRouter();

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();

    // 실제로는 백엔드에서 로그인 API 호출
    const userData = {
      id: '123',
      name: 'John Doe',
      email: email,
    };

    // 로그인 성공 시 Redux에 사용자 정보를 저장
    dispatch(login(userData));

    // 대시보드로 리디렉션
    router.push('/dashboard');
  };

  return (
    <form onSubmit={handleLogin}>
      <div>
        <label>Email</label>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>
      <div>
        <label>Password</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          required
        />
      </div>
      <button type="submit">Login</button>
    </form>
  );
};

export default LoginPage;



Next.js에 Redux를 적용하며 마주하게 된 사사로운 오류사항들


가장 많이 언급되는 불편함: 새로고침 하면 날라가요 ,,,

이를 해결하기 위한 몇가지 방법이 있다.


아. 일단 왜 이런 문제가 발생하냐면, 브라우저를 새로고침 하면 페이지 전체를 다시 로드하기 때문에 Redux 상태가 초기화된다.


첫번째 해결방법) redux-persist 라이브러리 사용


간편해서 많이 사용하는 방법!

간략한 사용법 설명


1. 설치
npm install redux-persist

  1. 스토어 설정
// lib/store.ts
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // 기본적으로 localStorage 사용
import rootReducer from './rootReducer'; // 모든 리듀서를 합친 rootReducer

const persistConfig = {
  key: 'root',
  storage, // localStorage에 저장
};

const persistedReducer = persistReducer(persistConfig, rootReducer);

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

export const persistor = persistStore(store);

  1. Provider 설정

// app/layout.tsx
'use client';

import './globals.css';
import { Provider } from 'react-redux';
import { store, persistor } from '../lib/store';
import { PersistGate } from 'redux-persist/integration/react';
import Header from '../components/Header';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Provider store={store}>
          <PersistGate loading={null} persistor={persistor}>
            <Header />
            <main>{children}</main>
          </PersistGate>
        </Provider>
      </body>
    </html>
  );
}

redux-persist의 PersistGate를 사용하여, 스토어가 복원되기 전까지 앱을 지연시킬 수 있다.


만약 세션 중에만 상태를 유지하고 싶다면 redux-persist/lib/storage/session을 사용할 수 있다.


단점

  • 스토리지의 크기가 제한적이고, 복잡한 상태 관리에는 다소 비효율적일 수 있습니다.
    상태가 커질수록 브라우저 로딩 시간이 길어질 수 있음.



두번째 해결방법) localStorage나 sessionStorage 직접 사용

  1. store가 업데이트 될 때마다 localStorage에 상태 저장

// lib/store.ts
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';

const loadState = () => {
  try {
    const serializedState = localStorage.getItem('reduxState');
    if (serializedState === null) {
      return undefined;
    }
    return JSON.parse(serializedState);
  } catch (e) {
    console.error('Could not load state', e);
    return undefined;
  }
};

const saveState = (state: RootState) => {
  try {
    const serializedState = JSON.stringify(state);
    localStorage.setItem('reduxState', serializedState);
  } catch (e) {
    console.error('Could not save state', e);
  }
};

const preloadedState = loadState();

const store = configureStore({
  reducer: rootReducer,
  preloadedState, // 저장된 상태를 미리 로드
});

store.subscribe(() => {
  saveState(store.getState());
});

export default store;

장점

  • redux-persist보다 가볍고, 직접 제어 가능.
  • 상태가 단순한 경우 빠르게 적용 가능.


    단점
  • 수동으로 상태 직렬화/역직렬화를 관리해야 하므로 코드 복잡도가 약간 증가.
  • 상태가 클 경우 저장 성능에 영향을 줄 수 있음.


세번째 해결방법) 쿠키 사용


쿠키를 사용하여 상태를 저장할 수 있지만, 쿠키는 브라우저가 서버에 전송하는 용도로 주로 사용되므로 큰 데이터를 저장하는 데는 부적합하다. 그러나 인증 정보나 작은 상태값을 저장하는 데에는 적합.


import { parseCookies, setCookie } from 'nookies'; // nookies 라이브러리 사용
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './rootReducer';

const loadStateFromCookies = () => {
  const cookies = parseCookies();
  return cookies.reduxState ? JSON.parse(cookies.reduxState) : undefined;
};

const saveStateToCookies = (state: RootState) => {
  setCookie(null, 'reduxState', JSON.stringify(state), {
    maxAge: 30 * 24 * 60 * 60,
    path: '/',
  });
};

const preloadedState = loadStateFromCookies();

const store = configureStore({
  reducer: rootReducer,
  preloadedState,
});

store.subscribe(() => {
  saveStateToCookies(store.getState());
});

export default store;

장점

  • 인증 정보나 작은 상태 저장에 적합.
  • 서버 사이드 렌더링(SSR)과 쉽게 연동 가능.


    단점
  • 쿠키는 용량 제한이 있으므로 큰 데이터를 저장할 수 없음.
  • 보안 이슈가 있을 수 있으며, 민감한 정보는 적절히 암호화 필요.



네번째 해결방법) 서버 사이드에서 상태 저장


Next.js의 API Routes를 활용하여 서버에 상태를 저장하고, 필요할 때 API를 통해 불러오는 방법.


// pages/api/saveState.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method === 'POST') {
    // 상태를 데이터베이스나 파일 시스템에 저장
    // 예: db.save(req.body)
    res.status(200).json({ message: 'State saved' });
  } else {
    res.status(405).json({ message: 'Method not allowed' });
  }
}

새로고침 시 상태가 서버로부터 복원될 수 있도록 useEffect로 API를 호출하여 상태를 불러오고, 저장할 때는 API로 상태를 보낸다.


장점

  • 큰 데이터를 저장하거나, 보안이 필요한 상태 관리에 적합.
  • 서버에서 관리되므로 클라이언트 측의 저장 용량 한계를 걱정할 필요 없음.


    단점
  • 상태 저장 및 불러오는 로직이 복잡해질 수 있음.
  • 상태 저장/복원 시 네트워크 지연이 발생할 수 있음.


나는 위 방법들 중에 localStorage 직접 설정을 택했다.


redux-persist 는 나중에 사용해보도록 하자 ~



localStorage에 직접 설정하기


import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./rootReducer";

const loadState = () => {
  try {
    const serializedState = localStorage.getItem("reduxState");
    if (serializedState === null) {
      return undefined;
    }
    return JSON.parse(serializedState);
  } catch (e) {
    console.log("Could not load state", e);
    return undefined;
  }
};

const saveState = (state: RootState) => {
  try {
    const serializedState = JSON.stringify(state);
    localStorage.setItem("reduxState", serializedState);
  } catch (e) {
    console.error("Could not save state", e);
  }
};

const preloadedState = loadState();




export const makeStore = () => {
  return configureStore({
    reducer: rootReducer,
    preloadedState,
  });
};

makeStore.subscribe(() => {
    saveState(makeStore.getState());
  });



export type AppStore = ReturnType<typeof store>;

export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];

이런식으로 작성했더니 타입 오류가 났다.


makeStore 함수에 직접적으로 subscribe 메서드를 호출하려고 했기 때문이다.

makeStore 함수는 store를 반환하는 함수이지 store 자체가 아니다.

(makeStore 함수가 호출된 후에 반환된 store 인스턴스를 사용해야 함)


코드를 수정해준 후, 디버깅을 해보았는데 store를 참조하고 있지 않았다.

(그래서 자꾸 데이터가 보존되지 않고 날아감)


각 store는 name에 따라 식별되고 있는데, localStorage에서 가져온 이 데이터가 어느 store에 대한 데이터인지 매칭이 안되고 있었다.


따라서 아래와 같이 수정해주었다.

import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./rootReducer";

const loadState = () => {
  try {
    const serializedState = localStorage.getItem("reduxState");
    if (serializedState === null) {
      return undefined;
    }

    const parsedState = JSON.parse(serializedState);

    // 로컬 스토리지의 상태를 rootReducer의 구조에 맞게 변환
    return {
      userAuth: parsedState, // userAuth로 감싸기
    };
  } catch (e) {
    console.log("Could not load state", e);
    return undefined;
  }
};

const saveState = (state: RootState) => {
  try {
    const serializedState = JSON.stringify(state.userAuth); // userAuth만 저장
    localStorage.setItem("reduxState", serializedState);
  } catch (e) {
    console.log("Could not save state", e);
    return undefined;
  }
};

const preloadedState = loadState();

export const makeStore = () => {
  return configureStore({
    reducer: rootReducer,
    preloadedState,
  });
};

// Create store instance
const store = makeStore();

// 아래와 같이 하면 타입 오류가 발생
// const store =  configureStore({
//  reducer: rootReducer,
//  preloadedState
// })

// Subscribe to store updates and save state to localStorage
store.subscribe(() => {
  const state = store.getState();
  saveState(state);
});


export type AppStore = ReturnType<typeof makeStore>;
export type RootState = ReturnType<AppStore["getState"]>;
export type AppDispatch = AppStore["dispatch"];



이상으로 Redux 적용기 1탄을 마무리해보도록 하겠다.



velog 예쁘고 가독성 있게 작성하는거 왜케 어려워 ...

profile
나도 성장하고파

0개의 댓글