문제 상황
공공데이터 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를 사용해서 상태 불변성을 유지하면서 시간 복잡도를 줄여야 한다.
'프로그래밍 > Frontend (React, Javascript, Typescript)' 카테고리의 다른 글
Vite Proxy with Base URL & CORS 해결 (2) | 2024.11.28 |
---|---|
10분 만에 웹사이트에 PWA 적용해서 웹앱 만들기! (2) | 2024.11.14 |
React Component와 객체지향의 차이에 관하여 (2) | 2024.09.29 |
Fixed Menu, 메뉴 바 상단 고정 구현 시 content에 margin-top을 주면 위험한 이유 (feat. margin collapsing) (4) | 2024.09.28 |
[JavaScript, Node.js] 프론트엔드(JavaScript Runtime)에서의 Race Condition (0) | 2024.04.11 |