(7) Todo List Redux - Local Storage로 state 리팩토링

2023. 6. 9. 22:21Github 프로젝트/todolist반응형웹앱

728x90
반응형
SMALL

지금까지 CRUD 전체를 다루는 연습을 하였습니다.

이번 시간에는 Redux 설치 및 적용, Local Storage를 다루는 시간으로 활용 하겠습니다.

지금까지 따라오신 후 이 글을 확인하시기 바랍니다.

 

(6) Todo List 기본 기능(삭제) : https://berkley.tistory.com/10

 

(6) Todo List 기본 기능 CRUD 추가 - 3 (삭제)

지난시간에는 갱신의 대해서 다뤄봤습니다. 갱신 부터 진행 후 다음은 삭제에 대해 다루겠습니다. (5) Todo List 기본 기능 CRUD 추가 - 2 (갱신) : https://berkley.tistory.com/9 (5) Todo List 기본 기능 CRUD 추가

berkley.tistory.com

 

 

그럼 작업 전 Redux와 Storage 방식의 대해 설명드리겠습니다.

1. Redux란?

각 모듈마다 state문을 사용을 하여 사용하였습니다.

리액트를 이용하여 SPA 기능으로 이용하여 렌더링 속도를 빠르게 진행을 해 나갈 수 있는데, 하나의 모듈에서 state 값으로 집어 넣고, 자식 모듈에게 state값을 넘기면서 진행하면 결합도가 높아지고, 유지보수 난이도가 높아지는 것을 확인할 수 있습니다.

또한 새로 고침 시 state 값은 다시 초기화 되어 사용하기 때문에 새로고침을 하지 않고 state 값을 유지 시키기 위해 Redux를 사용합니다.

대표적으로 로그인/로그아웃 기능의 예시로 활용할 수 있습니다.

리덕스 같은 경우는 먼저 초기 state값을 설정을 하고, state 변경 내역을 초기화 되지 않게 해줍니다.

여기서 Redux 사용 방식은 Session Storage를 이용한 방식과 Local Storage를 이용하는 방식으로 사용이 가능합니다.

현재 설명 할 분량이 많으므로 Session Storage와 Local Storage만 간략하게 설명하고, Todolist 프로젝트를 도입하겠습니다.

 

2. Session storage vs local storage

Redux state 값을 저장하기 위해 Session Storage와 Local Storage 방식으로 값을 유지 시킬 수 있습니다.

각각 차이점을 설명 드리겠습니다.

- Session Storage : 말그대로 Session에 저장되기 때문에 웹 브라우저를 닫을 시 state 값이 초기화 됩니다. 주로 로그인/로그아웃 방식으로 이용 할 수 있습니다.

- Local Storage : domain/port/protocol가 같은 경우 사용자가 직접 조작하지 않는 이상, OS 재부팅, 웹브라우저를 닫아도 지속적으로 유지 됩니다. 이 점을 활용하여 todolist를 공유할 수 있습니다. 주로 인트라넷이나 제한된 사용자들끼리 사용하기에는 적합합니다.

따라서 필자는 Local Storage를 이용하고 Redux의 5대 요소를 간단히 파헤친 후에 작업을 진행하겠습디ㅏ..



3. Redux 5대 요소 

  • Action, Reducer, Store, Dispatch, Subscribe

 

==> Redux의 개념 참조 사이트 : https://wonit.tistory.com/344

 

 

4. Redux Local storage로 state 값으로 Refactoring 하기

4-1) 사전 작업

먼저 npm에서 redux와 redux와 react-redux, redux-persist를 설치합니다.

 

 

redux 관련 설치 후 package.json 에서 네모 박스에 설치가 되어있는지 확인

 

 

4-2) Index.js에서 redux 환경으로 설정 작업

현재 상태에서 아래와 같이 default로 설정 되어 있습니다.

이것은 맨 처음 설치 시 기본으로 설정 되어있습니다.

 

<수정 전 코드>

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
   <React.StrictMode>
     <App />
   </React.StrictMode>
);

 

 

 

이것을 Redux 셋팅을 위한 환경 설정을 진행한다. 

Redux 사용 시 핵심이 되는 기술이 store이고,

Provider를 통해 수많은 컴포넌트들이 Store에 접근 할 수 있게 하는 기능으로 구성한다.

redux-persist 의 라이브러리 중 하나인 PersistGate는 App 컴포넌트를 PersistGate로 매핑하고, persistStore가 redux에 저장때 까지 앱 UI 렌더링 지연시키는 역할을 수행 하도록 해주는 구조로 정의 하였다.

persistStore 같은 경우는 새로 고침 or 웹 서비스 종료 시에도 지속될 store 생성 해주는 기능을 수행한다.

 

 

<수정후>

import React from "react";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

import { createRoot } from "react-dom/client";
import { persistStore } from "redux-persist";
import { PersistGate } from "redux-persist/integration/react";
import store from "./redux/store";
import { Provider } from "react-redux";

const persistor = persistStore(store);

const container = document.getElementById("root");
const root = createRoot(container);
root.render(
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
      <App />
    </PersistGate>
  </Provider>
);

reportWebVitals();

 

 

여기까지 설정을 하였으면 store 부터 설정하고, reducer와 action을 모듈을 추가해준다.

(지금까지는 store.js가 없으므로 아직 에러 발생)

 

 

 

4-3) redux에서  store, reducer, action을 각 모듈별로 구현하여 지정

 

 

- store.js : createStore를 통해 store를 생성 해주는 기능으로 reducer가 있는 위치를 생성해줍니다.

import rootReducer from './reducer';
import { legacy_createStore as createStore } from "redux"

const store = createStore(rootReducer);

export default store;

 

- reducer.js : 먼저 persistConfig를 통해 새로운 persist를 선언하고, key 값은 reducer의 어느 지점에서부터 데이터를 저장할 것인지, storage는 Local인지 Session인지 정합니다. 

만일 SessionStorage로 저장하고 싶을 경우 import storage from "redux-persist/lib/storage/session"; 으로 하시면 되고 LocalStorage로 저장하고 싶을 경우 import storage from "redux-persist/lib/storage";로 하시며 됩니다. 그리고, action에서 각각 state 별로 나눠 진행 하고 싶을 경우 combineReducer를 사용하여 각 action을 결합하여 사용하시고, persistReducer를 통해 persist를 환경 설정하고, 결합된 reducer를 사용을 합니다.

import { combineReducers } from "redux";
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
// import storage from "redux-persist/lib/storage/session";

import todoListReducer from "./action/todolist.js"


const persistConfig = {
  key: "root",
  // localStorage에 저장합니다.
  storage,
  // whitelist: ["user"]
  // blacklist -> 그것만 제외합니다
};

export const rootReducer = combineReducers({
  todoList:todoListReducer,
});

export default persistReducer(persistConfig, rootReducer);

 

 

- action 디렉토리 : reducer의 각 state와 action을 취하고 싶을 경우 각각의 모듈로 나눠 사용 할수 있습니다.

회원 정보의 관련 state는 user.js, 인증의 관련 정보는 auth.js를 생성해 각각 행위를 취할 수 있는 메서드와 state를 이용하여 사용 가능하고, 필자는 todolist의 대한 state와 행위를 취하기 위해 todolist.js를 이용하여 구현하였습니다. 

만일 각각 reducer를 설정시 combineReducers에 설정합니다.

 

reducer.js에 삽입할 코드 예시

combineReducers({

  todolist:todolistReducer,

  user:userReducer

})

 

 

현재, todolist.js 생성 후, 기본 셋팅 

// 리덕스 state 초기 셋팅
const InitState = {
  array: [
    // {
    //   title: undefined,
    //   contents: undefined,
    // },
  ],
};

const todoListReducer = (state = InitState, action) => {
  switch (action.type) {
    // case 문 입력 시 해당 state 문 변경 - 차후 다룰 예정
    // setTodoList, getTodoList 인 경우는 샘플 case이므로 
    // 리덕스를 다루면서 CRUD 적용을 합니다.
    case "setTodoList":
      return {
        title: !!action.title ? action.title : state.title,
        contents: !!action.contents ? action.contents : state.contents,
      };
    case "getTodoList":
      return { ...state };
    // case 문 입력 시 해당 state문 변경 끝 
    default:
      return state;
  }
};

export default todoListReducer;

 

 

이에 대한 redux-persist의 대해 이해가 부족하실 경우 franchesca님의 글을 참조바랍니다. 

참조 : https://velog.io/@franchesca/Redux-persist-%EB%9E%80

 

 

 

5. Redux Local storage로 state 값으로 Refactoring 하기 

5-1)  다음은 App.js 에서 기존에 todolist를 구현된 코드를 Redux 형식으로 리팩토링 작업 실시

먼저 useState()를 사용 한 것을 Redux 타입으로 변경하기  useSelector()로 리팩토링을 해줍니다. (현재 리덕스로 이용해서 데이터 제거를 하는 방법에 대해 설명 드리기 위해 useEffect() 제거

 

(1) useState() -> useSelector() 로 변경

 

<수정 전>

const [todoList, setTodoList] = useState([]);

 

<수정 후>

const todoList = useSelector(state => state.todoList.array);

 

 

 

(2) useEffect() 에 샘플 데이터를 넣은 것을 제거하고, 샘플 데이터를 todoLlst.js에 InitState에 데이터를 삽입하여 Redux State에 저장

<App.js 코드에 useEffect() 제거>

  useEffect(() => {
    setTodoList([
      {
        title: "Redux 학습하기",
        contents: [
          "로그인/로그아웃 가능 여부 확인",
          "localstorage/sessionstorage 학습",
        ],
      },
      {
        title: "SpringBoot 학습하기",
        contents: ["Spring Security 구현", "Spring MVC 패턴 익히기"],
      },
      {
        title: "JWT 인증 처리",
        contents: ["JWT 개념 익히기"],
      },
    ]);
  }, []);

 

<action/todolist.js 에 샘플 데이터 InitState 변경>

// 샘플 데이터 넣고 추가시
const InitState = [
  {
    title: "Redux 학습하기",
    contents: [
      "로그인/로그아웃 가능 여부 확인",
      "localstorage/sessionstorage 학습",
    ],
  },
  {
    title: "SpringBoot 학습하기",
    contents: ["Spring Security 구현", "Spring MVC 패턴 익히기"],
  },
  {
    title: "JWT 인증 처리",
    contents: ["JWT 개념 익히기"],
  },
]

 

 

여기 까지 진행을 하셨으면 Redux state가 정상 작동 되고 읽을 수 있는 것을 확인 할 수 있습니다.

 

 

 

다음으로 쓰기, 갱신, 삭제에 관련된 알고리즘 셋팅의 용도로 쓰기 위해 

todolist.js에서 action 함수 작성 (기존 샘플 case 지우고 아래와 같이 수정)

dispatch로 작성한 type:"setTodoList"로 이용하여 state 값을 반영시키는 역할을 한다.

const todoListReducer = (state = InitState, action) => {
  switch (action.type) {
    /**
     * setTodoList : redux state 값을 바꿔주는 기능으로 App.js에서 해당 알고리즘 crud 사용하여 변경 
     */
    case "setTodoList":
      return {
        array: !!action.array ? action.array : state.array,
      };
    default:
      return state;
  }
};

 

6. action 함수 구현 CRUD

6-1) 다음은 쓰기 기능의 대해 리팩토링을 해보자.

action type을 setTodoList를 이용하여 switch문을 활용해 state 값을 변경 시킬 수 있습니다.

이 경우 쓰기, 삭제, 갱신 작업을 진행 할 수 있습니다.

App.js 에서 쓰기를 useState를 사용하여 작성했던 코드를 action 함수를 구현하고 dispatch로 이용하여 기능을 구현한다.

 

메서드 createTitleButton을 ustState() 방식으로 사용된 setTodoList(to); 제거하고, dispatch({type:"addTitle", array:to}) 로 변경한다.

 

App.js

  /**
   * 입력시 Todolist 추가하는 기능
   */
  const createTitleButton = () => {
    let to = [];
    to.push(...todoList);
    to.push({
      title: createInputTitle?.title,
      contents: [],
    });
    // setTodoList(to); // 이함수 제거
    /**
     * dispatch는 todolist.js 의 reducer의 action으로 사용되는 함수로
     * state 값을 변경시켜준다.
     * redux의 state 값은 불변성을 유지하는 것이 목적이다.
     * switch문의 action.type 변수에 따라 return으로 명령어대로 해당 state 값을 변경 시켜준다.
     */
    dispatch({type:"setTodoList", array:to})
  };

 

이후, 추가 기능이 동작하는 것을 확인 할 수 있습니다.

 

 

 

 

6-2) 다음은 갱신 기능의 대해 리팩토링을 해보자.

App.js에서 갱신 기능의 대해 Redux 스타일로 리팩토링 해본다.

먼저 useState()를 사용된 setTodoList()를 제거해 주고 dispatch()로 이용하여 적용 시킵니다. 

  // 수정 부분 입력 후 갱신 메서드
  const updateTitle = (index) => {
    /**
     * 불변성 유지하면서 갱신 시킬 수 있습니다.
     */
    dispatch({
      type: "setTodoList",
      array: update(todoList, {
        [index]: {
          title: { $set: changeTitle[index].title },
        },
      }),
    });
    // setTodoList(
    //   update(todoList, {
    //     [index]: {
    //       title: { $set: changeTitle[index].title },
    //     },
    //   })
    // );
  };

 

 

이 경우 갱신이 동작 되는 것을 확인 할 수 있습니다.

 

 

6-3) 다음은 삭제 기능의 대해 리팩토링을 해보자.

마찬가지로 쓰기, 삭제, 갱신의 역할을 수행 가능합니다.

useState()로 이용하여 쓰여진 setTodoList()를 useDispatch() 기능으로 이용하여 state 값 변환(삭제기능)을 수행 가능합니다.

  // 삭제 관련 메소드 (제목)
  const removeTitle = (index) => {
    /**
     * 불변성 유지하면서 삭제 시킬 수 있습니다.
     */
    dispatch({
      type: "setTodoList",
      array: update(todoList, {
        $splice: [[index, 1]],
      }),
    });
    // setTodoList(
    //   update(todoList, {
    //     $splice: [[index, 1]],
    //   })
    // );
  };

 

역시 삭제 기능이 활성화 상태로 정상 작동합니다.

 

 

7. 완성된 코드

 

(1)  App.js

import {
  Button,
  Card,
  CardBody,
  CardHeader,
  CardTitle,
  Form,
  Input,
} from "reactstrap";
import "./App.css";
import { PlusCircle, Trash3 } from "react-bootstrap-icons";
import { useState } from "react";
import update from "immutability-helper";
import { useDispatch, useSelector } from "react-redux";

function App() {
  /**
   * 필요 key:value값
   * title, contents
   * 이 값은
   */
  const todoList = useSelector((state) => state.todoList.array);
  const dispatch = useDispatch();
  const [createInputTitle, setCreateInputTitle] = useState();
  // 갱신모드 설정
  const [isTitleUpdate, setIsTitleUpdate] = useState(false);

  // Todolist title 수정용 state
  const [changeTitle, setChangeTitle] = useState();

  /**
   * 다음은 TodoList 입력용 이벤트 함수
   */
  const createTitleOnchange = (e) => {
    const { name, value } = e.target;
    setCreateInputTitle({
      ...createInputTitle,
      [name]: value,
    });
  };
  /**
   * 입력시 Todolist 추가하는 기능
   */
  const createTitleButton = () => {
    let to = [];
    to.push(...todoList);
    to.push({
      title: createInputTitle?.title,
      contents: [],
    });
    /**
     * dispatch는 todolist.js 의 reducer의 action으로 사용되는 함수로
     * state 값을 변경시켜준다.
     * redux의 state 값은 불변성을 유지하는 것이 목적이다.
     * switch문의 action.type 변수에 따라 return으로 명령어대로 해당 state 값을 변경 시켜준다.
     */
    dispatch({ type: "setTodoList", array: to });
  };

  // 수정용 메서드
  const updateTitlOnChange = (e, index) => {
    const { name, value } = e.target;
    setChangeTitle({ [index]: { [name]: value } });
    console.log(changeTitle);
  };

  // 수정 부분 입력 후 갱신 메서드
  const updateTitle = (index) => {
    /**
     * 불변성 유지하면서 갱신 시킬 수 있습니다.
     */
    dispatch({
      type: "setTodoList",
      array: update(todoList, {
        [index]: {
          title: { $set: changeTitle[index].title },
        },
      }),
    });
  };

  // 삭제 관련 메소드 (제목)
  const removeTitle = (index) => {
    /**
     * 불변성 유지하면서 삭제 시킬 수 있습니다.
     */
    dispatch({
      type: "setTodoList",
      array: update(todoList, {
        $splice: [[index, 1]],
      }),
    });
  };

  return (
    <div className="my-2 background-container">
      <Card className="my-2 container">
        <CardHeader className="header-container">
          To-do list 반응형 웹 개발
        </CardHeader>
        <CardBody>
          <CardTitle tag="h2">To-do list</CardTitle>
          <div className="todo">
            {
              // 다음은 각각 데이터를 불려올 때 map을 주로 사용합니다.
              // todoList에 저장된 state 값을
              // 유사 forEach문처럼 todo를 이용하여 출력하고,
              // 상위 div에 key값을 설정하기 위해 index 값을 집어 넣어야 합니다.
              todoList?.map((todo, index) => (
                <div key={index} className="todoContainer">
                  <div className="todoTitle">
                    {isTitleUpdate ? (
                      <div>
                        <Input
                          name="title"
                          defaultValue={todo?.title}
                          onChange={(e) => updateTitlOnChange(e, index)}
                        />
                        <Button onClick={() => updateTitle(index)}>
                          todolist수정
                        </Button>
                        <Button
                          onClick={() => setIsTitleUpdate(!isTitleUpdate)}
                        >
                          취소
                        </Button>
                      </div>
                    ) : (
                      <div>
                        {todo?.title}
                        <Button
                          onClick={() => setIsTitleUpdate(!isTitleUpdate)}
                        >
                          수정
                        </Button>{" "}
                        <PlusCircle />
                        <Button onClick={() => removeTitle(index)}>
                          <Trash3 />
                        </Button>
                      </div>
                    )}
                  </div>

                  {todo?.contents?.map((tc, tcIndex) => (
                    <div key={tcIndex} className="todoContents">
                      - {tc} <Trash3 />
                    </div>
                  ))}
                </div>
              ))
            }
          </div>

          {/**
           *
           */}
          <Form className="addGroup">
            <Input
              className="addInput"
              name="title"
              defaultValue={createInputTitle}
              onChange={createTitleOnchange}
            />
            <Button className="addButton" onClick={createTitleButton}>
              추가
            </Button>
          </Form>
        </CardBody>
      </Card>
    </div>
  );
}

export default App;

 

 

(2) redux/store.js

import rootReducer from './reducer';
import { legacy_createStore as createStore } from "redux"

const store = createStore(rootReducer);

export default store;

 

 

(3) redux/reducer.js

import { combineReducers } from "redux";
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage";
// import storage from "redux-persist/lib/storage/session";

import todoListReducer from "./action/todolist.js"


const persistConfig = {
  key: "root",
  // localStorage에 저장합니다.
  storage,
  whitelist: ["todoList"]
  // blacklist -> 그것만 제외합니다
};

export const rootReducer = combineReducers({
  todoList:todoListReducer,
});

export default persistReducer(persistConfig, rootReducer);

 

 

(4) redux/action/todolist.js

// 리덕스 state 초기 셋팅
// const InitState = {
//   array: [
//     // {
//     //   title: undefined,
//     //   contents: undefined,
//     // },
//   ],
// };

// 샘플 데이터 넣고 추가시
const InitState = {
  array: [
    {
      title: "Redux 학습하기",
      contents: [
        "로그인/로그아웃 가능 여부 확인",
        "localstorage/sessionstorage 학습",
      ],
    },
    {
      title: "SpringBoot 학습하기",
      contents: ["Spring Security 구현", "Spring MVC 패턴 익히기"],
    },
    {
      title: "JWT 인증 처리",
      contents: ["JWT 개념 익히기"],
    },
  ],
};

const todoListReducer = (state = InitState, action) => {
  switch (action.type) {
    /**
     * setTodoList : redux state 값을 바꿔주는 기능으로 App.js에서 해당 알고리즘 crud 사용하여 변경 
     */
    case "setTodoList":
      return {
        array: !!action.array ? action.array : state.array,
      };
    default:
      return state;
  }
};

export default todoListReducer;

 

 

 

결과물 (1200px 기준)

Local Storage는 해당 서버를 끄지 않는 이상 데이터 상태는 지속적으로 유지 되는 것을 확인 할 수 있습니다.

 

 

 

 

 

 

 

다음 시간에는 소분류(상세 부분) CRUD 넣는 작업을 추가하도록 하겠습니다.

https://berkley.tistory.com/12

 

(8) Todolist 메인 부분 이외의 상세부분의 대해 CRUD 작성

지금까지 TodoList CRUD 및 UI 반응형 웹앱, Redux까지 반영을 시켰습니다. Redux 반영에 대해서는 링크 참조 바랍니다. https://berkley.tistory.com/11 (7) Todo List Redux - Local Storage로 state 리팩토링 지금까지 CRUD

berkley.tistory.com

 

728x90
반응형
LIST