토이 프로젝트에 redux 적용하기

최경락 (K_ROCK_)·2022년 6월 16일
0

개요

  • 이전 Redux 연습 프로젝트를 통해, Redux 를 적용시켜보고, ducks 패턴을 적용시켜 본 뒤, Redux Toolkit(RTK) 으로 전환하는 것까지 진행을 해보았다.
  • 그리고 props drilling 을 이용하여 상태관리를 하던 프로젝트에 Redux 를 적용해보고자 한다.

다시 한번 보자

  • ReduxRTK 라이브러리의 차이점은 코어기능만 설치하느냐, 아니면 유용한 라이브러리를 함께 설치하느냐의 차이이다.

createStore + combineReducers + thunk + devTool→ configureStore
action + action type + reducer + immer → createSlice

  • 위 처럼 여러 역할을 한번에 수행하는 메소드로 처리 할 수 있어, 보일러 플레이트가 줄어든다.
    → 보일러 플레이트 : 어떤 기능을 사용하기 위해 꼭 작성해야 하는 코드
  • 공식문서상에서도 RTK 를 함께 설치하는 것을 권장한다.
    → Redux 에 포함된 기능들도 이미 적용되어 있으니 RTK 를 설치하면 모든 것이 해결된다.
  • ReduxReact 환경과 연결하기 위해서는 react-redux 를 함께 설치해주어야 하는데, 이는 그 둘을 연결 시켜주는 역할을 한다.

적용

설치

npm i redux react-redux @reduxjs/toolkit
  • npm 을 이용하여 두 라이브러리를 설치해주면 준비는 끝난다.

reducer, action 생성

  • src 폴더에 modules 라는 폴더를 생성해주었고, 여기에 todos 라는 파일을 만들어 상태를 관리하기로 했다.
// modules/todos.ts

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

interface ITodo {
  id: string;
  done: boolean;
  content: string;
}

const initialState = {
  todos: [],
};

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    getLocalStorage(state: { todos: ITodo[] }) {
      ...
    },
    addTodo(state: { todos: ITodo[] }, action: { payload: ITodo }) {
      ...
    },
    toggleTodoStatus(
      state: { todos: ITodo[] }, action: { payload: { id: string } }
    ) {
      ...
    },
    editTodo(state: { todos: ITodo[] }, action: { 
      payload: { id : string, content: string } }
    ) {
      ...
    },
    deleteTodo(state: { todos: ITodo[] }, action: { payload: { id: string } }) {
      ...
    },
    clearDoneList(state: { todos: ITodo[] }) {
      ...
    },
  },
});

export const todosActions = todosSlice.actions;
export default todosSlice.reducer;
  • 위 처럼 createSlice 메소드를 불러오고, 이름과 초기 상태, 필요한 reducer 를 객체로 전달해주었다.
  • reducers 에 작성된 함수들은 todosSlice.actions 에 저장되어 기존 action 처럼 사용가능하다.

name 속성이 유니크한 action 타입을 자동으로 만들어주고, action 또한 reducers 에 작성된 함수의 이름으로 자동생성 되므로, 따로 action 타입을 만들 필요도, action 생성 함수를 만들 필요도 없다.

  • store 에 전달하기 위한 reducertodosSlice.reducer 에 저장된다.

index.tsx

// index.tsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter as Router } from 'react-router-dom';

import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';

import App from './App';

import todosSlice from './modules/todos';

const store = configureStore({
  reducer: {
    todosSlice,
    ...
    // 다수의 리듀서 작성가능.
  },
});

const root = ReactDOM.createRoot(
  document.getElementById('root') as HTMLElement
);
root.render(
  <Provider store={store}>
    <Router>
      <App />
    </Router>
  </Provider>
);
  • 먼저 store 를 전달할 컴포넌트를 Provider 로 감싼다.
  • configureStore 에 객체를 이용하여 reducer 를 전달하여 store 를 만들고, 해당 값을 store 변수에 저장한다.
    combineReducers 의 기능이 내장되어 있으므로, reducer 속성에 다수의 reducer 를 작성 할 수 있다.
  • Providerstore props 에 저장한 store 를 전달한다.

상태 가져오기, action 실행하기

// App.tsx

import { useSelector } from 'react-redux';

...
const todoSlice = useSelector(
    (state: { todosSlice: { todos: ITodo[] } }) => state.todosSlice.todos
  );
  • useSelector를 이용하여 store 에 저장된 상태를 가져 올 수 있다. 위 처럼 작성하여 todoSlice 에 해당 상태가 담길 수 있도록 했다.
    → 콜백함수의 매개변수에 store에서 전달한 상태가 담긴다.
// components/Input.tsx

import { useDispatch } from 'react-redux';
import {todoActions} from 'modules/todos';

...
const dispatch = useDispatch()

...
const addTodo = (
    e:
      | React.FormEvent<HTMLFormElement>
      | React.MouseEvent<HTMLButtonElement, MouseEvent>
  ) => {
    e.preventDefault();

    if (!inputValue) return;

    dispatch(
      todosActions.addTodo({
        id: `${uuidv4()}`,
        done: false,
        content: inputValue,
      })
    );
    setInputValue('');
  };
  • useDispatch 를 이용하여 전달받은 action 을 실행하고, 상태를 변경한다.
  • useDispatch 를 변수에 저장하고, 해당 변수에 인자를 담아 실행하는 것으로 인자 내부의 내용을 payload 로 전달하여 상태를 변경 할 수 있다.

로직 수정하기

  • 기존에 useState 를 사용하며 작성한 로직을 옮겨올 땐 같은 기능이라도 구현하는 방식이 달라질 수 있다는 것을 고려해야한다.

로컬 스토리지의 데이터 불러오기

  • 현재 토이 프로젝트에서 서버를 사용하지 않고, 로컬 스토리지를 이용하여 서버를 대신하고 있다.
  • 페이지가 렌더링 될 때 로컬 스토리지의 정보를 불러와 상태를 업데이트 하고, 상태가 변경 될 때 해당 상태를 로컬 스토리지에 업데이트하는 방식이다.
  • 기존의 코드는 아래와 같다.
const [todoList, setTodoList] = useState<ITodo[]>([]);

useEffect(() => {
  const data: string | null = window.localStorage.getItem('data');
  if (data) setTodoList(JSON.parse(data));
}, []);

// 페이지를 불러 올 때 데이터가 있는 경우, 
// todoList 의 값을 해당 JSON 을 객체로 파싱한 값으로 갱신한다.

useEffect(() => {
  window.localStorage.setItem('data', JSON.stringify(todoList));
}, [todoList]);

// 상태가 변경될 때 해당 내용을 로컬 스토리지에 저장한다.
  • useState 를 사용하는 경우 위의 코드만으로 가능했다.
  • 하지만, redux 를 사용할 땐 reducer 내부에서 useEffect 를 사용 할 수 없기 때문에 reducer 에서 로컬 스토리지의 값을 가져와 갱신하는 action 을 만들고, 렌더링시에 호출하는 방식을 사용하였다.
    → 기존의 addTodo action 을 사용하기 위해선 반복문으로 일일히 넣어줘야하는데, 이 보단 전체 객체를 가져와 업데이트 하는 방식이 더 빠를 것.
  • 위의 방식으로 작성한 코드는 아래와 같다.
// module/todos.ts

const initialState = {
  todos: [],
};

const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    // 로컬 스토리지 가져오기
    getLocalStorage(state: { todos: ITodo[] }) {
      const localData = window.localStorage.getItem('todosData') as string;
      state.todos = JSON.parse(localData);
    },
  },
});

export const todosActions = todosSlice.actions;
export default todosSlice.reducer;
// App.tsx

const dispatch = useDispatch();

useEffect(() => {
  const data: string | null = window.localStorage.getItem('todosData');
  if (data) dispatch(todosActions.getLocalStorage());
}, []);

useEffect(() => {
  window.localStorage.setItem('todosData', JSON.stringify(todoSlice));
}, [todoSlice]);

+

  • 로직을 하나의 파일에 몰아 작성하고, 실 사용시 디스패치만 하면 되는게 상당히 편하다고 느꼈다.
  • 만약 서로 다른 컴포넌트에서 하나의 상태를 두고 동일한 동작을 하고자 할 때, 각 컴포넌트에서 구현, 전달 할 필요 없이 같은 기능을 제공하는 action 을 통해 해결 할 수 있다는 장점이 있다.
  • 동작의 에러를 수정하기 위해 이 파일 저 파일 몇번째 줄을 찾아다닐 필요없이 하나의 파일에서 해당하는 리듀서를 찾아 수정을 할 수 있다는 점은 확실히 장점이라고 생각한다.

0개의 댓글