안녕하세요 와탭랩스입니다. 와탭랩스는 웹 기반으로 다양한 모니터링 서비스를 제공하고 있습니다. 그중 로그 모니터링은 하루 최대 24억 건의 데이터를 수집하고 조회할 수 있을 만큼 대규모 데이터를 처리합니다.
이번 글에서는 애플리케이션, 서버, 데이터베이스, 쿠버네티스, 네트워크 장비 등에서 수집된 로그를 실시간으로 확인할 수 있는 ‘라이브 테일(Live Tail)’ 화면의 UX를 개선하기 위해, 우리가 정의했던 문제와 그 해결 과정을 공유하려고 합니다.


위 영상을 보셨을 때 어떤 생각이 드셨나요? 몇몇 고객분들이 아래와 같은 문제들을 제기하였고, 저 또한 이 문제에 공감했습니다.
로그 모니터링 고객은 터미널에서 tail -f 명령어를 입력하여 사용하는 UX에 익숙하여 이를 참고하기로 결정했습니다.

와탭의 로그 데이터는 하루 최대 24억 건을 수용할 수 있기에 virtualized 기법을 사용하여 화면에 보이는 요소만 렌더링하고 있습니다.
자동 스크롤: 새로운 로그가 들어왔을 때, 최하단으로 움직이는 스크롤 이벤트
수동 스크롤: 사용자가 스크롤을 직접 움직이는 스크롤 이벤트
사용자가 과거 데이터를 확인하기 위해 스크롤을 위로 올리면 자동 스크롤이 일시 중지되어 로그를 안정적으로 읽을 수 있습니다. 이후 사용자가 스크롤을 최하단으로 내리면, 자동으로 최신 로그 스트리밍이 다시 시작됩니다.

애니메이션은 스크롤 이동 속도를 조절하여 구현할 수 있습니다. 애니메이션의 주기를 설정하고 진행률을 계산 후, scrollTop을 업데이트하는 방식으로 구현했습니다.
export const linear = (startValue: number, endValue: number, t: number): number => {
return startValue + (endValue - startValue) * t;
};
export const easeInOutQuad = (t: number): number => {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
};
scrollTop값의 업데이트 전후 값을 비교하여 자동 및 수동 스크롤 여부를 판단할 수 있다고 생각했습니다. 하지만 virtualized 기법을 사용하고 있어 업데이트 전후 scrollTop값이 동일하다는 것을 예측하기 어렵다고 생각했고, 로그 컨텐츠는 높이가 컨텐츠에 따라 동적으로 달라지기에 scrollTop으로 비교가 불가능했습니다.
그리하여 자동 및 수동 스크롤을 판단하는 이벤트를 구분하는 방식을 접근했습니다.
하지만 동일한 브라우저 이벤트를 사용하고 있어, 구분이 어려웠습니다.
사용자가 스크롤을 움직일 때는 mouse, touch, wheel 이벤트로 수동 스크롤 상태를 판단하고, mouse, touch이벤트에서는 사용자가 스크롤 바 영역을 조작했는지 여부를 판단했습니다.
const handleMouseDown = (e) => {
const isScrollbar = e.offsetX > e.currentTarget.clientWidth;
if (!isScrollbar) {
return;
}
updateScrollStatus('manual');
stopScrollAnimation();
};
element.addEventListener('mousedown', handleMouseDown);
로그 데이터는 데이터마다 컨텐츠의 크기가 다르다는 특성이 있습니다.


요소의 높이가 고정된 가상화 리스트에서는 element.scrollTop = element.scrollHeight - element.clientHeight로 최하단 이동이 가능합니다. 하지만 요소마다 높이가 서로 다른 가상화 리스트에서는 DOM 요소들이 동적으로 생성/제거되고 높이가 실시간으로 변경되기 때문에 이 방법이 제대로 작동하지 않습니다.
그리하여 rAF를 활용하여 auto 모드일 때, 지속적으로 바닥으로 자동 스크롤하도록 설계했습니다.
function startContinuousScroll() {
function continuousScrollLoop() {
// 이전 애니메이션 취소
if (rAFIdRef.current) {
cancelAnimationFrame(rAFIdRef.current);
}
if (scrollMode !== 'auto' || isUserInteracting) {
return;
}
// 매 프레임마다 최신 scrollHeight 계산
const maxScrollTop = element.scrollHeight - element.clientHeight;
element.scrollTop = maxScrollTop;
// 다음 프레임에서 다시 실행
rAFIdRef.current = requestAnimationFrame(() => {
continuousScrollLoop();
});
}
rAFIdRef.current = requestAnimationFrame(continuousScrollLoop);
}




세 가지 문제들을 해결함으로써,목표로 했던 사용자 경험을 제공할 수 있었습니다.
추가적으로 실시간 대용량 로그 렌더링에 필요한 힙 메모리를 효율적으로 사용하는 방법 또한 고민해야 하는데요. 이 경험은 다음 편에서 소개드리겠습니다. 감사합니다.