티스토리 뷰

1️⃣ 브라우저db에 저장하여 북마크 정보 유지할 수 있도록 로컬스토리지 사용하기

redux-persist

redux-persist를 이용해서 구현했다. 사용법은 아주 간단하다.

// src/redux/index.js
import { combineReducers } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import bookmarkListSlice from './bookmarkListSlice';
import productListSlice from './productListSlice';

const persistConfig = {
  key: 'bookmark',
  storage,
  whitelist: ['bookmarkList'],
};

export const rootReducer = combineReducers({
  bookmarkList: bookmarkListSlice.reducer,
  productList: productListSlice.reducer,
});

export default persistReducer(persistConfig, rootReducer);
// src/redux/store.js
import { configureStore } from '@reduxjs/toolkit';
import persistedReducer from './index';
import persistStore from 'redux-persist/es/persistStore';

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

export const persistor = persistStore(store);
export default store;
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import store, { persistor } from './redux/store';
import { PersistGate } from 'redux-persist/integration/react';

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

 

 

redux-persist 제거

그런데 사실 로컬스토리지 로직은 어려운 작업이 아니다. 굳이 라이브러리를 쓰지 않아도 4줄의 코드만 추가함으로써 간단하다!

// src/redux/bookmarkListSlice
import { createSlice } from "@reduxjs/toolkit";
const bookmarkListSlice = createSlice({
  name: "bookmarkList",
  initialState: [],
  reducers: {
    add: (state, action) => {
      state.push(action.payload);
      localStorage.setItem("bookmarks", JSON.stringify(state)); // 로컬스토리지 업데이트
    },
    remove: (state, action) => {
      state.splice(
        state.findIndex((el) => el.id === action.payload),
        1
      );
      localStorage.setItem("bookmarks", JSON.stringify(state)); // 로컬스토리지 업데이트
    },
    init: (state, action) => action.payload, // App.js에서 로컬스토리지 데이터로 초기화하기 위한 리듀서
  },
});
export const { add, remove, init } = bookmarkListSlice.actions;
export default bookmarkListSlice;
// App.js
function App() {
  const dispatch = useDispatch();
  
  useEffect(() => {
    ...
   
    if (!localStorage.getItem("bookmarks")) localStorage.setItem("bookmarks", JSON.stringify([]));
    dispatch(init(JSON.parse(localStorage.getItem("bookmarks"))));
  }, []); // 앱 구동시 로컬스토리지의 bookmarks를 bookmarkList상태에 init하고 시작
  
  return (<>...</>)
  }



🙌 github commit 🙌

 

feat: redux-persist 삭제, 로컬스토리지 연결 구현 · hahagarden/fe-sprint-coz-shopping@b7d9e63

Show file tree Showing 8 changed files with 35 additions and 54 deletions.

github.com

 

 

2️⃣ 무한스크롤 구현하기

자바스크립트의 스크롤 이벤트를 사용하여 구현했다.
사실 무한스크롤을 목업?한 느낌이다.
스크롤 시 데이터를 api로 추가적으로 불러와서 렌더링하는 것이 아니라, 이미 데이터를 한꺼번에 다 받아오고, 데이터배열을 slice로 나눌 뿐이기 때문이다.

무한스크롤을 위한 상태는 currentList, currentIndex, isEnd이다. 
isEnd는 스크롤이 끝에 다다름을 감지하여 갱신되고,
currentIndex는 한번에 데이터를 30개씩 받아온다고 했을 때 초기값이 30이고,
currentList는 filteredList를 currentIndex부터 currentIndex+30까지 slice하여 기존의 상태에 추가한다.
isEnd가 갱신되면 currentList를 갱신하고, currentIndex도 30을 추가하여 갱신한다.

하단의 코드는 불필요한 로컬스토리지를 사용하였고, 너무 많은 상태를 사용하고 baseList를 useSelector로 불러오는 관계로 useEffect헬이 생겼다. (코드의 실행시점이 useState는 컴포넌트 렌더링 전, useSelector는 렌더링 후이기 때문에 useEffect를 사용해야 했다.)
추후 리팩토링을 하여 개선하였다.

function SubPageTemplate({ baseList }) {
  const DATA_PER_PAGE = 30;

  const [currentIndex, setCurrentIndex] = useState(DATA_PER_PAGE);
  const [filteredList, setFilteredList] = useState([]);
  const [currentList, setCurrentList] = useState([]);
  const [isEnd, setIsEnd] = useState(false);

  const handleScroll = () => {
    const scrollHeight = document.documentElement.scrollHeight;
    const scrollTop = document.documentElement.scrollTop;
    const clientHeight = document.documentElement.clientHeight;
    if (scrollTop + clientHeight >= scrollHeight) {
      setIsEnd(true);
    }
  };

  const addNextData = () => {
    if (isEnd) {
      setCurrentList([...currentList, ...filteredList.slice(currentIndex, currentIndex + DATA_PER_PAGE)]);
      setCurrentIndex(currentIndex + DATA_PER_PAGE);
      setIsEnd(false);
    }
  };

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    if (!getLocalStorage(StorageKey.FILTER_OPTION)) setLocalStorage(StorageKey.FILTER_OPTION, Types.ALL);
    return () => {
      window.removeEventListener('scroll', handleScroll);
      setLocalStorage(StorageKey.FILTER_OPTION, Types.ALL);
    };
  }, []);

  useEffect(() => {
    if (getLocalStorage(StorageKey.FILTER_OPTION) === Types.ALL) setFilteredList(baseList);
    else setFilteredList(baseList.filter((product) => product.type === getLocalStorage(StorageKey.FILTER_OPTION)));
  }, [baseList]);

  useEffect(() => {
    setLocalStorage(StorageKey.CURRENT_INDEX, DATA_PER_PAGE);
    setCurrentIndex(DATA_PER_PAGE);
    setCurrentList(filteredList.slice(0, currentIndex));
  }, [filteredList]);

  useEffect(() => {
    addNextData();
  }, [isEnd]);

  const handleFilterClick = (type) => {
    if (type === Types.ALL) setFilteredList(baseList);
    else setFilteredList(baseList.filter((product) => product.type === type));
    setLocalStorage(StorageKey.FILTER_OPTION, type);
  };

  console.log('rendered.');

  return (
    <SubPageWrapper>
      <FilterList handleFilterClick={handleFilterClick} />
      <ProductList products={currentList} />
    </SubPageWrapper>
  );
}
export default SubPageTemplate;

 

🙌 github commit 🙌

 

feat: 무한스크롤 상품 리스트 완성 by hahagarden · Pull Request #13 · hahagarden/fe-sprint-coz-shopping

이미 서버에서 전체 데이터를 받아왔기 때문에 '상품리스트 상태'에 저장해놓은 것을 불러오고, 리스트에 렌더링 할 배열은 '현재리스트 상태'입니다. 자바스크립트 스크롤 이벤트 이용하여 스

github.com

 

useState로 상태관리? localStorage에 저장?

상품리스트 페이지/북마크 페이지에 무한스크롤 기능이 들어간다. 그런데 이 페이지에는 리스트를 상품, 카테고리, 기획전, 브랜드별로 볼 수 있는 필터 기능도 있다. 이 때, 무한스크롤을 위한 상태와 필터를 위한 상태를 로컬스토리지에 저장할지 useState훅을 사용할지 고민했다.

로컬스토리지를 사용하는 경우는 새로고침을 했을 때 데이터를 유지시킬 것이냐의 여부이다. 

처음에 기능 구현시에는 useState와 로컬스토리지의 역할을 제대로 구별하지 않고 사용하다보니, 코드가 비효율적이었다.
useEffect에서 컴포넌트가 마운트될 때 로컬스토리지에 해당 데이터가 없으면 초기화를 한다.
그리고 사용자가 무한스크롤 또는 필터 기능을 이용하면 해당 데이터가 로컬스토리지에 업데이트된다.
컴포넌트가 언마운트될 때 로컬스토리지에 해당 데이터를 초기값으로 초기화한다.
그러므로 새로고침을 해도 기존 데이터를 유지하고 싶을 때 사용하는 로컬스토리지의 역할을 하지 않고 있다.
새로고침마다 컴포넌트가 언마운트/마운트되어 항상 초기값이기 때문이다.

처음에 로컬스토리지를 사용한 이유는, 사용자가 스크롤 중이나 필터를 적용한 후에 새로고침을 하여도 원래 있던 곳에 있게 하기 위함이었다. 로컬스토리지에 현재 스크롤 위치와 필터 값을 저장하여 불러오면 새로고침을 해도 원래 있던 곳에 위치하기 때문이다.
그런데 이렇게 하면 해당 페이지에서 새로고침 시에는 잘 되지만, 다른 페이지를 이동하고 나서 돌아올 때에도 마지막에 있던 위치로 돌아온다. 예를 들어서 메인 페이지를 갔다가 상품리스트 페이지로 돌아와도 스크롤이 마지막에 위치했던 곳으로 오는 것이다. 다른 페이지를 이동했다가 돌아올 때에는 기본 필터인 전체리스트, 스크롤은 맨 위에 위치해있는 것이 바람직하다.
때문에 컴포넌트 언마운트시 로컬스토리지의 필터, 스크롤위치를 초기값으로 초기화해주는 기능을 추가하였다. 이는 로컬스토리지의 기능을 퇴색시켰다.

현재는 로컬스토리지를 사용하지 않고 useState로 지역상태를 사용한다. 이는 새로고침시에는 항상 초기화되어 내가 원하던 기능과는 다르지만 새로고침을 하지 않는다면 싱글 페이지 어플리케이션으로서는 스크롤도 잘 되고 필터 기능도 잘 되기 때문에 문제가 없다.

내가 원하는대로 동작하도록 만들기 위해서는 라우팅을 이용하면 될 것 같다.
상품, 카테고리, 기획전, 브랜드마다 엔드포인트를 두고 다른 페이지로 만들어서 필터를 클릭하면 페이지를 이동한다.
그리고 로컬스토리지에 필터별로 스크롤위치를 저장할 수 있도록 객체를 저장하여 관리하고, 필터 페이지 마운트 시 다른 필터들의 스크롤 위치는 초기값으로 초기화하고 해당 필터에서는 스크롤 위치를 계속 업데이트하여 유지하는 방법으로 할 수 있을 것이다.
그러면 해당 필터페이지를 새로고침하여도 원래 있던 위치로 갈 것이고, 다른 페이지를 다녀오면 0으로 되어있을 것이다.
하지만 여전히 문제는 있어보인다. 메인페이지를 다녀온다면? 필터페이지끼리 이동이 아니라 다른 여러 페이지들을 다녀온다면? 페이지마다 로컬스토리지의 모든 필터의 스크롤위치 정보를 0으로 초기화하는 함수를 호출할 것인가?
결론적으로 SPA이고 새로고침만 하지 않는다면 스크롤한 상태에서 북마크 값을 조작하여도 반영이 잘 되고 스크롤도 이동하지 않기때문에 굳이 필요한 기능은 아닌 것 같다. 

 

 

반응형
댓글