프로그래밍/Frontend (React, Javascript, Typescript)

전역 상태(zustand)에서 배열 concat의 side effect, 해결 방안 (feat. React Strict Mode)

Turtle-hwan 2024. 12. 15. 16:03

문제 상황

공공데이터 API에서 받은 하루 동안의 특정 경로의 고속버스 운행 리스트 데이터를 전역 상태인 useTowardBusListStore()에 Array concat으로 기존 배열 데이터 + 받은 데이터를 저장하고 있었다.

  • zustand로 전역 store를 만들었으며, 아래에서 useForwardBusListStore의 concat 함수 코드만 보면 된다.
    • concat 함수를 보면 원래 state list에 새로 들어온 list를 합치는 작업을 한다.
    • forwardBusList: [...state.forwardBusList, ...newforwardBusList]
import { create } from 'zustand';
import type { ForwardBusListState } from './index.types';

const initialState = {
  forwardBusList: [],
};

const useForwardBusListStore = create<ForwardBusListState>((set) => ({
  ...initialState,
  reset: () => set({ ...initialState }),
  concat: (newforwardBusList) =>
    set((state) => ({
      ...state,
      forwardBusList: [...state.forwardBusList, ...newforwardBusList],
    })),
  deleteByStartId: (targetStartId) =>
    set((state) => ({
      ...state,
      forwardBusList: state.forwardBusList.filter(
        (forwardBus) => forwardBus.startId !== targetStartId
      ),
    })),
}));

export default useForwardBusListStore;

 

  • 아래 코드는 의존성 배열이 빈 useEffect()를 사용해 페이지가 처음 렌더링 될 때, 공공데이터 API에서 받아온 정보를 useForwardBusListStore의 concat()을 호출해서 전역 상태에 넣고 있다.
  useEffect(() => {
    getBusTicketsAPI(
      searchQuery.startId || 'NAEK032',
      searchQuery.destId || 'NAEK300',
      convertYYYYMMDD(searchQuery.startDate)
    )
      .then((data) => {
        concat(
          convertBusTicketsToBusList(data.response.body.items.item, searchQuery)
        );
      })
  }, []);

 


의문 사항 및 문제 정의

  • Q0. 이 때 어떤 문제가 발생할까?
    • A0. React Strict Mode로 인해 useEffect가 두 번 실행되는데, concat이 두 번 되어 배열에 동일 정보가 두 번 저장되는 문제가 발생한다!!!!
    • 즉, React Strict Mode로 우리는 전역 상태 관리 함수에서 side effect가 발생한다는 것을 알 수 있다.
  • Q1. 어떻게 side effect를 없앨 수 있을까?
  • Q2. 그렇다면 이게 왜 side effect로 간주되는 걸까?
  • Q3. side effect를 없앴을 때 성능 이슈는 없을까?

side effect가 어디서 발생하는지 알았으니 자연스럽게 이를 어떻게 해결할지 궁금해졌고, 이 의문들에 대해 실력 좋은 친구와 이야기하면서 더 깊게 이해할 수 있었다.


해결 방안

Q1. 어떻게 side effect를 없앨 수 있을까?

A1. 먼저 어떻게 side effect를 없앨 수 있을지 이야기해보았다. 물론 StrictMode를 주석처리한다는 선택지도 있겠지만 말이다.

  • A1-1. 처음 떠오르는 생각은 원래 있던 배열 내용 뒤에 concat으로 이어붙이는 것이 아니라, 원래 있던 배열을 제거하고 대신 새로운 배열을 넣는 방법이었다.
    • 이 방법은 지금 사례와 같이 API에서 받아오는 배열 길이가 작아서 백에서 항상 한 번에 받아올 수 있는 경우에는 효과적이다.
    • 그런데 만약 무한 스크롤이나 pagenation 때문에 백엔드에서 일정량 단위로 정보를 잘라서 받아와야 한다면 어떨까?
      • 그러면 결국 이를 전역 상태에 저장하기 위해선 이전에 받아온 페이지들의 정보도 제거되면 안 되고 전역 상태에 유지되어야 한다.
  • A1-2. 이 문제를 해결하기 위해선 기존 상태를 탐색하고 필터링해서 존재하지 않는 것만 추가하면 된다.
    • 기존 상태에 존재하면 추가하지 않고, 존재하지 않으면 추가하는 방식은 우리의 의도와도 맞아떨어지면서 side effect도 발생하지 않는다.

 

Q2. 그렇다면 이게 왜 side effect로 간주되는 걸까?

A2. 그런데 왜 배열을 단순히 concat 하는 것이 side effect를 발생시키는지 의문이 들었다. 지금 사례에서는 그다지 부작용이 일어나 보이지 않아서 뭔가뭔가 다른 사례를 생각해 보기로 했다.

  • 사례1) 만약 API를 호출하는데 응답이 지연되어 재호출을 시도했으나 지연된 응답과 재호출 응답이 두 번 도달하여 각각 전역 상태를 변경한 경우
  • 사례2) 전역 상태 store 변경 함수가 여러 컴포넌트에서 호출될 가능성이 있고, 동시에 호출되어 전역 상태를 변경한 경우
  • 위 두 사례 모두 다 중복 필터링 검증을 하지 않으면 전역 상태에 중복 값이 저장된다!!!

 

  • A2-1. 게다가 순수 함수의 정의가 동일 입력에 대한 동일 출력이므로, 애초에 단순 concat 결과를 넣어 반환하는 함수는 순수 함수가 아니었던 것이다!!!!
    • concat 자체는 원본 배열을 변경하지 않고 새로운 배열을 반환하므로 순수 함수이다!
    • 그러나 원본 상태에 concat 한 배열을 다시 저장하여 반환하면 동일한 배열 입력을 계속 반복해서 주었을 때 return 값은 계속 달라지게 되므로 순수하지 못하다.

 

Q3. side effect를 없앴을 때 성능 이슈는 없을까?

A3. 그런데 중복 필터링 검증을 하면 안 할 때에 비해 탐색을 다 돌려야해서 성능 이슈가 생길 것 같았다.

원래 길이 n의 배열이 있고, 새로 추가될 배열이 m이라 하면 단순하게만 봐도 O(nm)의 시간이 걸린다.

물론 프론트에서 이 많은 양의 정보를 저장하지 않고, 백에서 필요할 때마다 불러오면 성능 문제를 걱정할 필요는 없을 것이다.

그럼 성능 이슈는 어떻게 해결 가능할까?

  • 배열이 아니라 객체로 저장하거나 id 기반 정규화가 필요하다. hashing을 해놓는 것도 좋을 수 있다.
  • id 기반 정규화를 프론트에서 진행해도 되지만 배열을 객체로 바꾸는 시간이 너무 길다면 백에서 정규화된 데이터를 받아오는 게 나을 수 있을 것이다.
    • 그런데 state는 상태 불변성을 위해 항상 새로운 객체를 만든다.
  • 이 때문에 immutable.js 를 도입해 상태 불변성을 유지하면서 시간복잡도를 줄여야 할 수 있다.

요약 정리

  • Q0. 이 때 어떤 문제가 발생할까?
    • A0. React Strict Mode로 인해 useEffect가 두 번 실행되는데, concat이 두 번 되어 배열에 동일 정보가 두 번 저장되는 문제가 발생한다!!!!
  • Q1. 어떻게 side effect를 없앨 수 있을까?
    • A1-1. 원래 배열을 제거하고 새로 만들어 넣는다.
    • A1-2. 무한 스크롤이나 pagenation 같은 경우, 기존 상태를 필터링해서 존재하지 않는 것만 추가한다.
  • Q2. 그렇다면 이게 왜 side effect로 간주되는 걸까?
    • A2. concat은 순수함수지만 원본 배열에 다시 concat한 배열을 넣어 반환하면 순수하지 못하다.
  • Q3. side effect를 없앴을 때 성능 이슈는 없을까?
    • 필요할 때마다 백에서 받아온 데이터를 사용하면 성능 이슈는 발생하지 않을 것이다.
    • 만약 많은 정보를 프론트에도 저장할 필요성이 있다면, id 기반 정규화를 하거나 immutable.js를 사용해서 상태 불변성을 유지하면서 시간 복잡도를 줄여야 한다.