안녕하세요.
백엔드 글을 주로 쓰고 싶었지만 회사 업무에서 프론트 업무를 하면서 치여 살다보니 몰랐던 내용을 정리할 겸 글을 쓰게 되었습니다. ㅜㅜ..
이전에 백엔드에서 API의 중복요청을 필수로 막아야하는 경우가 있어 Redis를 통해 해당 API 요청의 고유 식별자를 Key값으로 관리하여 Redis의 Key로 저장하여 해당 요청이 정상적으로 응답되거나 핸들링 가능한 처리된 에러가 발생한 경우는 해당 Key를 삭제하여 다시 요청할 수 있는 상태로 초기화를 시켜주었습니다. (비정상 종료나 타임아웃이 발생한 경우는 레디스 TTL을 설정하여 키가 자동 삭제)
우선 첫번째로 프로젝트에서 사용했던 리엑트 쿼리 기술의 효과적인 API 중복 요청 방지 메커니즘을 이해하려고 합니다.
Key 기반 캐싱
백엔드와 마찬가지로 리엑트 쿼리에서도 요청마다의 Key 캐시를 만들어 동일한 키를 사용하는 추가 요청은 실행되지 않고 기존 요청이 완료되면 동일한 키를 사용하는 모든 컴포넌트는 최신 데이터가 공유되게 됩니다.
Key 기반 캐싱과 일반 요청의 차이
요청 재사용 | 동일 키로 진행 중인 요청을 재사용 | 요청마다 별도 네트워크 호출 실행 |
캐시 | 고유 키를 사용하여 캐시 관리 | 캐시 없음, 항상 새로 요청 |
데이터 최신 상태 유지 | 오래된 데이터를 자동으로 재요청 | 자동 재요청 없음 |
효율성 | 불필요한 네트워크 호출 감소 | 중복 요청으로 인해 네트워크 낭비 가능 |
에러 처리 | 키별로 중앙화된 에러 상태 관리 | 요청별 독립적인 에러 처리 |
이러한 Key 기반의 요청 하나 하나의 캐시를 이용해 Hook을 통해 API의 요청 진행 상태를 추적이 가능하게 되고 플래그를 hook에서 꺼내와서 바로 사용가능하게 됩니다.
isLoading 처음으로 데이터를 요청할 때의 로딩 상태 (캐시가 없는 상태에서 요청 시작).
isFetching 데이터를 다시 가져오는 중인지 여부 (캐시가 있더라도 네트워크 요청 중일 때)
2. useCallBack을 사용했음에도 불구하고 특정 함수가 중복으로 호출 되는 문제.
지도 관련 서비스를 프론트로 개발하면서 이벤트리스너를 이용해 지도 내부 클릭시 간혈적으로 특정 알럿창이 3~4번 중복으로 표시되는 문제가 있었습니다.
onMapClick 이라는 함수를 useCallback으로 감싸 함수의 중복 호출을 막고 콜백 이벤트로 특정 기능을 수행할 수 있도록 개발을 진행했었는데요, 처음에는 오류나던 화면이 아니었던 것 같은데 특정 기능 추가 후에 함수가 중복호출 되는 문제가 발생했습니다. 디버깅을 해봐도 이해가 잘 가지 않았는데요 문제의 코드 입니다.
import React, { useCallback, useEffect } from "react";
const MapComponent = ({ map }) => {
const handleMapClick = useCallback((event) => {
console.log("지도 클릭:", event);
}, []); // 빈 의존성 배열로 인해 동일 함수 유지
useEffect(() => {
if (!map) return;
// 지도 클릭 이벤트 리스너 등록
map.addEventListener("click", handleMapClick);
// Cleanup 함수로 이벤트 제거
return () => {
map.removeEventListener("click", handleMapClick);
};
}, [map, handleMapClick]); // handleMapClick 포함하여 리렌더링 시 재등록 방지
return <div id="map">지도</div>;
};
export default MapComponent;
언뜻봐서는 문제가 있는 것 같진 않은데요 처음에는 간혈적으로 특정 Case에서만 발생하여 useEffect의 의존성과 조건문에 신경을 더쓰려고 했었습니다. 하지만 문제는 React와 Dom 이벤트의 결합 특성을 이해하지 않아 생긴 문제였습니다.
지도 컴포넌트가 리렌더링 될때 React 내부에서는 함수가 새로 생성되지만, 이벤트 핸들러는 이전 핸들러를 제거하지 않는 이상 계속 등록되어 있는 상태로 남아있게 됩니다. 리렌더링 시 이벤트 핸들러로 전달된 함수는 새로운 함수 객체로 재생성되기 때문에 removeEventListener를 걸어놨더라도 위와 아래가 다른 함수를 참조하고 있을 가능성이 높게 됩니다.
따라서 지도 컴포넌트의 의존성에 무언가가 리렌더링이 발생한경우 1번 MapClick 리스너, 2번 MapClick리스너, 3번 MapClick 리스너 ... 여러개의 리스너가 중복 되어 만들어지고 조건문에 부합된 경우 특정 함수가 3~4번 호출되는 것 입니다.
해결방법은 아래처럼 클릭리스너 자체를 useRef를 통해 컴포넌트의 리렌더링과 무관하게 동일한 객체를 참조할 수 있게 하나의 이벤트 리스너를 관리시켜주니 해결되었습니다.
import { useEffect, useRef } from "react";
const MapComponent = ({ map }) => {
const handleMapClickRef = useRef(); // 최신 핸들러를 저장할 Ref
// 최신 핸들러를 업데이트하는 Effect
useEffect(() => {
handleMapClickRef.current = (event) => {
console.log("지도 클릭:", event); // 최신 핸들러 동작
};
}, []); // 핸들러는 최신 상태를 유지
// 이벤트 리스너 등록 및 해제
useEffect(() => {
if (!map) return; // map 객체가 없는 경우 안전하게 종료
// 이벤트 리스너로 Ref를 사용하는 핸들러 등록
const handleMapClick = (event) => {
if (handleMapClickRef.current) {
handleMapClickRef.current(event);
}
};
map.addEventListener("click", handleMapClick);
return () => {
map.removeEventListener("click", handleMapClick); // 정리 단계에서 리스너 제거
};
}, [map]); // map이 바뀔 때만 Effect 실행
return <div id="map">지도</div>;
};
export default MapComponent;
useCallback과의 차이점이 그럼 뭐야! (GPT Chance)
useCallback의 한계
- useCallback도 함수 참조를 메모이제이션하여 리렌더링 시 동일한 함수 객체를 유지합니다.
- 하지만, useCallback으로 생성된 함수 자체는 여전히 의존성 배열에 포함된 값이 변경될 때 새로 생성됩니다.
useRef의 장점
- useRef는 항상 동일한 current 참조를 유지하기 때문에, 의존성 배열 없이도 단 하나의 함수 객체 값을 안전하게 저장하고 사용할 수 있습니다.
- 리렌더링과 무관하게 동일한 current 객체를 보장하기 때문에 이벤트 리스너와 같은 외부 시스템과의 연결에 적합합니다.
지도 내부의 마커를 관리하거나 그럴때도 느꼈지만 외부스크립트, 외부 시스템과의 연결에는 useRef를 통해 컴포넌트의 상태와 무관하게 만들 수 있다! 라고 느꼈던 것 같습니다.
다음부터는 다시 백엔드 글로 돌아오겠습니다.
2024년도 벌써 끝나가네요 다들 건강 잘 챙기시고 파이팅입니다ㅏㅏ
'글또' 카테고리의 다른 글
[글또] 애플리케이션 성능 테스트 (0) | 2025.02.02 |
---|---|
[글또] 이직 실패.. 그래서 무엇을 채워야 할까? (1) | 2024.11.23 |
[글또] 2024년 이직을 위한 서류, 코딩테스트, 면접 후기 (6) | 2024.10.27 |
[글또] 활동 시작 전 다짐글 (9) | 2024.10.11 |