버튼 클릭 이벤트를 모아서 로깅하기

슽이·2025년 4월 27일
0
post-thumbnail

1. 요구사항

특정 feature가 추가되면서 사용성이 얼마나 되는지를 알기 위해 버튼 클릭 이벤트를 로깅 해야 했습니다.

하지만 클릭할 때마다 서버로 요청을 보내는 방식은 서버 부하가 크고, 네트워크 문제로 실패할 위험도 있었습니다.
그래서 클라이언트에 로그를 모아두었다가, 일정량 이상 쌓이면 서버로 전송하는 방식을 고민하게 되었습니다.

  • 프론트와 백엔드 스펙
    • 백엔드 : Spring + logback, log4j
    • 프론트 : javascript(ES6불가), juqery

2. 해결 방법

여러 가지 방법을 검토했습니다.

  • localStorage: 동기 처리라서 UI에 영향을 줄 수 있고, 용량도 적었습니다.
  • sessionStorage: 브라우저를 닫으면 데이터가 사라집니다.
  • 쿠키: 매 요청마다 서버로 보내져서 오히려 트래픽만 증가시킬 수 있습니다.
  • IndexedDB: 비동기로 동작하고, 꽤 큰 용량을 저장할 수 있으며 브라우저를 닫아도 유지됩니다.

결론적으로, IndexedDB를 선택했습니다.
비동기 처리, 데이터 유지성, 넉넉한 저장 용량이라는 점이 결정적인 이유였습니다.

또한, 로그를 남기는 목적은 개발자가 디버깅하는 용도가 아니라,
사용자의 행동을 분석하고 제품 개선에 활용하려는 목적이었습니다.


3. IndexedDB로 로깅

3.1 IndexedDB 초기화

var DB_NAME = 'AILoggingDB';
var STORE_NAME = 'logs';
var DB_VERSION = 1;

var AILogger = function() {
    var self = this;
    self.db = null;
    
    self.initDB = function() {
        var request = indexedDB.open(DB_NAME, DB_VERSION);

        request.onerror = function(event) {
            console.error('Database error:', event.target.error);
        };

        request.onupgradeneeded = function(event) {
            var db = event.target.result;
            if (!db.objectStoreNames.contains(STORE_NAME)) {
                db.createObjectStore(STORE_NAME, { keyPath: 'timestamp' });
            }
        };

        request.onsuccess = function(event) {
            self.db = event.target.result;
        };
    };

    self.initDB();
};

3.2 로그를 저장하고 일정량 이상이면 전송

var BATCH_SIZE = 100;

AILogger.prototype.log = function(data) {
    var self = this;
    return new Promise(function(resolve) {
        if (!self.db) {
            resolve();
            return;
        }

        var transaction = self.db.transaction([STORE_NAME], 'readwrite');
        var store = transaction.objectStore(STORE_NAME);

        var logEntry = {
            timestamp: new Date().getTime(),
            data: data
        };

        var addRequest = store.add(logEntry);

        addRequest.onsuccess = function() {
            self.getLogCount().then(function(count) {
                if (count >= BATCH_SIZE) {
                    self.sendLogsToServer().then(resolve);
                } else {
                    resolve();
                }
            });
        };

        addRequest.onerror = function() {
            console.error('Error logging data');
            resolve();
        };
    });
};

AILogger.prototype.getLogCount = function() {
    var self = this;
    return new Promise(function(resolve) {
        var transaction = self.db.transaction([STORE_NAME], 'readonly');
        var store = transaction.objectStore(STORE_NAME);
        var countRequest = store.count();
        countRequest.onsuccess = function() {
            resolve(countRequest.result);
        };
    });
};

AILogger.prototype.sendLogsToServer = function() {
    var self = this;
    return new Promise(function(resolve) {
        var transaction = self.db.transaction([STORE_NAME], 'readonly');
        var store = transaction.objectStore(STORE_NAME);
        var getAllRequest = store.getAll();

        getAllRequest.onsuccess = function() {
            var logs = getAllRequest.result;
            if (logs.length === 0) {
                resolve();
                return;
            }
            // 서버로 전송 (예: axios 사용)
            axios.post('/api/ailogs', logs).then(function() {
                // 전송 성공 후 로그 삭제
                self.clearLogs().then(resolve);
            }).catch(function() {
                console.error('Error sending logs');
                resolve();
            });
        };
    });
};

AILogger.prototype.clearLogs = function() {
    var self = this;
    return new Promise(function(resolve) {
        var transaction = self.db.transaction([STORE_NAME], 'readwrite');
        var store = transaction.objectStore(STORE_NAME);
        var clearRequest = store.clear();
        clearRequest.onsuccess = function() {
            resolve();
        };
    });
};

4. 마무리

로그를 개발자가 직접 보는 것이 아니라,
사용자의 행동 흐름을 간단히 파악하는 목적이라면 이 정도 구조가 충분히 깔끔하고 가벼웠습니다.

IndexedDB를 사용해서 브라우저 안에 데이터를 쌓고,
부하 없이 서버로 천천히 모아서 보내는 방식은 꽤 만족스러웠습니다.

다음에는 쌓인 로그를 조금 더 쉽게 분석할 수 있는 툴을 붙이거나,
자동으로 CSV로 변환하는 기능도 추가해볼 생각입니다.

profile
하고싶은게 많은 개발자가 되고싶은

0개의 댓글