🖥️ Frontend/React

React 입력값 처리 최적화: Debounce vs 클라이언트 필터링

hjwjo 2025. 9. 13. 00:58

 

React에서 사용자의 입력을 처리할 때 흔히 고민하는 문제가 있습니다.

바로 실시간으로 입력값이 바뀔 때 서버 호출을 최소화할 수 있는가 입니다. 

오늘 회사에서 React 입력값 처리 방식에 대해 팀과 의견을 나누는 자리가 있었습니다.
제가 예전에 적용했던 “클라이언트 필터링”과 onChange Debounce 전략에 대해 이야기하다가, 잘 모르면서 상사 분들과 동료 직원분들께 아는 척을 한 순간이 있었습니다.
사실 정답이 있는 것은 아니지만 , 그때 무심코 제가 잘 모르는 Debounce에 대해 코드만 읽고 단순하게 판단하여 ..
지금 생각하면 쥐구멍에라도 숨고 싶은 심정이지만… 이미 엎어진 물은 돌이킬 수 없으니 ㅠ
이 글에서는 당시 제가 고민했던 문제와, 입력값 변화 시 서버 호출을 최소화하는 여러 전략에 대해 정리해보겠습니다.
(앞으로는 잘 모르면서 나서지 말자.)

 


1. Debounce를 이용한 Ajax 호출 최적화

입력이 끝난 후 일정 시간(예: 300~1000ms) 동안 입력이 멈추면 Ajax를 호출하는 방식입니다.
React에서는 useCallback과 lodash.debounce를 조합해 구현하는 것이 일반적입니다.

import React, { useState, useCallback } from "react";
import axios from "axios";
import debounce from "lodash.debounce";

function SearchInput() {
  const [value, setValue] = useState("");

  // 마지막 입력 후 500ms 뒤에 서버 호출
  const sendData = useCallback(
    debounce(async (val) => {
      try {
        const response = await axios.post("/api/search", { query: val });
        console.log("서버 응답:", response.data);
      } catch (err) {
        console.error(err);
      }
    }, 500),
    []
  );

  const handleChange = (e) => {
    const val = e.target.value;
    setValue(val);      // input value 업데이트
    sendData(val);      // Debounce 적용된 서버 호출
  };

  return <input value={value} onChange={handleChange} placeholder="검색..." />;
}

export default SearchInput;

장점

  1. 서버 데이터가 항상 최신 상태임
  2. 입력이 많아도 불필요한 서버 호출을 최소화
  3. 검색, 자동완성, 실시간 검증 등 실무에서 많이 사용

단점

  1. 연속 입력 중 서버 호출이 늦게 발생 → 즉시 반영 어려움
  2. 구현 시 타이머 관리, Axios 취소 등 부가 로직 필요
  3. 컴포넌트 state가 바뀌면 여전히 렌더링 발생

2. 클라이언트 필터링

한 번에 전체 데이터를 서버에서 받아와서, 이후 입력값은 클라이언트 메모리에서 필터링하는 방식입니다.

예를 들어 상품 목록, 사용자 리스트 등을 모두 가져온 뒤 Array.filter()로 필터링합니다.

import React, { useState, useEffect } from "react";
import axios from "axios";

function UserList() {
  const [users, setUsers] = useState([]);
  const [filter, setFilter] = useState("");

  // 처음 한 번 전체 데이터 가져오기
  useEffect(() => {
    axios.get("/api/users").then((res) => setUsers(res.data));
  }, []);

  const filteredUsers = users.filter(user =>
    user.name.toLowerCase().includes(filter.toLowerCase())
  );

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="사용자 검색..."
      />
      <ul>
        {filteredUsers.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

장점

  1. Ajax 호출을 거의 하지 않아 서버 부담 최소화
  2. 입력값 변화에 즉시 반응 → UX 향상
  3. Debounce, Throttle 같은 최적화 필요 없음

단점

  1. 데이터가 많으면 브라우저 메모리 부담 및 초기 로딩 느림
  2. 서버 데이터 변경 반영이 어렵고, 동기화 필요
  3. 큰 데이터는 성능 최적화 필요

3. 결론

  • 데이터 양이 작거나 중간 정도이면 클라이언트 필터링이 직관적이고 효율적
  • 데이터가 많거나 실시간 변화가 중요한 경우에는 Debounce + Ajax 호출 패턴이 안정적
  • 실제 프로젝트에서는 두 방식을 혼합하기도 함
    • 기본적으로 클라이언트 필터링 + 일정 시점 서버 갱신
    • 검색어가 특정 길이 이상일 때만 서버 요청

React를 공부하면서 깨달은 중요한 포인트는 단순히 동작을 구현하는 것이 아니라, 입력값 변화와 렌더링, 서버 호출 최적화를 함께 고민하는 것입니다.

클라이언트 필터링과 Debounce 두 방식의 장단점을 이해하면, UX와 서버 부담을 모두 고려한 설계를 할 수 있습니다.

하지만 제가 이번에 잘못했던 점은, Debounce를 구현하면서 onChange 안에서 처리했고, 300~1000ms 딜레이 설정으로는 타이핑을 충분히 감지하기 어렵다고 생각했습니다. 그 결과, 오히려 과도한 AJAX 호출로 서버에 더 많은 쓰로틀과 부하를 줄 수 있다는 우려를 지나치게 어필하며, 남의 의견을 충분히 듣지 않았던 것입니다.

이번 경험을 통해, 잘 모르는 것에 귀를 열고 늘 배우려는 마음을 갖는 것이 얼마나 중요한지 다시 한번 깨달았습니다.

 

요즘 React와 Spring Boot를 배우면서, 한편으로는 많은 걱정이 되기도 하고, 한편으로는 새로운 것을 배우는 즐거움도 느낍니다.
앞으로도 신기하고 무한히 배울 것이 많다는 생각에 설레면서도, 실무에 바로 적용하려면 수많은 리스크를 감수해야 한다는 고민이 깊어지는 밤입니다.