심심해서 찔러보는 IT

컴퓨터 실행 추적 프로그램

eazy51 2026. 4. 30. 09:06

Claude와 함께 업무 자동 기록 프로그램을 만든 과정

기획 의도 / 사고 확장 흐름 / 버전별 개선 사이클 / 기술 결정 근거


왜 만들었나 - 문제 발견

하루를 마치고 나서 "오늘 뭘 했지?"라는 질문에 명확하게 답하기 어렵다는 문제 인식에서 출발했다. 일정 앱이나 노션 기록은 직접 입력해야 하고, 기억에 의존하면 누락이 생긴다. 수동 기록은 지속하기 어렵다.

목표: OS 레벨에서 활성 창을 자동 감지하고, AI가 1시간 단위로 요약하는 방식으로 기록 자동화를 구현한다.


요구사항 정의: 무엇을 기록할 것인가

프로그램을 만들기 전에 "무엇을 수집할 수 있는가"와 "무엇이 유의미한가"를 먼저 분리했다.

수집 가능한 것들을 나열하면 활성 창 이름, 브라우저 탭 제목, 키보드 입력, 마우스 이동, 스크린샷 등이 있다.

이 중 프라이버시와 정보 밀도 사이의 균형을 기준으로 범위를 정했다.

수집 항목 방식 제외 이유 또는 포함 이유

활성 앱/창 이름 win32gui + psutil 어떤 앱을 썼는지 가장 직접적인 지표
브라우저 탭 제목 창 제목 파싱 URL보다 가독성 높음
키보드 입력 횟수 pynput 작업 강도 지표. 내용은 수집 안 함
스크린샷 프라이버시 문제로 제외
마우스 이동 정보 밀도 낮아 제외

1시간 단위 요약으로 주기를 정한 이유는 너무 짧으면 맥락이 없고, 너무 길면 디테일이 사라지기 때문이다. 하루 업무 사이클 기준으로 1시간이 가장 자연스러운 단위라고 판단했다.


기술 스택 선택 근거

Python을 선택한 이유는 Windows API 접근(pywin32), GUI(tkinter), 배포(PyInstaller) 세 가지를 단일 언어로 처리할 수 있어서다. 외부 프레임워크 없이 표준 라이브러리 중심으로 구성해 의존성을 최소화했다.

win32gui / win32process  →  활성 창 정보 (Windows 전용)
psutil                   →  프로세스 이름 조회
pynput                   →  키보드 이벤트 리스닝
tkinter                  →  GUI (Python 내장)
sqlite3                  →  이벤트 상세 저장 (Python 내장)
http.server              →  대시보드 웹서버 (Python 내장)
PyInstaller              →  단일 exe 패키징

내장 라이브러리 비중을 높인 이유는 PyInstaller로 단일 exe 배포 시 외부 패키지가 많을수록 빌드 오류와 용량이 증가하기 때문이다.


버전 발전 과정

v1 — 작동하는 최소 단위 먼저

첫 번째 목표는 "기능이 완성되는 것"이 아니라 "작동하는 것"이었다.

구조:

activity_tracker.py   ←  추적 + 스케줄러 + GUI 한 파일에
dashboard.html        ←  기록 열람용 정적 HTML
setup_and_run.bat     ←  설치 + 실행 자동화

저장 구조를 두 가지로 나눈 이유가 있다. SQLite는 상세 이벤트(앱명, 창 제목, 지속 시간)를 저장하고, JSON은 대시보드에서 바로 읽을 수 있는 요약 데이터를 저장한다. DB는 나중에 쿼리가 필요할 때를 위한 것이고, JSON은 빠른 서빙을 위한 것이다.

첫 번째 문제: bat 파일의 한글과 특수문자(╔, ║, ✓, ├)가 Windows 콘솔에서 명령어로 인식되어 실행 오류가 발생했다.

원인은 Windows 콘솔의 기본 인코딩과 bat 파일 인코딩의 불일치였다. chcp 65001로 UTF-8을 지정해도 배치 파일 자체의 문자가 파싱 전에 해석되는 문제가 있다.

결론: bat 파일은 ASCII 문자만 사용한다. 한글, 이모지, 박스 문자 전부 금지.


v2 — 외부 연동 가능성 탐색

기록을 내부에만 저장하는 것에서 한 단계 더 나아가, 외부 서비스로 내보내는 방향을 검토했다.

Google Sheets 연동은 매시간 요약 행을 자동 추가하는 방식으로 설계했다. Google Calendar는 활동 요약을 이벤트로 등록해 시간대별로 조회할 수 있게 했다. OAuth 2.0 인증을 google_token.json으로 캐시해서 매번 로그인하지 않아도 되도록 구성했다.

전략 전환 이유: 기능 자체는 완성됐지만, 사용자가 Google Cloud Console에서 OAuth 클라이언트를 직접 발급해야 한다는 진입 장벽이 있었다. 배포 대상이 일반 사용자라면 이 과정이 프로그램 자체보다 더 복잡하다. exe 하나로 해결하는 방향이 더 실용적이라고 판단해 구글 연동은 선택 기능으로 남겨두고 진행했다.


v3 — 배포 가능한 단일 exe

핵심 질문: Python이 설치되지 않은 환경에서도 실행할 수 있어야 한다.

PyInstaller의 --onefile 옵션으로 Python 런타임, 모든 라이브러리, 대시보드 HTML까지 하나의 exe 파일에 패키징했다. 이때 중요한 코드 패턴이 있다.

if getattr(sys, "frozen", False):
    EXE_DIR = Path(sys.executable).parent  # exe 실행 중
else:
    EXE_DIR = Path(__file__).parent        # 스크립트 실행 중

PyInstaller로 빌드된 exe는 실행 시 임시 폴더(_MEIPASS)에 리소스를 풀기 때문에, 데이터 저장 경로를 잘못 잡으면 임시 폴더에 기록이 쌓이다가 재실행 시 사라진다. 반드시 exe 파일의 실제 위치(sys.executable)를 기준으로 경로를 잡아야 한다.

대시보드 HTML을 코드 안에 내장한 이유: 단일 exe 배포를 위해서는 html 파일을 별도로 같이 배포하거나, 코드 안에 포함시키는 두 가지 방법이 있다. 파일을 별도로 배포하면 사용자가 파일을 지웠을 때 대시보드가 깨진다. 문자열로 내장하면 exe 하나만으로 완결된다.

두 번째 문제 — 대시보드 자동 연동: HTML 파일을 브라우저로 열면(file:// 프로토콜) 브라우저 보안 정책상 fetch()로 로컬 파일을 읽을 수 없다. 즉, 대시보드가 activity_log.json을 자동으로 읽어올 수 없고 매번 파일 선택이 필요하다.

해결: Python 내장 http.server로 localhost 웹서버를 띄운다.

# GET /      → 대시보드 HTML 반환
# GET /data  → activity_log.json 실시간 반환
socketserver.TCPServer(("127.0.0.1", 19473), _DashHandler)

http://127.0.0.1:19473으로 접속하면 같은 오리진에서 /data를 fetch할 수 있어 보안 제약이 없다. 브라우저에서 60초마다 자동으로 데이터를 가져오므로 별도 조작 없이 실시간 연동된다.

세 번째 문제 — Windows Tkinter 이모지: 버튼 텍스트에 ⚡ 📊 🔄 ⚙를 사용했더니 Windows에서 버튼이 아예 렌더링되지 않았다. Windows의 Tkinter는 이모지 렌더링을 지원하지 않아 레이아웃 계산이 실패한다.

[ 지금 요약 ], [ 대시보드 열기 ] 형태의 텍스트 버튼으로 교체하고, 버튼 위치도 창 하단에서 API Key 입력창 바로 아래로 올렸다. 창 높이에 따라 버튼이 잘리는 문제를 구조적으로 해결하기 위해서다.


v4 — 자동 수집의 한계를 보완하는 UX

한계 인식: 창 제목 기반 수집은 "Chrome을 켜놨다"는 것은 알 수 있지만, "Chrome에서 무엇을 했는가"는 알 수 없다. 브라우저 탭을 URL 단위로 수집하려면 Chrome 확장프로그램이 필요하고, 이는 설치 복잡도를 높인다.

해결 방향: 자동 수집의 정확도를 높이려는 대신, 사용자가 직접 맥락을 보완할 수 있는 수단을 제공한다.

메모 팝업 설계

매시간 요약 직전에 팝업 창이 뜨는 구조다. 설계 시 고려한 요소들이 있다.

위치: 화면 우측 하단. 작업 중인 창을 가리지 않는 위치다.

w.geometry(f"380x360+{sw-400}+{sh-410}")

타임아웃: 30초 후 자동으로 닫힌다. 팝업이 작업을 방해하면 안 되므로, 무시하면 그냥 넘어가도록 했다. 팝업이 강제적이면 사용자가 프로그램 자체를 꺼버리는 결과로 이어진다.

태그 시스템: 자유 텍스트 입력 외에 "집중 작업 / 휴식 / 미팅 / 리서치" 태그를 제공했다. 타이핑하기 귀찮을 때 클릭 한 번으로 분류할 수 있게 하기 위해서다.

AI 요약과의 연결: 메모 내용은 Claude API에 보내는 프롬프트에 포함된다. 앱 사용 데이터만으로는 맥락을 모르지만, 사용자가 "발표 준비"라고 입력하면 AI가 훨씬 정확한 요약을 생성할 수 있다.

테마 시스템 설계

3종 테마를 딕셔너리로 정의하고, T(key) 함수 하나로 현재 테마의 색상을 조회하는 구조다.

THEMES = {
    "dark":   { "BG": "#08081a", "PUR": "#a78bfa", ... },
    "light":  { "BG": "#faf7f2", "PUR": "#6b5b3e", ... },
    "purple": { "BG": "#f5f3ff", "PUR": "#5b21b6", ... },
}

def T(key):
    return THEMES.get(CFG.get("theme", "dark"), THEMES["dark"])[key]

설정에서 테마를 변경하면 _build_ui()를 호출해 전체 UI를 다시 그린다. 위젯별로 색상을 바꾸는 방식보다 전체 재빌드가 코드가 단순하고 누락이 없다.

통계 대시보드

v3까지는 시간별 기록만 표시했다. v4에서 탭 4개를 추가했다.

데이터는 이미 activity_log.json에 모두 있기 때문에, 추가 수집 없이 JavaScript로 집계하는 방식을 선택했다. 서버에서 집계해 내려주는 방식보다 대시보드 HTML만 수정하면 되어 유지보수가 간단하다.

탭 데이터 소스 집계 방식

앱 통계 top_apps[].seconds 기간 전체 합산, 오늘 필터
브라우저 browser_tabs[] 제목 기준 빈도수 카운트
히트맵 period_start, key_count 시(hour) 추출 후 합산
요일 생산성 summary 텍스트 정규식으로 점수 파싱

실사용 중 발견한 버그: 데이터 누적 오류

배포 후 실사용 단계에서 "어제 기록이 대시보드에 안 보인다"는 문제가 발생했다.

증상 → 원인 탐색 과정:

1차로 JSON 파일을 확인했다. 데이터는 있었다. 즉 저장 자체는 되고 있다.

2차로 웹서버 코드를 확인했다. LOG_PATH 하나만 읽고 있었다.

3차로 실제 JSON 파일 내용을 열어봤다.

[ ...어제 기록 19개... ]
[ ...오늘 기록 1개...  ]

배열이 두 개 붙어있었다. 이 형태는 유효한 JSON이 아니므로 json.loads()가 예외를 발생시킨다.

근본 원인: v3 exe와 v4 exe를 다른 폴더에서 실행했다. 각 exe는 자기 옆에 ActivityTracker_Data/를 만들기 때문에, 데이터 폴더가 두 곳에 생겼다. 새 폴더에서 실행하면 기존 JSON이 없으므로 logs = []로 초기화되고, 새 기록 1개만 저장된다. 다음 실행에서도 같은 일이 반복된다.

코드의 문제는 예외 처리 방식에 있었다.

# 기존 코드 - 파싱 실패 시 조용히 초기화
try:
    logs = json.loads(LOG_PATH.read_text("utf-8"))
except:
    logs = []  # 기존 데이터 전부 날림

수정 방향: 파싱 실패 자체를 복구 대상으로 처리했다.

def _safe_load_logs():
    # [ ][ ] 형태를 감지 → 각 배열을 분리 파싱 → 병합 → 정상 파일로 덮어쓰기
    # period_start 중복 제거 후 시간순 정렬

데이터 손실 방지를 위한 원칙을 하나 세웠다. except: pass 이후 데이터를 초기화하는 패턴은 쓰지 않는다. 실패하면 빈 상태로 계속 진행하는 것보다, 실패 원인을 복구하려는 시도가 먼저여야 한다.


각 버전 핵심 결정 요약

버전 핵심 결정 이유

v1 SQLite + JSON 이중 저장 DB는 쿼리용, JSON은 서빙용으로 역할 분리
v2 Google 연동을 선택 기능으로 격리 진입 장벽이 코어 기능보다 커지는 문제
v3 localhost 웹서버 file:// 보안 제약 우회, 자동 연동
v3 HTML 코드 내장 단일 exe 완결성
v3 이모지 제거 Windows Tkinter 렌더링 불가
v4 메모 팝업 (30초 타임아웃) 브라우저 내용 수집 한계 보완, 비강제적 설계
v4 JS 클라이언트 집계 서버 수정 없이 대시보드만 업데이트 가능
fix _safe_load_logs() except 후 초기화 패턴 제거

회고: PM으로서 배운 것

범위 설정의 기준은 "수집 가능한 것"이 아니라 "유의미한 것"이다.

스크린샷, 마우스 이동 등 수집할 수 있는 데이터는 많다. 하지만 프라이버시 부담과 정보 밀도를 함께 고려하면 범위는 자연스럽게 좁아진다.

배포 대상이 누구인지가 기술 선택을 결정한다.

Google 연동은 기능적으로 완성됐지만, 설치 과정이 사용자에게 너무 복잡했다. 기능의 완성도보다 사용자가 실제로 쓸 수 있는지가 먼저다.

자동화의 한계를 수동 입력으로 보완하는 구조.

브라우저 탭 제목만으로는 무엇을 했는지 정확히 알 수 없다. 100% 자동을 목표로 복잡도를 높이는 대신, 사용자가 직접 맥락을 보완하는 메모 팝업을 설계했다. 데이터 수집의 완전성보다 실제 사용 지속성이 더 중요하다.

에러 처리는 "조용히 넘어가기"가 아니라 "복구 시도"여야 한다.

except: pass로 초기화하는 패턴은 당장은 오류가 안 나지만, 데이터 손실이라는 더 큰 문제를 뒤에서 만든다.


앞으로 더 생각해 볼것

  • 윈도우 생태계말고 IOS 적용방법
  • 더 간편한 방식?
  • 다양한 테마 디자인

사용 도구: Claude AI (코드 생성 및 디버깅), Python 3.11, PyInstaller, Tkinter, SQLite


프로그램 파일

ActivityTracker.exe
10.34MB

 

프로그램 화면과 대시보드 화면


최종 정리



신규 기능
 
구조 개선
 
버그/문제 수정
v1
 
초기 프로토타입
아이디어 검증 단계. 핵심 기능만 구현해 작동 여부 확인.
앱/창 추적 (win32gui) 키보드 횟수 집계 1시간 자동 요약 SQLite + JSON 이중 저장 Tkinter GUI HTML 대시보드 분리
bat 파일 한글/특수문자 → Windows 콘솔 깨짐. ASCII only 원칙 정립.
v2
 
외부 연동 확장 시도
기록을 외부 서비스로 내보내는 방향 탐색. 별도 모듈로 분리 설계.
Google Sheets 자동 저장 Google Calendar 이벤트 등록 OAuth 2.0 인증 캐시
기능 구현 완료. 단, 설치 복잡도 증가 → exe 단순 배포 방향으로 전략 전환.
v3
 
단일 exe 패키징 + 대시보드 자동 연동
배포 가능성 확보. Python 없는 환경에서도 실행. 대시보드 수동 로드 문제 해결.
PyInstaller 단일 exe dashboard.html 코드 내장 localhost:19473 웹서버 Live Sync (60초 자동 갱신) 버튼 이모지 → 텍스트 교체 버튼 위치 상단 이동
Tkinter 이모지 렌더링 불가 → 버튼 미표시. file:// 보안 제약 → fetch 차단.
v4
 
UX 고도화 + 통계 대시보드
자동 수집의 한계를 수동 입력으로 보완. 누적 데이터 시각화. 사용자 커스터마이징.
메모 팝업 (태그 + 카운트다운) 3종 테마 (Dark / Light / Purple) 앱 통계 탭 브라우저 탭 빈도 분석 활동 히트맵 (24h) 요일별 생산성 차트 메모 → AI 프롬프트 반영
fix
데이터 누적 버그 수정
실사용 중 발견. 증상 → 원인 → 코드 수정까지 전체 디버깅 사이클 수행.
JSON 이중 배열 파싱 오류 _safe_load_logs() 자동 복구 period_start 중복 체크 주변 폴더 자동 병합
exe 실행 폴더가 달라 ActivityTracker_Data가 두 곳에 생성 → JSON 파싱 실패 → 기존 기록 초기화 반복.