들어가며

Redux는 리액트 애플리케이션의 강력한 상태 관리 방법을 제공하지만, 잘못 사용하면 성능 이슈가 발생할 수 있습니다. 이번 글에서는 Redux를 사용할 때 적용할 수 있는 성능 최적화 기법에 대해 알아보겠습니다.


Redux와 함수 컴포넌트에서 발생하는 성능 이슈

다음과 같이 크게 2가지 문제를 생각해 볼 수 있습니다.

  • 불필요한 리렌더링 : Redux 상태가 변경될 때마다, 연결된 컴포넌트들은 리렌더링됩니다. 하지만 모든 상태 변경이 해당 컴포넌트와 관련된 것은 아닐 수 있습니다. 이로 인해 불필요한 리렌더링이 발생하여 성능 저하를 일으킬 수 있습니다.
  • 비효율적인 상태 선택과 계산 : 컴포넌트에서 필요한 상태를 선택할 때 불필요한 연산이나 깊은 비교가 발생하면 렌더링 성능에 영향을 미칩니다.

이제, 본격적으로 Redux 성능을 최적화 할 수 있는 방법에 대해 알아봅시다.


Redux 성능 최적화

1. React Redux Hooks의 효율적 사용

첫 번째 최적화 기법은 useSelector 훅을 최적화하는 방법입니다.

  • useSelector 훅의 최적화:
    • useSelector는 Redux 스토어의 상태를 선택하여 컴포넌트에서 사용할 수 있게 해주는 훅입니다. 기본적으로 useSelector는 참조 비교를 수행하기 때문에, 선택한 상태가 새로운 객체나 배열로 반환되면 내용이 같더라도 리렌더링이 발생할 수 있습니다.
    • 해결 방법:
      • shallowEqual을 사용하여 얕은 비교를 통해 불필요한 리렌더링을 방지할 수 있습니다. 이는 특히 상태가 객체나 배열일 때 유용합니다.
      • 상태가 복잡한 중첩된 객체인 경우, 커스텀 비교 함수를 사용하여 객체 내 특정 값만 비교하도록 최적화할 수 있습니다.

두 번째로, createSelector를 사용한 메모이제이션 기법입니다.

  • 메모이제이션된 셀렉터 사용 (createSelector):
    • createSelector는 Redux Toolkit에 포함된 reselect 라이브러리의 일부로, 불필요한 계산을 방지하고 성능을 향상시킬 수 있습니다.
    • 사용 예시:

        const selectItems = (state) => state.items;
        const selectFilter = (state) => state.filter;
              
        const selectFilteredItems = createSelector(
          [selectItems, selectFilter],
          (items, filter) => items.filter((item) => item.type === filter)
        );
              
        const filteredItems = useSelector(selectFilteredItems);
              
      
    • createSelector는 상태가 변경되지 않으면 이전에 계산된 결과를 반환하여, 불필요한 연산과 리렌더링을 방지할 수 있습니다.

메모이제이션 (Memoiztion) : 컴퓨터 프로그램이 동일한 계산을 반복적으로 해야할 때, 이전에 계산한 값을 메모리에 저장하여 중복적인 계산을 제거하여 전체적인 실행속도를 빠르게 해주는 기법입니다.


2. Redux Toolkit을 통한 효율적인 상태 관리

Redux Toolkit은 Redux의 공식 툴킷으로, 보일러플레이트 코드를 줄이고 일반적인 Redux 작업을 간소화합니다. 기본적으로 Immer와 Redux Thunk를 포함하고 있어, 불변성 관리와 비동기 작업 처리가 수월합니다.

  • createSlice와 createSelector 사용:
    • createSlice를 사용하면 상태와 리듀서를 간결하게 정의할 수 있으며, createSelector와 결합하여 효율적인 상태 관리와 성능 최적화를 할 수 있습니다.
    • 사용 예시:

        const usersSlice = createSlice({
          name: 'users',
          initialState: { list: [], status: 'idle' },
          reducers: {
            addUser: (state, action) => {
              state.list.push(action.payload);
            },
          },
        });
              
        const selectUsers = (state) => state.users.list;
        const selectActiveUsers = createSelector(
          [selectUsers],
          (users) => users.filter((user) => user.isActive)
        );
              
      
    • 이를 통해 불필요한 리렌더링과 성능 저하를 방지하며, 애플리케이션의 효율성을 높일 수 있습니다.

3. 불변성 유지와 Immer의 활용

불변성을 유지하는 것은 Redux 성능 최적화에서 중요한 역할을 합니다. Redux Toolkit은 내부적으로 Immer 라이브러리를 사용하여 불변성을 자동으로 관리합니다. 이를 통해 상태를 직관적이고 간단하게 업데이트할 수 있습니다.

  • 불변성 유지:
    • 불변성을 유지하면 상태 변화 추적이 용이하고, 예기치 않은 부작용을 방지할 수 있습니다.
  • Redux Toolkit과 Immer:
    • 사용 예시:

        const todosSlice = createSlice({
          name: 'todos',
          initialState: [],
          reducers: {
            addTodo: (state, action) => {
              state.push(action.payload); // Immer가 자동으로 불변성을 유지해줌
            },
            toggleTodo: (state, action) => {
              const todo = state.find(todo => todo.id === action.payload);
              if (todo) {
                todo.completed = !todo.completed;
              }
            },
          },
        });
              
      
    • 이를 통해 명령형 프로그래밍 스타일로 상태를 쉽게 업데이트하면서도 불변성을 유지할 수 있습니다.

Immer : 상태 변경 시 원본 상태를 복사한 후 변경된 부분만 업데이트하여 새로운 상태를 반환하는 불변성 관리 라이브러리입니다. 직관적인 불변성 관리를 통해, 개발자가 실수로 원본 상태를 수정하는 것을 방지할 수 있습니다.


4. 미들웨어와 캐싱을 활용한 비동기 작업 최적화

비동기 작업을 처리할 때의 최적화 기법입니다. Redux Thunk를 사용하면 비동기 로직을 명확하게 관리할 수 있으며, 불필요한 API 호출을 줄이기 위해 캐싱을 활용할 수 있습니다.

  • Redux Thunk의 활용:
    • 비동기 작업을 관리하고, 필요한 시점에 상태를 업데이트할 수 있습니다.
  • 비동기 작업의 성능 최적화:
    • 예를 들어, 동일한 데이터를 반복적으로 요청하는 대신, 한 번 요청한 데이터를 스토어에 저장하고 필요할 때마다 스토어에서 가져오는 방식으로 성능을 최적화할 수 있습니다.
    • 사용 예시:

        const fetchUserById = (userId) => async (dispatch, getState) => {
          const cachedUser = getState().users.find(user => user.id === userId);
          if (cachedUser) {
            return; // 캐시된 데이터가 있으면 추가 API 호출을 방지
          }
          const response = await fetch(`/api/users/${userId}`);
          const data = await response.json();
          dispatch(usersSlice.actions.addUser(data));
        };
              
      
    • 이를 통해 비동기 작업이 성능에 미치는 영향을 최소화할 수 있습니다.

결론

지금까지 리액트의 함수 컴포넌트에서 Redux를 사용할 때 적용할 수 있는 다양한 성능 최적화 기법을 살펴보았습니다. useSelector의 최적화, createSelector를 활용한 메모이제이션, 불변성 유지, 그리고 Redux Thunk를 통한 비동기 작업 최적화는 모두 애플리케이션의 성능과 사용자 경험을 크게 향상시킬 수 있는 중요한 기법들입니다.

다양한 최적화 기법들을 상황에 맞게 적용하여, 보다 빠르고 안정적인 애플리케이션을 개발하시길 바랍니다.