Загружаем каталог…
Загружаем каталог…
2026 TECHEER SUMMER SW BOOTCAMP - 프론트엔드 최적화 기록 OWLBY 는 Windows·macOS 기기의 보안 이벤트를 검색하고 조사하는 엔드포인트 탐지 및 대응(EDR) 대시보드입니다. 이 중에서 최적화가 가장 필요했던 부분은 수집된 이벤트를 프로세스명 등의 조건으로 찾는 Events 검색 화면 이었어요. 예를 들어 Events 검색창 에서 프로세스명에 powershell.exe 를 입력하는 상황에서, 아직 pow 까지만 쳤는데 브라우저는 p , po , pow 로 각각 검색하는 문제가 있었습니다. 빠르게 14글자를 타이핑 하는 동안 검색 요청도 14번 시작됐죠. Issue: 글자 하나에 검색 요청 하나가 붙어 있었다 Events 화면은 검색 조건을 URL에 저장 합니다. 새로고침하거나 주소를 공유해도 같은 검색을 이어가기 위해서예요. 그런데 입력창도 URL 값을 그대로 사용하고, 타이핑할 때마다 URL을 바꾸고 있었습니다. // 변경 전 EventsPage의 핵심 흐름 <input value={params.get(field) ?? ""} onChange={event => setParams(updateParams(params, { [field]: event.target.value })) } /> useQuery({ queryKey: ["events", query], queryFn: ({ signal }) => api.events(query, signal), }); TanStack Query는 URL에서 만든 조회 조건 을 조회 키 로 사용합니다. 따라서 글자 하나를 입력할 때마다 다음 과정이 반복됐죠. 입력 → URL 변경 → 조회 키 변경 → API 요청 이전 요청은 AbortSignal 로 취소했지만, 브라우저에서 취소하기 전에 서버가 검색을 시작했을 수 있습니다. 반대로 요청을 줄이려고 검색을 늦추면 결과도 늦게 보일 수 있어요. 그래서 브라우저에서 시작한 요청 수 와 마지막 입력부터 최종 결과 행이 DOM에 나타난 다음 프레임까지의 시간 을 함께 측정했습니다. Approach 1. 디바운스 - 요청을 줄였지만 더 느려진 결과 출처: 이웅모, 모던 자바스크립트 Deep Dive, 2020. 첫 번째로 적용한 방법은 디바운스 였습니다. 디바운스Debounce 는 연속해서 발생하는 이벤트(e)를 그룹화하여, 마지막 이벤트가 발생하고 일정 시간이 지난 후에 단 한 번만 함수(f)를 호출하도록 제어하는 최적화 기술입니다. 입력이 이어지는 동안 타이머를 다시 시작하고, 마지막 입력 후 150ms 동안 새 글자가 없을 때 검색하는 방식을 적용했습니다. 입력창에 보이는 값( draft )은 즉시 바꾸고 검색 조건인 URL만 늦게 갱신했습니다. 한글을 조합하는 중에는 검색어를 확정하지 않고, Enter를 눌렀을 때는 바로 검색하도록 했어요. 입력 → draft 즉시 변경 └ 마지막 입력 후 150ms → URL 변경 → API 요청 타이머가 URL에 이전 글자를 반영하는 순간 새 글자가 들어오면, 새 입력을 이전 URL 값이 덮는 문제 도 있었습니다. 타이머가 p 를 URL에 확정합니다. 사용자가 o 를 입력해 draft 는 po 가 됩니다. 늦게 처리된 URL 동기화가 draft 를 다시 p 로 덮습니다. 요청을 줄이다가 검색어를 잃어버린 거죠. 화면이 직접 바꾼 URL에는 입력값을 다시 맞추지 않고, 브라우저 뒤로 가기처럼 외부에서 주소가 바뀌었을 때만 URL 값을 읽도록 고쳤습니다. 이 첫 번째 실험은 실제 DB가 아닌 로컬 합성 HTTP API 로 진행했습니다. 아래 요청 수와 행 표시 시간은 각 조건의 중앙값 입니다. 입력 간격 네트워크 시작한 요청 마지막 입력부터 결과 행 표시 100ms 일반 14 → 1회 83.8 → 241.4ms 100ms 제한 14 → 1회 250.7 → 411.4ms 400ms 일반 14 → 14회 88.9 → 251.5ms 400ms 제한 14 → 14회 257.2 → 400.3ms 환경: 100개 고정 레코드와 50ms API 응답 대기 사용, 조건마다 20회 측정 일반 : 추가 네트워크 제한이 없는 로컬 연결 제한 : 브라우저에서 지연 200ms·다운로드 1.6Mbps·업로드 750Kbps를 설정한 조건(CPU는 제한X) 빠른 입력에서는 요청을 14회에서 1회로 줄였지만, 최종 결과는 약 150ms 늦어졌습니다. 느린 입력에서는 글자 사이에 타이머가 끝나 요청이 그대로 나갔고, 결과만 늦어졌어요. 요청 수를 줄이는 데는 성공했지만, 검색 결과를 빨리 보여주는 방법은 아니었습니다. 그래서 요청을 늦추는 대신 실제 API의 응답 경로를 살펴봤죠. 실제 API는 결과 행을 찾고도 전체 건수를 기다렸어요 기존 GET /api/v1/events 는 현재 페이지에 보여줄 행을 찾은 뒤, 검색 조건에 맞는 전체 이벤트가 몇 건인지 다시 셌어요. 전체 건수가 있어야 페이지 수를 정확히 표시할 수 있었기 때문입니다. # 기존 이벤트 목록의 실행 순서 rows = events.search(..., limit=query.size, offset=offset) total = events.count_search(...) return rows, total 두 값을 하나의 응답으로 보내니, 행을 찾았어도 전체 건수를 세는 쿼리( count_search )가 끝날 때까지 목록을 그릴 수 없었습니다. 목록에는 보이지 않는 원본 이벤트 내용( raw_payload )까지 행 조회에서 읽고 있었고요. 현재 페이지의 이벤트 행은 전체 건수 없이도 보여줄 수 있습니다. 다음 페이지가 있는지만 알면 건수를 기다리지 않고 탐색할 수도 있죠. Approach 2. COUNT 쿼리의 결과 표시 크리티컬 패스 분리 크리티컬 패스 는 입력 후 결과 행이 보이기까지 반드시 끝나야 하는 작업의 순서입니다. 기존에는 행 조회뿐 아니라 전체 건수 조회도 이 경로에 있었어요. 두 번째 시도에서는 행과 건수를 서로 다른 요청 으로 나눠, 행이 건수를 기다리지 않도록 했습니다. 다른 화면에서 쓰는 기존 API는 그대로 두고, 검색 화면에 두 경로를 추가했어요. 경로 반환 값 조회 시점 /api/v1/events/rows 현재 페이지 행, 다음 페이지 여부 검색 조건이 바뀌면 바로 /api/v1/events/count 정확한 전체 건수 입력이 500ms 동안 멈추고 행 응답이 도착하면 예를 들어 한 페이지에 50건을 보여준다면 행 조회는 51건을 가져옵니다. 51번째 행이 있으면 다음 페이지가 있다는 뜻이죠. 이 값이 `hasMore`예요. 행 조회에서 읽는 열도 목록 화면에 필요한 것으로 제한했습니다. # EventService.list_page의 현재 DB 조회 경로를 줄인 코드 rows = events.search( **filters, sort_order=query.sort_order, limit=query.size + 1, offset=offset, columns=EVENT_LIST_COLUMNS, ) return [_event_dto(row) for row in rows[:query.size]], len(rows) > query.size 프론트엔드는 다시 글자 입력과 동시에 행을 검색합니다. 입력 중에는 현재 URL 기록을 교체해 글자마다 뒤로 가기 기록이 쌓이지 않도록 했어요. 건수만 입력이 500ms 동안 멈추고 해당 조건의 행이 도착했을 때 조회합니다. // EventsPage의 실제 조회 로직을 간추린 예시 const countQuery = { ...query }; delete countQuery.page; delete countQuery.size; delete countQuery.sortOrder; const queryIdentity = JSON.stringify(countQuery); const [countIdentity, setCountIdentity] = useState(queryIdentity); useEffect(() => { const timer = window.setTimeout(() => setCountIdentity(queryIdentity), 500); return () => window.clearTimeout(timer); }, [queryIdentity]); const rows = useQuery({ queryKey: ["events-rows", query], queryFn: ({ signal }) => api.eventRows(query, signal), enabled: !invalid, }); const countReady = countIdentity === queryIdentity && Boolean(rows.data); const count = useQuery({ queryKey: ["events-count", countQuery], queryFn: ({ signal }) => api.eventCount(countQuery, signal), enabled: !invalid && countReady, }); const total = countReady ? count.data?.data.total : undefined; 검색 조건이 바뀌면 새 조회 키를 사용하고, 새 조건의 건수가 준비되기 전에는 전체 건수를 표시하지 않습니다. 따라서 이전 검색의 건수가 새 행 옆에 붙지 않습니다. 건수 조회가 실패해도 행은 유지하고 건수만 재시도할 수 있습니다. 변경 전 : 행 조회 → COUNT 조회 → 응답 → 결과 행 표시 변경 후 : 행 조회 → 응답 → 결과 행 표시 입력 정지 500ms + 행 응답 → COUNT 조회 → 전체 건수 표시 전체 건수를 나중에 가져온다는 점은 Lazy Loading과 비슷합니다. 다만 건수는 스크롤이나 클릭을 기다리지 않고, 입력이 멈춘 뒤 행 응답이 오면 자동으로 요청해요. 이 글에서는 이를 COUNT 쿼리의 지연 조회(Deferred Fetching) 라고 부르겠습니다. 건수가 아직 없어도 hasMore 로 다음 페이지에 갈 수 있습니다. 건수 조회만 실패했다면 결과 행은 그대로 두고 건수 조회를 다시 시도할 수 있어요. 이제 이렇게 바꾼 화면에서 행이 실제로 얼마나 일찍 나타나는지 측정해봤습니다. 실제 조회 경로에서 결과 행 표시 p75가 낮아졌어요 두 번째 방식은 첫번째 방식과 다른 실험 환경 에서 비교했습니다. 일회용 ClickHouse에 합성 이벤트 5만 건 을 넣고 FastAPI의 실제 Events 조회 경로에 연결했습니다. 인증과 PostgreSQL 메타데이터는 실험용으로 대체했고, API에 고정 응답 지연을 넣지 않았습니다. 각 조건을 10회 측정한 결과 행 표시 p75 예요. p75가 300ms라면 측정한 입력의 75%에서 마지막 글자부터 결과 행이 보이기까지 300ms 이내였다는 뜻입니다. 입력 간격 네트워크 변경 전 행 표시 p75 변경 후 행 표시 p75 변경 후 건수 표시 p75 100ms 일반 384.3ms 217.9ms 618.5ms 100ms 제한 475.8ms 292.7ms 777.3ms 400ms 일반 210.8ms 193.8ms 648.5ms 400ms 제한 282.5ms 277.8ms 775.8ms 빠른 입력에서는 일반·제한 네트워크 모두 최종 행이 더 일찍 보였어요. 느린 입력에서도 p75는 낮아졌지만 차이는 작았습니다. 100ms 간격·제한 네트워크 를 20회 더 비교했을 때도 404.2 → 277.1ms 였어요. 두 화면에 목록 열 선택 변경도 공통으로 적용했습니다. 전체 건수를 기다리지 않고 행을 먼저 보여준 효과 가 느껴지시나요? 결론: 요청 수가 아닌, 결과를 기다리는 시간을 줄였습니다 Events 검색에서 첫 번째로 시도한 150ms 디바운스 는 powershell.exe 입력 중 시작하는 요청을 14회에서 1회로 줄였지만, 최종 결과를 약 150ms 늦췄습니다. 그래서 두 번째 시도에서는 COUNT 쿼리를 결과 행 표시의 크리티컬 패스에서 분리 했습니다. 행은 바로 조회해 보여주고, 정확한 전체 건수는 입력이 멈춘 뒤 따로 가져옵니다. 합성 이벤트 5만 건을 사용한 로컬 API·ClickHouse 실험에서 빠른 입력의 결과 행 표시 p75는 다음과 같이 속도가 개선되었어요. 일반 네트워크 384.3ms → 217.9ms 제한 네트워크 475.8ms → 292.7ms 느린 입력에서도 개선 폭은 작지만 결과 행이 더 일찍 나타났다는 점에서 의의가 있어요. 앞으로 다양한 방법의 성능 최적화를 진행해보고 싶습니다. 참고도서 애디 오스마니·하산 지르데. (2025). 『대규모 리액트 웹 앱 개발: 확장 가능한 대규모 자바스크립트 웹 애플리케이션을 구축하는 방법』. 김모세 옮김. 제이펍. 이웅모. (2020). 『모던 자바스크립트 Deep Dive: 자바스크립트의 기본 개념과 동작 원리』. 위키북스.
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
디바운스에서 COUNT 쿼리 분리까지: Events 검색 최적화. 2026 TECHEER SUMMER SW BOOTCAMP - 프론트엔드 최적화 기록 OWLBY 는 Windows·macOS 기기의 보안 이벤트를 검색하고 조사하는 엔드포인트 탐지·대응(EDR) 대시보드입니다. 이 중에서 최적화가 가장 필요했던 부분은 수집된 이벤트를 프로세스명 등의 조건으로 찾는 Events 검색 화면 이었습니다. 예를 들어 Events 검색창 에서 프로세스명에 powershell.exe 를 입력하는 상황에서, 아직 pow 까지만 쳤는데 브라우저는 p , po , pow 로 각각 검색하고 있었어요. 빠르게 14글자를 타이핑 하는 동안 검색 요청도 14번 시작됐죠. Issue: 글자 하나에 검색 요청 하나가 붙어 있었다…
Открыть источник디바운스에서 COUNT 쿼리 분리까지: Events 검색 최적화. 2026 TECHEER SUMMER SW BOOTCAMP - 프론트엔드 최적화 기록 OWLBY 는 Windows·macOS 기기의 보안 이벤트를 검색하고 조사하는 엔드포인트 탐지 및 대응(EDR) 대시보드입니다. 이 중에서 최적화가 가장 필요했던 부분은 수집된 이벤트를 프로세스명 등의 조건으로 찾는 Events 검색 화면 이었어요. 예를 들어 Events 검색창 에서 프로세스명에 powershell.exe 를 입력하는 상황에서, 아직 pow 까지만 쳤는데 브라우저는 p , po , pow 로 각각 검색하는 문제가 있었습니다. 빠르게 14글자를 타이핑 하는 동안 검색 요청도 14번 시작됐죠. Issue: 글자 하나에 검색 요청 하나가 붙어…
Открыть источник