Library

[redux-persist] WebStorage에 light/dark mode 저장하기

반응형

아직 남아있는 문제

  1. WhiteList에 'mode'를 지정했음에도 counterReducer가 같이 저장되는 현상은 왜 발생하는 걸까?
  2. 이왕 Mode를 적용했으니 시각적인 효과도 주고 싶다. -> Design System에 대해 공부해야 함 

사용해보게 된 계기

WebStorage를 활용해야 하는 업무가 주어졌다고 하자, 멘토님이 redux-persist를 알려주셨다. 공부해보라고 하셨는데 프로젝트를 구성해서 직접 사용해보았다. 

나는 세팅된 redux를 사용해본적밖에 없기 때문에, redux 최초 세팅을 해보는 게 처음이었다. 생각해 보기로, redux-persist가 프로젝트에서 단독으로 쓰이기보다는 redux와 함께 쓰일 가능성이 매우 높을 것 같았다. 그래서 redux에서 예제로 자주 쓰는 Counter 컴포넌트를 살리고 거기다가 redux-persist를 활용하는 Mode(light/dark) 컴포넌트를 추가하기로 했다. 

App 컴포넌트 감싸주기 

먼저 redux-persist를 사용하기 위해, PersistGate를 import해서 App컴포넌트를 감싸주었다. 

// main.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import persistStore from './store/store.ts';

import { Provider } from 'react-redux';
import { PersistGate } from 'redux-persist/integration/react';

import * as serviceWorker from './serviceWorker';

const { store, persistor } = persistStore();

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate loading={<div>...loading</div>} persistor={persistor}>
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>
);

컴포넌트 만들기 

우선 간단하게 컴포넌트를 구성했다. Counter.tsx 는 제공된 컴포넌트를 쓱싹 잘라내서 +와 -기능만 남겼다. 

// /components/Counter.tsx

import {
  decrement,
  increment,
  selectCount,
} from '../slices/counter/counterSlice';
import styles from './Counter.module.css';
import { useDispatch, useSelector } from 'react-redux';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();

  return (
    <>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
      </div>
    </>
  );
}

export default Counter;

Mode.tsx 컴포넌트는 css도 입히지 않고 그냥 버튼 하나만 넣었다.

// /components/Mode.tsx

import * as React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { selectMode, setMode } from '../slices/mode/modeSlice';

enum MODE {
  LIGHT = 'light',
  DARK = 'dark',
}
const Mode = () => {
  const mode = useSelector(selectMode);
  const dispatch = useDispatch();

  const handleChangeMode: React.MouseEventHandler<
    HTMLButtonElement
  > = event => {
    if (mode === MODE.LIGHT) dispatch(setMode(MODE.DARK));
    else dispatch(setMode(MODE.LIGHT));
  };

  return (
    <button type="button" onClick={handleChangeMode}>
      {mode} 모드
    </button>
  );
};

export default Mode;

리듀서 슬라이스 만들기 

이번을 계기로 슬라이스의 개념에 대해 이해하게 되었다. 그동안 리듀서의 슬라이스라는 개념이 무엇인지 너무 헷갈렸는데 직접 코드를 짜보니까 슬라이스라는 것이 counter에 해당하는 리듀서만, mode에 해당하는 리듀서만 조각내놓은 'slice' 라는 것을 이해할 수 있었다.

// /slices/counter/counterSlice.ts

import { createSlice } from '@reduxjs/toolkit';
import { IRootState } from '../../store/states';

export const counterSlice = createSlice({
  name: 'counter',
  initialState: { count: 0 },
  reducers: {
    increment: state => {
      state.count += 1;
    },
    decrement: state => {
      state.count -= 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;

export const selectCount = (state: IRootState) => state.counter.count;

export default counterSlice.reducer;

  Mode의 슬라이스도 생성해 주었다.

// /slices/mode/modeSlice.ts

import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { IRootState } from '../../store/states';

export const modeSlice = createSlice({
  name: 'mode',
  initialState: {
    mode: 'light',
  },
  reducers: {
    setMode: (state, action: PayloadAction<'light' | 'dark'>) => {
      state.mode = action.payload;
    },
  },
});

export const { setMode } = modeSlice.actions;
export const selectMode = (state: IRootState) => state.mode.mode;

export default modeSlice.reducer;

Slice를 구성하면서, export const selectMode 와 같은 방식으로 원하는 상태값만 export 하는 방식을 사용할 수도 있다는 점을 알게 되었다. 이 때 조금 삽질했던 부분은 store.ts에서 counter와 mode, 두 리듀서 함수를 combineReducer를 통해 rootReducer로 묶어주었기 때문에 state도 IRootState를 사용해야 하고, 원하는 값을 명시할 때에도 state.mode가 아닌 state.mode.mode로 호출해줘야 한다는 점이었다. state 타입과 객체 참조값에서 우왕좌왕하면서 오래 헤맸다; 

Store 구성하기 

store는 리듀서 슬라이스들을 묶어서 내보내주는 곳이었다. redux-persist를 사용하기 위해 persistConfig를 설정해주어야 했고, 리듀서들을 combineReducers로 모아 rootReducer를 구성한 뒤에 persistReducer라는 이름으로 persistConfig와 rootReducer를 묶어주었다. combineReducers에서 리듀서를 묶어줄 때 약어를 지정해줄 수도 있고 풀네임을 그대로 쓸 수도 있었다. 

import { combineReducers, configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web
import modeReducer from '../slices/mode/modeSlice';
import counterReducer from '../slices/counter/counterSlice';

const persistConfig = {
  key: 'mode-persist', // Web Storage에 저장되는 키값이 됨
  storage, // 어떤 storage를 사용할 것인지 선택
  whiteList: ['mode'], // mode만 유지할 것이라는 명시
};

const rootReducer = combineReducers({
  mode: modeReducer,
  counter: counterReducer,
});

const persistedReducer = persistReducer(persistConfig, rootReducer);

export default () => {
  const store = configureStore({
    reducer: persistedReducer,
    middleware: getDefaultMiddleWare =>
      getDefaultMiddleWare({ serializableCheck: false }),
  });
  const persistor = persistStore(store);
  return { store, persistor };
};

State 관리하기

두 슬라이스에 대한 state들을 한번에 관리하기 위해 /store 디렉토리에 states.ts 파일을 생성했다. 무지성으로 추가하고 사용해온 IRootState가 어떻게 생성되었는지 알게 된 순간이었다...

// /store/states.ts 

export interface ICounterState {
  count: number;
}

export interface IModeState {
  mode: 'light' | 'dark';
}

export interface IRootState {
  mode: IModeState;
  counter: ICounterState;
}

한참의 삽질 후에 redux-persist가 반영된 프로젝트를 구성하게 되었다. 

Github 바로가기

 

GitHub - jazzo5947/color-theme-with-redux-persist: Color theme setting by redux-persist

Color theme setting by redux-persist. Contribute to jazzo5947/color-theme-with-redux-persist development by creating an account on GitHub.

github.com

 

728x90
반응형