Skip to content

Conversation

@arty0928
Copy link
Member

#️⃣ 연관된 이슈

close: #297


1) 도입 배경

위치 검색창 입력 시 디바운싱과 쓰로틀링을 중첩 적용하여, 사용자가 서울시 강남구 등 한글을 타이핑할 때마다 최대 22회의 API 호출이 발생했습니다.
React 18의 StrictMode 더블 마운트와 AbortController 부재로 인해 모든 요청이 완료될 때까지 진행되어, 불필요한 네트워크 비용 및 렌더링 과부하가 발생했습니다.

주요 문제점

  1. 디바운스 + 쓰로틀 중첩으로 멈춤마다 1회 + 경계 1회 추가 호출
  2. StrictMode의 마운트/언마운트로 ref 초기화 → 동일 요청 중복 발화
  3. AbortController 미적용으로 이전 요청 미취소 (in-flight 요청 중첩)
  4. INP 111 ms / 전체 타임라인 5995 ms 로 사용자 체감 지연 심화

2) 작업 내용

1️⃣ 디바운스 단일화

  • 쓰로틀 훅(useThrottle) 완전 제거
  • “최종 확정값 1회 호출” 흐름으로 단순화

2️⃣ StrictMode 중복 호출 가드

  • lastReqRef 도입 → (keyword, page, append) 조합으로 요청 키 생성
  • 동일 키 연속 호출 시 조기 return

3️⃣ AbortController 적용

  • controllerRef로 이전 요청 추적
  • 새 요청 시 abort()로 기존 요청 강제 취소
  • signal을 fetch 옵션에 전달해 중간 요청 즉시 중단
  • useEffect clean-up 시점에서도 abort 실행

3) 핵심 코드 스니펫

✅ 1. 디바운스 단일화 (스로틀 제거)

useEffect(() => {
  if (!debouncedKeyword) {
    setSearchResults([]);
    return;
  }
  setPage(1);
  setHasNext(true);
  performSearch(debouncedKeyword, 1, false);
}, [debouncedKeyword, performSearch]);

✅ 2. StrictMode 중복 호출 가드

const lastReqRef = useRef<string | null>(null);

const performSearch = useCallback(async (keyword: string, pageNum = 1, append = false) => {
  const trimmed = keyword.trim();
  const reqKey = `${trimmed}::${pageNum}::${append ? 'append' : 'replace'}`;
  if (lastReqRef.current === reqKey) return;  // 중복 요청 차단
  lastReqRef.current = reqKey;
  // ...
}, []);

✅ 3. AbortController 도입

const controllerRef = useRef<AbortController | null>(null);

const performSearch = useCallback(async (keyword: string, pageNum = 1, append = false) => {
  controllerRef.current?.abort(); // 이전 요청 중단
  const controller = new AbortController();
  controllerRef.current = controller;

  const params = { keyword, page: pageNum, size: 15, signal: controller.signal };
  try {
    const results = await searchPlaces(params);
    // ...
  } catch (err: any) {
    if (err.name !== 'AbortError') console.error('검색 오류:', err);
  }
}, []);

✅ 4. fetch signal 전달

export const searchPlaces = async ({ keyword, page = 1, size = 15, signal }: SearchPlacesParams) => {
  const res = await fetch(
    `https://dapi.kakao.com/v2/local/search/keyword.json?query=${encodeURIComponent(keyword)}&page=${page}&size=${size}`,
    {
      headers: { Authorization: `KakaoAK ${process.env.NEXT_PUBLIC_KAKAO_MAP_REST_API_KEY}` },
      signal, // 요청 취소 신호 연결
    },
  );
  return res.json();
};


성능 최적화 전후 비교

1. API 호출 횟수 개선 및 네트워크 요청 중첩 제거

  • 디바운스 + 쓰로틀 중첩으로 최대 22회 API 호출 발생 | 디바운스 단일화 + AbortController 적용 → 1회로 단일화
  • 이전 요청이 완료될 때까지 모두 유지 (in-flight 22건) | 새 요청 전 abort() 실행으로 즉시 중단
Before After
이전 요청이 완료될 때까지 모두 유지 (in-flight 22건) 새 요청 전 abort() 실행으로 즉시 중단
image image

2. 전체 타임라인 범위 감소 및 INP(입력 응답 시간) 개선

  • 111 ms — 입력 시 지연 발생, 메인 스레드 과부하 | 48 ms — 반응 즉시 렌더링, 부드러운 타이핑
Before After
Full Range: 5995 ms — 연속 JS 실행으로 긴 차트 Full Range: 2506 ms — 불필요한 실행 절반 이하로 감소
image image

5. 요약 수치 비교

지표 Before After 개선율
API 호출 횟수 22회 1회 –95 %
INP (입력 지연) 111 ms 48 ms –57 %
Full Range 5995 ms 2506 ms –58 %
중첩 요청 수 10 건 이상 0 건 –100 %

💡 결과 요약:
불필요한 API 호출과 in-flight 요청을 모두 제거하면서,
사용자의 입력 지연(INP)을 57% 단축하고 렌더링 범위를 절반 이하로 줄였습니다.

@arty0928 arty0928 added this to the 🏷️ 대여 milestone Oct 12, 2025
@arty0928 arty0928 self-assigned this Oct 12, 2025
@arty0928 arty0928 added 🔨 Refactor 코드 리팩토링 👍Performance 성능 최적화 labels Oct 12, 2025
@vercel
Copy link

vercel bot commented Oct 12, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
badata Ready Ready Preview Comment Oct 12, 2025 8:12am

@coderabbitai
Copy link

coderabbitai bot commented Oct 12, 2025

📝 Walkthrough

Summary by CodeRabbit

  • 신기능
    • 검색 중 입력 변경 시 이전 요청을 취소하여 최신 결과만 표시됩니다.
  • 버그 수정
    • 동일한 조건의 중복 검색 호출을 방지하여 잘못된/지연된 결과 노출을 줄였습니다.
    • 취소된 요청에서 발생하는 오류를 무시해 불필요한 에러 노출을 줄였습니다.
  • 리팩터링
    • 검색 흐름을 단순화하고 로딩/더보기 상태 표시를 개선했습니다.
    • 검색어가 비어 있을 때 결과를 즉시 초기화합니다.
    • 주소 기록 존재 여부 판단 로직을 안정화했습니다.

Walkthrough

검색 훅에서 스로틀을 제거하고 디바운스 기반 호출로 전환했습니다. AbortController를 도입해 진행 중 요청 취소와 동일 파라미터 요청 중복 방지 로직을 추가했으며, AbortError는 무시하고 그 외 오류만 처리합니다. API 유틸은 AbortSignal을 전달·전파하도록 시그니처를 확장했습니다.

Changes

Cohort / File(s) Summary
검색 훅: 위치 키워드 검색 흐름 리팩터링
src/features/rental/search/hook/useSearchPlacesHooks.ts
스로틀 제거, 디바운스 트리거로 직접 호출. AbortController 도입(요청 취소, 언마운트/키워드 변경 시 abort, signal 전달). 요청 중복 제거(lastReqRef). AbortError 무시, 비-append 시 오류 로깅/노티. 로딩 상태 토글 정리.
검색 훅: 위치 선택/히스토리 계산 로직 정리
src/features/rental/search/hook/useSearchPosHooks.ts
handleSelectPlace 의존성 배열을 비움(재생성 빈도 변화). hasAddressHistory 계산 시 pages 가드 추가 및 직접 파생 계산으로 단순화. 주석 보정.
검색 API 유틸: Abort 지원
src/features/rental/search/utils/address/searchPlaces.ts
SearchPlacesParamssignal?: AbortSignal 추가. fetch 옵션으로 signal 전달. AbortError 재throw, 기타 오류 로깅 후 재throw.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant UI as SearchInput
  participant HK as useSearchPlacesHooks
  participant AC as AbortController
  participant API as searchPlaces()
  participant BE as fetch/http

  U->>UI: 키워드 입력
  UI->>HK: 디바운스된 키워드 변경
  alt 키워드 비어있음
    HK-->>API: (호출 안 함)
    HK->>HK: 결과 초기화, 진행 중 요청 abort
  else 키워드 있음
    HK->>AC: 기존 요청 abort, 새 컨트롤러 생성
    HK->>HK: 동일 (keyword,page,append) 중복 여부 확인
    alt 중복 요청
      HK-->>API: 스킵
    else 신규 요청
      HK->>API: searchPlaces({ keyword, page, append, signal })
      API->>BE: fetch(..., { signal })
      alt 성공
        BE-->>API: 응답 데이터
        API-->>HK: 결과 반환
        HK->>HK: 상태 업데이트(append 여부 반영)
      else AbortError
        BE--x API: AbortError throw
        API--x HK: AbortError 전파
        HK->>HK: 오류 무시(로딩 정리)
      else 기타 오류
        BE-->>API: 오류
        API--x HK: 오류 전파
        HK->>HK: (append 아님) 로그/노티
      end
    end
  end
  note over HK: 언마운트 시 AC.abort()로 정리
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning useSearchPosHooks.ts 파일의 handleSelectPlace와 hasAddressHistory 관련 변경은 입력 디바운싱 적용 목표와 직접적인 관련이 없어 보여 이 부분은 별도 PR로 분리하거나 변경 사유를 명확히 제시하는 것이 바람직합니다. 관련 없는 훅 로직 수정은 별도 PR로 분리하거나 PR 설명에 해당 변경의 목적과 근거를 명확히 기술해주세요.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues Check ✅ Passed 변경 사항이 이슈 #297에서 요구한 디바운스 적용 및 API 호출 최소화 목표를 충족하며 중복 호출 방지와 AbortController 적용 구현으로 문제를 해결하고 있습니다.
Description Check ✅ Passed PR 설명은 디바운스 및 AbortController 적용 등 변경된 주요 내용과 배경을 상세히 서술하여 변경 사항과 완전히 연관되어 있습니다.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
Title Check ✅ Passed 제목은 위치 검색창에 디바운스 및 AbortController 적용으로 중복 요청을 제거하는 주요 성능 개선을 명확하게 요약하여 PR의 핵심 변경 사항을 잘 반영합니다.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/BADA-388-search-debounce

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • BADA-388: Request failed with status code 404

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

Failed to generate code suggestions for PR

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/features/rental/search/hook/useSearchPlacesHooks.ts (1)

53-105: StrictMode에서 검색이 멈추는 중대한 회귀

Line 53~105의 lastReqRef 가드가 동일 키 연속 호출을 전부 막고 있는데, React 18 StrictMode에서는 이펙트가 즉시 정리되고 재실행되면서 첫 번째 호출을 abort()로 끊은 뒤 두 번째 호출이 같은 키라는 이유로 아예 실행되지 않습니다. 개발 모드뿐 아니라 추후 재시도(동일 키 재검색)도 영구적으로 차단됩니다. 요청이 진행 중일 때만 중복을 막도록, 완료/중단 시 lastReqRef를 초기화하는 등 가드를 수정해 주세요.

       try {
         …
       } catch (error) {
         …
       } finally {
+        lastReqRef.current = null;
         setIsLoading(false);
         setIsLoadingMore(false);
       }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Jira integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 7060b05 and 18e7a06.

📒 Files selected for processing (3)
  • src/features/rental/search/hook/useSearchPlacesHooks.ts (5 hunks)
  • src/features/rental/search/hook/useSearchPosHooks.ts (2 hunks)
  • src/features/rental/search/utils/address/searchPlaces.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/features/rental/search/hook/useSearchPosHooks.ts (2)
src/features/rental/search/utils/address/searchPlaces.ts (1)
  • PlaceSearchResult (2-10)
src/shared/lib/queryClient.ts (1)
  • queryClient (3-10)

@arty0928 arty0928 changed the base branch from main to develop October 12, 2025 08:13
@arty0928 arty0928 changed the title Refactor/bada 388 search debounce Performance: 위치 검색창 디바운스·AbortController 적용으로 중복 요청 제거 Oct 12, 2025
@arty0928 arty0928 merged commit d681c93 into develop Oct 12, 2025
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

👍Performance 성능 최적화 🔨 Refactor 코드 리팩토링

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] 대여_검색 디바운싱 적용

2 participants