Redux - 시작하기

sarang_daddy·2023년 11월 4일
0

Redux

목록 보기
1/3

Why Redux?

리액트 프로젝트에서 useContext와 useState, useReducer를 활용하여 상태관리를 진행해왔다. 하지만 이들 만으로는 약간의 아쉬움이 존재했다. 특히 전역 상태 관리를 도와주는 Context의 경우 하위 컴포넌트가 모두 리렌더링되기에 Provider마다 하나의 상태값만을 주게되어 전역 상태가 늘어날 수록 추가되는 Provider로 번거로움과 코드 가독성이 좋지 못함을 경험하게 되었다.

전역 상태관리 라이브러리를 사용해보고 싶어졌다.
그렇다면 어떤 라이브러리를 선택해야 할까?
프로젝트 규모와 팀원들과의 이해관계가 필요하겠지만 학습 단계인 나는 가장 많이 사용되고 있는 Redux를 적용해 보려고 한다.

npm trends
리액트 상태관리 트렌드의 변화

Redux의 철학

Redux를 사용하기에 앞서 Redux가 지향하고자 하는 철학에 대해서 알아보자.

1. Single Source of Truth

리덕스는 애플리케이션의 모든 상태를 하나의 스토어(store) 내에 있는 하나의 객체 트리(tree)에 저장한다. 이것은 애플리케이션의 상태를 예측 가능하게 만들고, 디버깅과 검사를 용이하게 한다.

2. State is Read-Only

상태를 변경할 수 있는 유일한 방법은 액션(action)을 발행(emit)하는 것이다. 액션은 무엇이 일어나야 하는지를 설명하는 일반 객체다. 이러한 제한은 상태 변경을 일관되게 추적할 수 있게 해주며, 애플리케이션에서 예측 가능한 동작을 보장한다.

3. Changes are Made with Pure Functions

액션에 의해 상태 트리가 어떻게 변화하는지를 지정하기 위해 리듀서(reducer)라 불리는 순수 함수를 사용한다. 리듀서는 이전 상태와 액션을 인자로 받아 새로운 상태를 반환하는 함수다. 이 원칙은 상태 변화의 로직을 명확하고 예측 가능하게 만들며, 테스트와 디버깅을 용이하게 해준다.

위와 같은 철학은 단방향 상태관리 Flux 아키텍쳐를 기반으로 Redux가 발전해왔기 때문이다.

상태관리로 유명한 패턴 중 하나인 MVC 패턴의 경우 양방향 데이터 바인딩이 가진 복잡성과 예측 불가능성으로 view가 많이 필요한 프론트엔드 개발에서는 어려움이 많다. 이를 해결하기 위해, Flux 아키텍처의 단방향 데이터 흐름 개념을 가져와서 발전한게 Redux 라이브러리다.

Redux 사용해보기

Redux 스토어 설정하기

  • Redux 스토어를 설정하는 첫 번째 단계는 프로젝트의 루트(index.tsx) 파일에서 생성한다.
  • 아래는 단일 루트 리듀서를 사용하여 스토어를 생성하는데, 이 리듀서는 다양한 모듈의 리듀서들을 가지고 있을 수 있다.
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './modules';
import App from './components/App';

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

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

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

root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
);

루트 리듀서

  • 리듀서들을 포함하고 있는 modules 폴더에서 index.ts 파일을 통해 모든 리듀서들을 결합한다.
  • 예시에서는 유저 데이터와 관련된 모든 액션을 처리하는 users 리듀서를 가지고 있다.
  • 전역에서 관리하고자 하는 상태의 다른 리듀서를 추가할 수 있다.
// modules/index.ts
import { combineReducers } from 'redux';
import usersReducer from './users';

const rootReducer = combineReducers({
  usersReducer,
  // + 다른 리듀서들
});

export default rootReducer;

리듀서 생성

  • 리듀서에 인자로 전달될 액션을 정의한다.
  • usersReducer는 세 가지 액션 타입 SET_USERS, ADD_USER, UPDATE_USERS에 대응하여 각각 다른 상태 변경을 수행하도록 설계되었다.
// modules/users.ts
export const setUsers = (users: IUser[]) => ({
  type: SET_USERS,
  payload: users,
});

export const addUser = (user: IUser) => ({
  type: ADD_USER,
  payload: user,
});

export const updateUsers = (userIds: number[]) => ({
  type: UPDATE_USERS,
  payload: userIds,
});

const initialState: IUserDataState = {
  users: [],
};

const usersReducer = (
  state = initialState,
  action: IUserDataActions,
): IUserDataState => {
  switch (action.type) {
    case SET_USERS:
      return {
        ...state,
        users: action.payload,
      };
    case ADD_USER:
      return {
        ...state,
        users: [...state.users, action.payload],
      };
    case UPDATE_USERS:
      return {
        ...state,
        users: state.users.map((user) =>
          action.payload.includes(user.id)
            ? { ...user, isDeleted: !user.isDeleted }
            : user,
        ),
      };
    default:
      return state;
  }
};

export default usersReducer;

dispatch로 리듀서에 액션 전달하여 상태 관리하기

  • useDispatch로 리듀서에 액션을 전달할 수 있다.
  • 리듀서는 전달 받은 액션 타입에 따라 현재 상태를 반환, 변경, 추가 한다.
  • 유저 정보는 서버에서 관리되는 데이터로 서버와의 통신이 필요하다.
  • 때문에 아래 예제는 비동기 요청 함수 내에서 dispatch를 적용하고 있다.
const getUsersData = async (dispatch: AppDispatch) => {
  try {
    const res = await axiosInstance.get(`/user_data`);
    if (res.status === 200) {
      // 유저 데이터 반환
      dispatch(setUsers(res.data));
      return res.data;
    }
  } catch (err) {
    throw err;
  }
};

const addUserData = async (data: IUser, dispatch: AppDispatch) => {
  try {
    const res = await axiosInstance.post(`/user_data`, data);

    if (res.status === 200) {
      // 유저 추가
      dispatch(addUser(res.data));
    }
  } catch (err) {
    return err;
  }
};

const updateUserData = async (
  ids: number[],
  updateValue: boolean,
  dispatch: AppDispatch,
) => {
  try {
    const userToUpdate = { isDeleted: updateValue };
    const queryString = ids.join(',');
    const res = await axiosInstance.patch(
      `/user_data?ids=${queryString}`,
      userToUpdate,
    );

    if (res.status === 200) {
      // 유저 상태 변경
      dispatch(updateUsers(ids));
    }
  } catch (err) {
    return err;
  }
};

Redux를 활용한 전역 상태 활용하기

  • 전역 단일 스토어에서 관리되는 리듀서이기에 어느 컴포넌트에서든 액션을 통해 서버로 부터 상태를 가져오거나 변경할 수 있다.
getUsersData(dispatch)
updateUserData(ids, false, dispatch);
addUserData(newUserData, dispatch);
  • useSelector로 상태를 구독하여 UI를 렌더링할 수 있다.
const users = useSelector((state: IRootState) => state.users.users);

// jsx
users.map((user) => (
    <Thumbnail
     key={user.id}
     user={user}
     isChecked={checkedUserIds.includes(user.id)}
     onCheckboxChange={onCheckboxChange}
     isActive={isActive}
    />
profile
한 발자국, 한 걸음 느리더라도 하루하루 발전하는 삶을 살자.

0개의 댓글