LocalStorage, SessionStorage 그리고 Redux-Persist

률루랄라·2020년 5월 24일
21
post-thumbnail
post-custom-banner

왜?

application을 만들면서 아마 대부분 localhost에 화면을 띄워놓고 잘 작동중인지 확인을 하며 작업을 하게 될 것이다.
이때 새로고침을 눌러보면 application은 최초의 코드로 돌아가게 된다. 특히 state에 변화된 내용들도 initial state로 다시 돌아가는 것을 알 수 있다.
하지만 이미 서비스 중인 사이트에 들어가서 새로고침을 해보면 조금 다른것을 알 수 있다.
예를 들어 자주 사용하는 인터넷 쇼핑몰 사이트에 가서 로그인을 하고 장바구니에 상품들을 추가해보자.
그 후 페이지를 새로고침하면 어떻게 되는가?
아마 장바구니는 새로고침을 누르기 전 상태 그대로 view에 나타날 것이다.
장바구니에 추가할 때 마다 장바구니를 데이터 베이스로 전송해 저장하여 사용하는 등 여러가지가 있다. redux를 사용중이라면 간단하게 Redux-persist를 설치하여 사용하면 된다.
하지만 먼저 redux-persist 안에서 작동이 어떻게 이루어 지는지 브라우저에서 기본적으로 제공해주는 저장소인 localstoragesessiongstorage에 대해서 알아보고 이 두개의 저장소를 redux에서 사용하게 해주는
Redux-persist에 대해서 알아보고 적용해보자.
(localstorage와 sessionstorage에 대해 잘 알고있다면 넘어가도 괜찮다.)


0. 기본 application 구성

나는 바나나와 사과를 좋아한다. 그래서 일주일에 얼마나 많은 사과와 바나나를 먹는지 궁금해서 application을 제작하였다.
사과와 바나나를 먹을 때 마다 application에 접속해서 버튼을 눌러서 확인하려한다.
그리고 application은 stateredux를 사용하여 관리하고 selector를 사용하여 state를 컴포넌트로 넘겨주었다.

컴포넌트 구성

// app.js
import React from 'react';
import './App.css';

import AppleButton from './components/apple-button.component';
import BananaButton from './components/banana-button.component';

const App = () => {
  return (
      <div className="foodlist-container">
        <AppleButton title="apple" />
        <BananaButton title="banana" />
    </div>
  );
};

export default App;

// apple-button.component.jsx
import React from 'react';
import { connect } from 'react-redux';
import { AddAppleCount } from '../redux/food/food.actions';
import { selectCurrentAppleCount } from '../redux/food/food.selectors';

const AppleButton = ({ AddAppleCount, appleCount, title }) => {
  return (
    <div>
      <button type="button" onClick={() => AddAppleCount(title)}>
        {title}
      </button>
      <div>{appleCount}</div>
    </div>
  );
};

const mapStateToProps = state => ({
  appleCount: selectCurrentAppleCount(state),
});

const mapDispatchToProps = dispatch => ({
  AddAppleCount: title => dispatch(AddAppleCount(title)),
});
export default connect(mapStateToProps, mapDispatchToProps)(AppleButton);


//banana-button.component.jsx
import React from 'react';
import { connect } from 'react-redux';
import { AddBananaCount } from '../redux/food/food.actions';
import { selectCurrentBananaCount } from '../redux/food/food.selectors';

const BananaButton = ({ AddBananaCount, bananaCount }) => {
  console.log(bananaCount);
  return (
    <div>
      <button type="button" onClick={() => AddBananaCount('바나나')}>
        바나나
      </button>
      <div>{bananaCount}</div>
    </div>
  );
};

const mapStateToProps = state => ({
  bananaCount: selectCurrentBananaCount(state),
});

const mapDispatchToProps = dispatch => ({
  AddBananaCount: banana => dispatch(AddBananaCount(banana)),
});
export default connect(mapStateToProps, mapDispatchToProps)(BananaButton);

redux 구성

// 경로: src/redux/food
// food.types.js
const FoodActionTypes = {
  ADD_APPLE: 'ADD_APPLE',
  ADD_BANANA: 'ADD_BANANA',
};

export default FoodActionTypes;


//food.actions.js
import FoodActionTypes from './food.types';

export const AddAppleCount = title => ({
  type: FoodActionTypes.ADD_APPLE,
  payload: title,
});

export const AddBananaCount = banana => ({
  type: FoodActionTypes.ADD_BANANA,
  payload: banana,
});


//food.reducer.js
import FoodActionTypes from './food.types';

const INITIAL_STATE = {
  apple: [],
  banana: [],
};

const foodReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case FoodActionTypes.ADD_APPLE:
      return {
        ...state,
        [action.payload]: state[action.payload].concat(action.payload),
      };
    case FoodActionTypes.ADD_BANANA:
      return {
        ...state,
        banana: state.banana.concat(action.payload),
      };
    default:
      return state;
  }
};

export default foodReducer;


//food.selectors.js
import { createSelector } from 'reselect';

const selectFood = state => state.foodList;

export const selectCurrentApple = createSelector(
  [selectFood],
  foodList => foodList.apple
);

export const selectCurrentBanana = createSelector(
  [selectFood],
  foodList => foodList.banana
);

export const selectCurrentAppleCount = createSelector(
  [selectCurrentApple],
  apple => apple.length
);

export const selectCurrentBananaCount = createSelector(
  [selectCurrentBanana],
  banana => banana.length
);

// src/redux/root-reducer.js
import { combineReducers } from 'redux';

import foodReducer from './food/food.reducer';

const rootReducer = combineReducers({
  foodList: foodReducer,
});

export default rootReducer;


// src/redux/store.js
import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';
import rootReducer from './root-reducer';

const middlewares = [logger];
const store = createStore(rootReducer, applyMiddleware(...middlewares));

export default store;


// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';

import { Provider } from 'react-redux';
import App from './App';

import store from './redux/store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

1. localstorage와 sessionstorage

최종적으로 사용하려는 redux-persist 라이브러리는 redux에서 localstorage와 sessiongstorge에 state를 저장하고 reducer를 업데이트하게 해주는 기능을 제공한다.
redux-persist를 사용하면 localstorage에 state를 저장하여 (INITIAL_STATE 상태라면 그대로, reducer가 변화를 줬다면 변화된 state 상태로) 브라우저에 변화 (탭을 닫거나 새로고침)가 있더라도 저장된(caching된) 데이터로 reducer가 업데이트 해주는 기능을 제공해준다.

그럼 redux-persist를 사용하기에 앞서 localstorage와 sessiongstorage에 대해서 알아보자.


이 두개의 저장소는 브라우저에서 기본적으로 제공하는 저장소(storage)이다. 이 저장소들은 application 전역에서 접근이 가능하다. 하나의 특징은 storage에 저장된 data는 JSON형태여야 하고 저장소에서 막 꺼낸 데이터는 JSON형태라는 것이다.

localstorage와 sessionstorage는 비슷하지만 하나의 차이점이 있다.
localstorage는 우리가 직접 지우기 전까지 저장되는 반면 sessionstorage는 session이 유지되는 한에서만 데이터가 유지된다.
session이란 브라우저의 탭 이라고 생각하면 편하다. 탭을 닫지않는 이상 essiongstorage에 저장된 데이터는 얼마든지 접근이 가능하다(새로고침 포함). 하지만 브라우저 탭을 닫게되면 sessionstorage에 저장된 데이터는 사라진다.

아래의 예제를 통해 사용법을 알아보자.
1. 저장하기

우선 변수에 저장하고자 하는 데이터를 객체로 할당해준다.
그다음 window.localStorage를 통해 localstorage에 접근한다.
그 후 setItem메서드를 사용하여 localstorage에 방금 선언한 데이터를 저장하려고 한다.
setItem메서드는 두 개의 매개변수가 필요하다. 첫번째 매개변수는 key로서 string형태여야 한다. 두번째 매개변수는 저장할 데이터인데 JSON형태여야 한다. 그래서 JSON.stringify를 통해 우리의 데이터를 변환시켜 함께 넣어주었다.
sessiongstorage에 저장할때도 매서드는 동일하다.

  1. 저장된 데이터에 접근하여 사용하기

    getItem메서드를 사용하여 저장된 데이터를 확인할 수 있다. 이때 각 데이터에 맞는 키값을 string형태로 꼭 넣어줘야한다.
    다시 말하지만 localstorage와 sessionstorage에 저장된 데이터는 json형태이다. 그렇기 때문에 JSON.parse메서드를 사용하여 데이터 형태를 복구시켜준다.

이렇게 각 상황에 맞게 저장소에 데이터를 저장한 후 꺼내와 사용할 수 있다. 저장소를 사용하면 매번 database와 통신할 필요가 없어지고 간단하게 데이터를 보존할 수 있는 등의 장점이 있다.
그럼 redux에서는 어떻게 사용해야 할까?

2. Redux-persist

redux에서 localstorage와 sessionstorage에 접근할 수 있는 기능을 제공하는 library이다. 사용법도 간단하니 가볍게 정리해보자.

npm install redux-persist
위 코맨드로 설치해주고 store.js에 가서 코드를 수정해주자.

// src/redux/store.js

import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';
1️⃣import { persistStore } from 'redux-persist';
// browser가 밑에 작성할 config에 관해 이 store를 cache할 수 있게 접근을 허용해준다
import rootReducer from './root-reducer';

const middlewares = [logger];
export const store = createStore(rootReducer, applyMiddleware(...middlewares));

2️⃣ export const persistor = persistStore(store);
// store의  persisted 버젼을 선언해준다.
//  이제 이 persistor와 store를 사용하여 application을 감싸고 있는 provider를 새롭게 만들어줄 것이다.

3️⃣export default { store, persistor };
// store와 persistor를 객체로 export default해줘서 다른 파일에서 사용할 수 있게 해준다.

그리고 root-reducer.js파일을 아래와 같이 수정해준다.

// src/redux/root-reducer.js

import { combineReducers } from 'redux';
1️⃣ import { persistReducer } from 'redux-persist';
// reducer를 persist할 수 있게 해주는 persistReducer import
2️⃣ import storage from 'redux-persist/lib/storage';

// type of store
// localstorage 경로
// : import storage from 'redux-persist/lib/storage'
// Redux-persist한테 나는 localstorage를 사용할 것이라고 알려주는것

// sessiongstorage 경로
// : import storageSession from 'redux-persist/lib/storage/session';
// Redux-persist한테 나는 sessionstorage를 사용할 것이라고 알려주는것

import foodReducer from './food/food.reducer';

3️⃣ const persistConfig = {
  // 새로운 persist config를 선언해준다.
  key: 'root',
  // reducer 객체의 어느 지점에서 부터 데이터를 저장할 것인지 설정해주는것이 key이다.
  // root부터 시작한다고 지정해준다.
  storage: storage,
  // 위에 import 한 성격의 storage를 지정해준다. 이 예제의 경우에는 localstorage
  whitelist: ["foodList"],
  // 유지 및 보존하고 싶은 데이터를 배열안에 지정해준다. 
  // string 형태이고 아래 combineReducers에 지정된 값들을 사용해주면 된다. 
};

const rootReducer = combineReducers({
  count: countReducer,
  foodList: foodReducer,
});

4️⃣ export default persistReducer(persistConfig, rootReducer)
// persistReducer 함수안에 persistConfig와  rootReducer를 넣어서 export default 해준다.
// 이 뜻은 수정된 rootReducer를 persistConfig의 조건에 맞게 persist하여 export 하겠다는 뜻이다. 
// 단순하게 rootReducer에 persist능력을 추가해준것이다. 

이렇게 수정하면 된다. 그 후 application 전체가 있는 index.js 파일을 아래와 같이 수정해준다.

// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
1️⃣ import { PersistGate } from 'redux-persist/integration/react';
// PersisGate를 import 해주고

import { Provider } from 'react-redux';
import App from './App';

import * as serviceWorker from './serviceWorker';
2️⃣ import { store, persistor } from './redux/store';
// store.js에서 persistor를 import 해준다.

3️⃣ // 아래에 application의 최상단 컴포넌트를 PersistGate로 감싸주고 props로 persistor를 넘겨준다.
ReactDOM.render(
  <Provider store={store}>
    3️⃣ <PersistGate persistor={persistor}>
      <App />
    </PersistGate>
  </Provider>,
  document.getElementById('root')
);

이렇게 수정하고 localhost에 화면을 눌러 버튼을 여러번 클릭해준 후 새로고침을 해보자.
숫자는 그대로 일 것이다. 심지어 탭을 닫고 다시 열어도 그대로이다.
이제 사과와 바나나 섭취 누적량을 언제든지 확인해볼 수 있게 되었다.

profile
💻 소프트웨어 엔지니어를 꿈꾸는 개발 신생아👶
post-custom-banner

4개의 댓글

comment-user-thumbnail
2020년 5월 24일

👍

답글 달기
comment-user-thumbnail
2020년 8월 6일

우왕! 덕분에 로덕스 잘 적용했습니다! 정말 감사합니다!😍👍
그런데 제목에 오타가...! SessiongStorage 라고 되어있네욥ㅎㅎㅎ🤷🏻‍♀️

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

대단히 고맙습니다. 큰 도움이 되었습니다 :)

답글 달기