리테일 매장 고객 행동 분석
시각지능 기반 고객 동선 분석 시스템

프로젝트 개요
말레이시아 소재 리테일 매장에서 YOLO 객체 인식 모델을 활용해 매장 내 고객의 동선을 실시간으로 추적하고 분석하는 시스템입니다. CCTV 영상에서 고객의 위치와 이동 경로를 인식하여, 어느 진열대에서 얼마나 머물렀는지, 어떤 동선으로 매장을 이동했는지 등의 데이터를 수집합니다. 이를 통해 매장 관리자는 고객의 행동 패턴을 이해하고, 진열 위치 최적화나 동선 개선 등의 인사이트를 얻을 수 있습니다. 백엔드에서는 PyTorch로 구현된 YOLO 모델이 실시간 영상 처리를 수행하고, 프론트엔드에서는 이 데이터를 받아 대시보드 형태로 시각화하는 역할을 담당했습니다. 프론트엔드 기술 스택으로는 의도적으로 VanillaJS(순수 JavaScript)를 선택했는데, 이는 React나 Vue 같은 프레임워크 없이도 복잡한 실시간 데이터 시각화를 구현할 수 있는지 검증하고, 번들 사이즈와 성능을 최적화하기 위함이었습니다.
프로젝트에서 프론트엔드 데이터 시각화 전반을 담당했으며, VanillaJS로 실시간 대시보드를 구축했습니다. 백엔드 팀이 WebSocket을 통해 전송하는 고객 위치 데이터를 받아, Canvas API와 SVG를 활용한 히트맵과 동선 트래킹 시각화를 구현했습니다. 매장 평면도 위에 고객의 현재 위치를 실시간으로 표시하고, 시간대별 고객 밀집도를 색상 그라데이션으로 나타내는 히트맵을 그렸으며, 개별 고객의 이동 경로를 선으로 연결하여 동선을 추적할 수 있도록 했습니다. 또한 통계 차트(시간대별 방문자 수, 구역별 체류 시간 등)를 Chart.js 라이브러리를 활용해 구현했습니다. 프레임워크 없이 상태 관리와 DOM 조작을 수행해야 했기 때문에, 간단한 상태 관리 패턴(Observer 패턴)과 컴포넌트 분리 구조를 직접 설계했습니다. 실시간으로 들어오는 데이터를 효율적으로 처리하기 위해 렌더링 최적화(RequestAnimationFrame, Debouncing)를 적용했고, 메모리 누수를 방지하기 위한 이벤트 리스너 정리 로직도 구현했습니다.

기술 구현
VanillaJS 기반 실시간 데이터 시각화 대시보드
React나 Vue 같은 프레임워크 없이 복잡한 실시간 대시보드를 구현해야 했습니다. 프레임워크가 제공하는 컴포넌트 구조, 상태 관리, 가상 DOM 등의 편의 기능 없이, 순수 JavaScript로 WebSocket 데이터를 받아 Canvas와 SVG에 실시간으로 렌더링하고, 사용자 인터랙션(줌, 팬, 시간대 필터링 등)을 처리해야 했습니다. 특히 초당 수십 개의 위치 데이터가 들어올 때 DOM을 직접 조작하면 성능 저하가 발생할 수 있어, 효율적인 렌더링 전략이 필요했습니다. 또한 코드 구조가 스파게티 코드로 변질되지 않도록 모듈화와 관심사 분리를 고민해야 했습니다.
ES6 Modules를 활용해 코드를 기능별로 모듈화했습니다. WebSocket 통신을 담당하는 DataService 모듈, Canvas 렌더링을 담당하는 HeatmapRenderer 모듈, SVG 동선 렌더링을 담당하는 PathRenderer 모듈, 차트 렌더링을 담당하는 ChartRenderer 모듈 등으로 분리했습니다. 상태 관리는 간단한 Observer 패턴을 구현하여, DataService가 새로운 데이터를 받으면 등록된 렌더러들에게 알림(notify)을 보내 각자 업데이트하도록 했습니다. Canvas 렌더링은 requestAnimationFrame을 활용해 브라우저의 리페인트 주기에 맞춰 렌더링하여 부드러운 애니메이션을 구현했고, 불필요한 렌더링을 줄이기 위해 데이터가 변경되었을 때만 다시 그리도록 조건을 설정했습니다. 히트맵은 2D 배열에 고객 체류 시간을 누적하고, 이를 색상 그라데이션으로 변환하여 Canvas에 그리는 방식으로 구현했습니다. SVG는 각 고객의 이동 경로를 path 엘리먼트로 표현하여 동적으로 추가/제거했습니다. 사용자 인터랙션(드래그로 화면 이동, 마우스 휠로 줌)은 이벤트 리스너를 등록하여 Canvas transform을 조작하는 방식으로 처리했습니다.
프레임워크 없이도 안정적이고 성능이 우수한 실시간 대시보드를 구현할 수 있었습니다. 번들 사이즈가 작아(Chart.js 제외 시 100KB 미만) 초기 로딩이 매우 빨랐고, Canvas 기반 렌더링 덕분에 수백 개의 데이터 포인트를 동시에 표시해도 60fps를 유지할 수 있었습니다. 모듈화된 코드 구조 덕분에 새로운 시각화 기능(예: 특정 구역 클릭 시 상세 정보 표시)을 추가할 때도 다른 모듈에 영향을 주지 않고 독립적으로 개발할 수 있었습니다. 매장 관리자는 대시보드를 통해 실시간으로 고객 동선을 모니터링하고, 시간대별 패턴을 분석하여 매장 레이아웃 개선에 활용할 수 있었습니다.
Canvas 기반 고객 동선 히트맵 및 트래킹 UI
매장 평면도 위에 고객의 동선을 직관적으로 표현해야 했습니다. 수십 명의 고객이 동시에 이동하는 상황에서 각 고객의 위치와 이동 경로를 실시간으로 표시하면서도, 전체적인 패턴(어느 구역이 인기 있는지, 어떤 동선이 많은지)을 한눈에 파악할 수 있는 시각화가 필요했습니다. 히트맵은 시간이 지남에 따라 누적되는 데이터를 색상으로 표현해야 하고, 동선 트래킹은 개별 고객의 경로를 선으로 연결하여 보여줘야 했습니다. 또한 사용자가 시간대를 선택하면 해당 시간대의 데이터만 필터링하여 보여주는 기능도 필요했습니다.
Canvas API를 활용해 매장 평면도를 배경으로 그리고, 그 위에 히트맵 레이어와 동선 레이어를 오버레이하는 방식으로 구현했습니다. 히트맵은 매장을 그리드로 나누고(예: 1m x 1m), 각 그리드 셀에 고객이 머문 시간을 누적한 뒤, 값의 크기에 따라 파란색(낮음) → 초록색(중간) → 빨간색(높음) 그라데이션으로 표현했습니다. Canvas의 fillRect와 createLinearGradient를 활용해 부드러운 색상 전환을 구현했습니다. 동선 트래킹은 각 고객의 이동 경로를 배열에 저장하고, Canvas의 lineTo와 stroke를 사용해 경로를 선으로 그렸습니다. 시간대 필터링은 사용자가 슬라이더를 조작하면 해당 시간 범위의 데이터만 필터링하여 히트맵과 동선을 다시 렌더링하도록 구현했습니다. 데이터가 많을 때 성능을 위해, 전체 데이터를 미리 계산하여 캐싱하고, 필터링 시에는 캐시된 데이터를 참조하여 빠르게 렌더링했습니다. 또한 고객이 특정 구역을 클릭하면 해당 구역의 상세 통계(평균 체류 시간, 방문 고객 수 등)를 툴팁으로 표시하는 인터랙션도 추가했습니다.
매장 관리자는 히트맵을 통해 '어느 진열대가 가장 인기 있는지', '고객이 잘 가지 않는 사각지대는 어디인지'를 시각적으로 파악할 수 있었습니다. 동선 트래킹을 통해 '입구에서 들어온 고객이 주로 어떤 경로로 이동하는지', '계산대까지 가는 동선이 효율적인지' 등을 분석할 수 있었습니다. 시간대 필터링 기능 덕분에 평일 오전, 주말 저녁 등 시간대별 패턴 차이를 비교할 수 있었고, 이를 바탕으로 진열 위치나 프로모션 전략을 조정하는 데 활용되었습니다. Canvas 기반 렌더링으로 수백 개의 경로를 동시에 표시해도 성능 저하 없이 부드럽게 동작했습니다.
트러블슈팅
실시간 데이터 렌더링 시 메모리 누수
대시보드를 장시간 실행하면 브라우저의 메모리 사용량이 계속 증가하여 결국 브라우저가 느려지거나 크래시되는 문제가 발생했습니다. 초기에는 WebSocket으로 들어오는 데이터를 계속 배열에 누적하고, Canvas를 렌더링할 때마다 전체 데이터를 순회하며 그렸는데, 시간이 지날수록 배열 크기가 커져 렌더링 속도가 느려지고 메모리를 과도하게 사용하게 되었습니다. 또한 이벤트 리스너를 제대로 정리하지 않아 메모리 누수가 발생했습니다.
데이터 보관 전략을 수정하여, 일정 시간(예: 1시간) 이상 지난 데이터는 자동으로 삭제하도록 했습니다. 순환 버퍼(Circular Buffer) 패턴을 적용해 최대 데이터 크기를 제한하고, 새로운 데이터가 들어오면 가장 오래된 데이터를 제거하는 방식으로 변경했습니다. 렌더링 최적화를 위해 전체 데이터를 매번 그리지 않고, 변경된 부분만 다시 그리는 부분 업데이트 방식을 도입했습니다. 이벤트 리스너는 컴포넌트가 제거될 때(페이지 이동 시) removeEventListener를 호출하여 명시적으로 정리했습니다. WebSocket 연결도 페이지를 벗어날 때 close()를 호출하여 연결을 종료했습니다. 또한 Chrome DevTools의 Performance Monitor와 Memory Profiler를 활용해 메모리 누수 지점을 찾아 수정했습니다.
메모리 누수가 해결되어 대시보드를 하루 종일 실행해도 브라우저가 안정적으로 동작하게 되었습니다. 메모리 사용량이 일정 수준 이하로 유지되었고, 렌더링 성능도 개선되어 장시간 사용해도 부드러운 애니메이션을 유지할 수 있었습니다. 매장 관리자는 대시보드를 계속 켜두고 실시간으로 모니터링할 수 있게 되었습니다.
VanillaJS 환경에서 상태 관리 복잡도 증가
프레임워크 없이 순수 JavaScript로 개발하다 보니, 상태 관리가 점점 복잡해졌습니다. 여러 모듈이 동일한 데이터를 참조하고 수정하면서 데이터 동기화 문제가 발생했고, 어떤 모듈이 데이터를 변경했는지 추적하기 어려워 디버깅이 힘들었습니다. 특히 사용자가 필터를 변경하면 여러 차트와 히트맵을 동시에 업데이트해야 했는데, 각 모듈을 수동으로 호출하다 보니 코드가 복잡해지고 에러가 발생하기 쉬웠습니다.
간단한 상태 관리 라이브러리를 직접 구현했습니다. Observer 패턴을 기반으로 중앙 Store 객체를 만들고, 각 모듈(렌더러, 차트 등)은 Store를 구독(subscribe)하여 데이터가 변경되면 자동으로 알림을 받도록 했습니다. Store는 상태를 불변(immutable) 방식으로 관리하여, 상태 변경 시 새로운 객체를 반환하고, 이전 상태와 비교하여 실제로 변경된 부분만 렌더링하도록 최적화했습니다. 상태 변경 로직은 Actions 객체에 정의하여, 모든 상태 변경이 명시적인 액션을 통해 이루어지도록 구조화했습니다. 이를 통해 Redux와 유사한 단방향 데이터 흐름을 VanillaJS 환경에서 구현했습니다. 디버깅을 위해 상태 변경 이력을 콘솔에 로깅하는 미들웨어도 추가했습니다.
상태 관리가 명확해지고 예측 가능해졌습니다. 데이터 변경이 발생하면 구독 중인 모든 모듈이 자동으로 업데이트되어, 수동으로 각 모듈을 호출할 필요가 없어졌습니다. 버그 발생 시 상태 변경 이력을 추적하여 원인을 빠르게 파악할 수 있었고, 새로운 기능 추가 시에도 기존 코드를 수정하지 않고 새로운 모듈을 구독시키기만 하면 되어 유지보수성이 크게 향상되었습니다. VanillaJS로도 충분히 복잡한 상태 관리가 가능하다는 것을 증명했지만, 동시에 React나 Vue 같은 프레임워크가 제공하는 가치(컴포넌트 구조, 가상 DOM, 개발자 도구 등)를 더욱 체감하게 되었습니다.
회고
프레임워크 없이 VanillaJS로 복잡한 실시간 대시보드를 구현하면서, JavaScript의 근본적인 동작 원리와 브라우저 렌더링 메커니즘에 대한 이해가 깊어졌습니다. Canvas API와 SVG를 직접 다루며 저수준 그래픽 렌더링 경험을 쌓았고, WebSocket을 통한 실시간 데이터 처리와 메모리 관리, 성능 최적화 등 실무 프론트엔드 개발에서 중요한 역량을 강화했습니다. 특히 상태 관리 패턴을 직접 설계하고 구현하면서, React나 Vue에서 제공하는 상태 관리 라이브러리가 내부적으로 어떻게 동작하는지 이해할 수 있었고, 이후 Redux나 Vuex를 사용할 때 더 깊이 있는 활용이 가능해졌습니다. 메모리 누수와 렌더링 성능 문제를 해결하며 Chrome DevTools를 활용한 프로파일링 기법을 익혔고, requestAnimationFrame, Debouncing, Observer 패턴 등 실무에서 자주 쓰이는 최적화 패턴을 실전에서 적용해보는 귀중한 경험을 했습니다. Canvas 기반 데이터 시각화를 통해 대용량 데이터를 효율적으로 표현하는 방법을 배웠고, 이는 이후 다른 프로젝트에서 차트나 그래프를 구현할 때 큰 도움이 되었습니다.
VanillaJS로 개발하면서 프레임워크의 필요성을 절감했습니다. 컴포넌트 재사용성, 선언적 UI, 가상 DOM 등 모던 프레임워크가 제공하는 편의 기능 없이 모든 것을 수동으로 구현하다 보니 개발 속도가 느리고 코드 중복이 많았습니다. 특히 여러 모듈 간 데이터 동기화나 DOM 업데이트 로직을 수동으로 관리하는 것이 번거로웠고, 실수로 인한 버그가 자주 발생했습니다. 만약 React를 사용했다면 상태 관리는 useState나 useReducer로 간단히 처리하고, Canvas 렌더링은 useEffect로 관리하여 코드를 훨씬 간결하게 작성할 수 있었을 것입니다. 또한 컴포넌트 기반 구조로 차트, 히트맵, 동선 등을 독립적인 컴포넌트로 분리하여 재사용성과 테스트 용이성을 높일 수 있었을 것입니다. 프로젝트 후반부에는 '이 정도 규모라면 React를 쓰는 게 맞았다'는 생각이 들었고, VanillaJS는 작은 규모의 인터랙티브 요소나 레거시 시스템 유지보수에는 적합하지만, 복잡한 SPA를 구축하기에는 한계가 있다는 것을 체감했습니다. 향후 유사한 프로젝트에서는 기술 선택 단계에서 프로젝트 규모와 복잡도를 고려하여, 적절한 프레임워크를 선택하는 것이 중요하다는 교훈을 얻었습니다. 다음에는 React + D3.js 조합으로 더 나은 구조의 데이터 시각화 대시보드를 구축해보고 싶습니다.