브라우저 기록은 데이터셋이다: 로컬 LLM으로 개인적이고 사적인 북마크 지능(Brain) 구축하기
요약
이 글은 로컬 LLM을 활용하여 브라우저 기록을 개인화된 지식 데이터셋으로 구축하는 실무자 디자인을 제시합니다. 기존 북마크나 히스토리의 한계를 극복하고, 사용자의 모든 탐색 활동을 의미론적으로 인덱싱하는 것이 핵심입니다. 특히 모든 데이터가 기기 외부로 나가지 않는 강력한 프라이버시 보호 아키텍처에 초점을 맞추고 있습니다.
핵심 포인트
- 북마크는 방치되어 퇴색하고, 히스토리는 체계화되지 않은 한계를 가집니다.
- 로컬 LLM을 활용하여 브라우저 기록 전체를 의미론적 인덱스로 변환합니다.
- 데이터가 기기 외부로 나가지 않는 개인 정보 보호 아키텍처가 핵심입니다.
- 단순 검색을 넘어 'X에 대해 뭘 읽었는지'라는 질문에 답할 수 있습니다.
A story climbing daily.dev Popular this week — "I gave my local LLM my browser history, and it built a better bookmark system than I ever had" (#6 Popular on 29 October 2026, tags #automation #google-chrome, 18 upvotes, 185.1K impressions) — lands because it names a frustration every heavy browser user knows: bookmarks rot, and browser history is a pile you can only search by remembering the exact words in a title. (매일 올라오는 daily.dev 인기 기사 - "내 로컬 LLM에 브라우저 기록을 제공했더니, 내가 가졌던 어떤 북마크 시스템보다 나은 것을 만들어냈다" (#6 Popular on 29 October 2026, tags #automation #google-chrome, 18 upvotes, 185.1K impressions) — 이 이야기는 모든 헤비 브라우저 사용자가 아는 좌절감을 지적하기 때문에 주목받습니다: 북마크가 퇴색하고(rot), 브라우저 기록은 제목에 적힌 정확한 단어를 기억해야만 검색할 수 있는 거대한 더미입니다. 흥미로운 부분은 데모 자체가 아닙니다. 그 밑에 숨겨진 아키텍처입니다. 바로 사용자의 자체 탐색 활동을 로컬에서 인덱싱하고, 의미를 통해 검색하며, 아무것도 기기 외부로 나가지 않게 하는 것입니다.)
저희는 생계를 위해 브라우저 자동화(browser automation)를 구축해 왔으며, 저희의 원칙은 how we work 페이지에 명시되어 있듯이 공격적으로 자동화하되 폭발 반경(blast radius)을 작게 유지하는 것입니다. 사용자의 자체 기록에 대한 개인 인덱스는 이 원칙을 완벽하게 테스트합니다: 데이터는 사용자 소유이며, 가치는 실재하고, 실패 모드—즉, 전체 브라우징 라이프를 호스팅된 모델에 맡기는 것—는 우리가 거부하는 바로 그 종류의 조용한 폭발 반경 확장입니다. 본 글은 이 시스템을 위한 저희의 실무자 디자인(practitioner design)입니다: 브라우저 기록이 실제로 무엇을 담고 있는지, 그것을 검색 가능한 인덱스로 변환하는 파이프라인, 대부분의 데모가 건너뛰는 개인 정보 보호 장치(privacy guardrails), 그리고 그 결과물이 2주 이상 유용하게 유지될지 결정하는 실패 모드에 대해 다룹니다.
1. 북마크가 실패하는 이유와 기록이 다르게 실패하는 이유
북마크는 방치로 인해 실패합니다. 나중에 읽을 의도로 페이지를 저장하고, 한때 의미가 있었던 폴더에 넣지만 다시는 그 폴더를 열지 않습니다. 컬렉션은 커지고, 검색 경로는 퇴화하며, 1년 후 북마크 바는 의도의 박물관이 됩니다. 북마크 내부에서 검색하는 것은 사용자에게 도움이 되지 못합니다. 왜냐하면 그것은 사용자가 작성하지 않은 제목에 대한 부분 문자열(substring) 검색일 뿐이기 때문입니다.
역설적으로, 히스토리는 반대 방향으로 실패한다. 그것은 완전하지만 체계화되지 않았다. 이는 모든 것을 시간 순서대로 기록할 뿐, 중요도, 주제 또는 의도의 개념이 없다. '3주 전에 읽었던 연결 풀 크기(connection pool sizing)에 관한 그 기사'를 찾는다는 것은 타임라인을 스크롤하거나 제목에서 나타나는 키워드를 추측해야 함을 의미한다. 두 도구 모두 잘못된 질문에 답한다. 북마크는 "내가 보관하기로 결정한 것이 무엇인가?"에 답하고, 히스토리는 "무엇을 어떤 순서로 열었나?"에 답할 뿐이다. 이 둘은 당신이 실제로 던지는 질문, 즉 "X에 대해 뭘 읽었고, 어디에 있나?"에는 아무것도 답하지 못한다.
히스토리 위에 구축된 의미론적 인덱스(semantic index)가 그 세 번째 질문에 답한다. 사용자가 자신의 말로 질문하면, 시스템은 제목, URL, 그리고 (선택적으로) 내용 면에서 가장 유사한 페이지들을 반환하며, 이를 클릭하여 검증할 수 있는 링크를 제공한다. 여기서 제품은 로컬 LLM이 아니다. 인덱스 그 자체다. 모델은 단지 검색 계층(retrieval layer)일 뿐이며, 이것을 로컬로 유지하는 것이 전체 시스템을 안전하게 구축할 수 있게 만드는 핵심 요소이다.
2. 브라우저 히스토리가 실제로 담고 있는 것들
무엇이든 설계하기 전에, 원재료를 살펴보자. Chromium 기반 브라우저(Chrome, Edge, Brave 등)는 History라는 이름의 SQLite 데이터베이스 파일에 히스토리를 저장한다. macOS에서는 ~/Library/Application Support/Google/Chrome/Default/History 경로에, Linux에서는 ~/.config/google-chrome/Default/History 경로에 존재한다. 대부분의 작업은 두 개의 테이블에서 이루어진다:
urls: 고유한 URL당 한 행을 가지며, 해당 URL 자체, 페이지 제목, 방문 횟수(visit count), 그리고 마지막 방문 시간을 포함한다.visits: 방문(visit) 건별로 한 행을 가지며, 방문 시간, 참조된 방문 기록(referring visit), 그리고 전환 유형(transition type: typed, link click, reload 등)을 포함한다.
이 스키마에 대한 두 가지 사실이 전체 설계를 좌우합니다. 첫째, 페이지 콘텐츠는 저장되지 않습니다. 기록(History)은 사용자가 어떤 URL을 방문했고 그 제목이 무엇이었는지만 알고 있을 뿐, 페이지가 무슨 내용을 담고 있었는지는 모릅니다. 역사 데이터만으로 '페이지 내용이 무엇에 관한 것이었는지'를 검색한다고 주장하는 모든 시스템은 사실상 제목과 URL만을 검색하고 있는 것입니다. 이는 'X에 대한 기사' 같은 검색에는 종종 충분하지만, 과대 포장해서는 안 됩니다. 만약 콘텐츠 검색을 원한다면, 페이지 자체를 가져오는 별도의 보강(enrichment) 단계가 필요합니다 (섹션 3 참조). 둘째, Chrome은 파일을 실행하는 동안 잠급니다. 절대로 라이브 데이터베이스에 직접 쿼리하지 마십시오. 먼저 복사한 후, 그 사본을 쿼리해야 합니다. 읽기 전용 복사본 역시 안전장치 역할을 합니다: 파이프라인이 브라우저 자체 데이터를 손상시킬 위험은 없습니다.
자신의 데이터에 처음 접근하는 것은 단 하나의 명령으로 가능합니다:
# 먼저 복사하세요 — Chrome은 라이브 파일을 실행 중에 잠급니다.
cp ~/Library/Application\ Support/Google/Chrome/Default/History /tmp/history-copy.db
...
이것을 실행하면 10초 만에 자신의 습관에 대해 무언가를 알게 될 것입니다. 보통은 세 개의 도메인이 브라우징의 절반을 차지한다는 사실입니다. 이 쿼리는 전체 파이프라인의 형태를 결정합니다: 작은 스크립트, 로컬 데이터베이스, 서비스 없음, 계정 없음, 업로드 없음.
나중에 필요한 또 다른 스키마 세부 사항이 있습니다. Chrome 타임스탬프는 유닉스 에포크(Unix epoch)가 아닌 1601년 1월 1일 이후 마이크로초 단위입니다. 모든 데이터 수집(ingest) 스크립트는 이를 변환해야 합니다 ((visit_time / 1000000) - 11644473600을 하면 유닉스 초가 됩니다). 그렇지 않으면 시스템의 '최근' 순위 지정이 조용히 잘못될 것입니다. 이것은 데모에서 배포되어 몇 달 동안 검색 결과를 오염시키는 종류의 지루한 변환 버그입니다. 어떤 출력을 신뢰하기 전에 알려진 방문 기록 하나에 대해 테스트해 보세요.
3. 파이프라인: 데이터 수집(ingest), 정리(clean), 보강(enrich), 임베딩(embed), 검색(retrieve)
이 시스템은 다섯 단계로 구성되어 있으며, 각 단계는 의도적으로 지루합니다. '지루하다'는 것이 핵심입니다: 모든 단계는 읽고, 재실행하고, 디버깅할 수 있는 스크립트이며, 어떤 단계도 호스팅 서비스가 필요하지 않습니다.
인제스트(Ingest) (증분적, 멱등성). 스케줄링된 작업(scheduled job) — Linux에서는 cron, macOS에서는 launchd — 이 History 파일을 복사하고 새로운 방문 기록을 가져옵니다. 워터마크(watermark): 이미 가져온 최대 방문 시간을 유지합니다. 각 실행은 워터마크보다 최신인 방문 기록만 가져오고 URL 기준으로 upsert를 수행하므로 재실행해도 무해합니다. 이는 저희가 [이벤트 기반 파이프라인(event-driven pipelines)] 글에서 설명한 것과 동일한 원칙입니다: 재시도 및 재생은 멱등성(idempotency)과 데드레터 경로(dead-letter path)를 필요로 하며, 그렇지 않으면 충돌한 실행이 다운스트림의 모든 것을 중복합니다. 세 번 가져오기에 실패한 방문 기록은 무한 재시도 루프가 아닌 작은 검토 테이블로 이동합니다.
클린(Clean) (공격적, 무엇보다 먼저). 원본 기록은 데이터셋이 아닙니다; 그것은 잡동사니 서랍입니다. 클리닝(Cleaning)은 임베딩(embedding)보다 항상 먼저 실행되어야 하며, 세 가지 작업을 수행합니다. 여러 소스에서 공유된 동일한 기사가 하나의 URL로 통합되도록 추적 매개변수(utm_*, gclid, fbclid 등)를 제거합니다. 또한 노이즈를 제거합니다: chrome:// 페이지, localhost 개발 서버, 새 탭 페이지, 그리고 정보를 추가하지 않고 행만 추가하는 리디렉션 점프(redirect hops). 그리고 임베딩이 존재하기 전에 민감한 카테고리를 완전히 제거하는 블랙리스트(denylist) (섹션 4)를 적용합니다. 클리닝은 이 시스템의 대부분의 취미용 버전이 실패하는 지점입니다: 그들은 모든 것을 임베딩하고, 나중에 쿼리 시간(query time)에 필터링하려고 시도하는데, 이는 민감한 데이터가 이미 벡터 스토어(vector store)에 놓여 잘못된 쿼리가 이를 표면화하기를 기다리고 있다는 의미입니다.
정보 보강(Enrich) (선택 사항이며 명확히 표기해야 함). 제목과 URL만으로도 충분한 검색 결과를 얻을 수 있는데, 이는 제목이 보통 주제를 담고 있기 때문입니다. 더 많은 정보를 원한다면 인덱싱 시점에 직접 페이지 텍스트를 가져와서 짧게 추출된 요약본을 저장하세요. 절대로 전체 페이지를 저장해서는 안 됩니다. 전체 페이지는 인덱스를 비대하게 만들고, 필요하지 않은 저작권이 있는 텍스트를 다시 포함시키기 때문입니다. 데이터를 가져오는 것(Fetching)에는 명확한 한계가 있습니다. 로그인 뒤에 가려진 페이지는 실패할 것이며, 방문 이후 변경된 페이지는 읽었던 내용과 다를 수 있고, 일부 사이트는 자동화된 데이터 가져오기를 차단할 것입니다. URL별로 데이터 가져오기 상태를 기록해야 합니다. '제목만 이용, 가져오기 실패'라고 표시된 결과가 모호하게 달리하는 것보다 훨씬 신뢰성이 높습니다.
임베딩(Embed) (로컬에서 항상 실행). 임베딩 모델을 자체 기계에서 실행하세요. Ollama와 같은 로컬 런타임에 임베딩 모델을 사용하거나, 이에 상응하는 다른 로컬 환경을 구축하여 URL당 하나의 벡터를 저장해야 합니다. 이 임베딩 단계는 절대 외부 서비스에 맡겨서는 안 되는 과정입니다. 기록(history)에서 파생된 벡터가 호스팅되는 API로 전송되는 순간, 원본 URL이 남아있더라도 프라이버시 보호는 끝납니다. 벡터 저장은 벡터 확장 기능이 있는 SQLite나, 이미 사용하고 있는 모든 로컬 벡터 스토어에 저장할 수 있습니다. 선택 자체보다 중요한 것은 위치, 즉 여러분의 디스크입니다. 임베딩 모델 버전은 데이터베이스에 벡터와 함께 보관하세요. 나중에 모델을 변경하게 된다면, 두 개의 모델에서 나온 벡터를 하나의 인덱스에 섞는 대신 모든 것을 한 번에 재임베딩(re-embed)해야 합니다. 혼합된 모델의 인덱스는 무작위적인 품질 손실처럼 보이는 방식으로 검색 성능이 떨어집니다.
검색(Retrieve) (순위 지정, 출처 포함). 쿼리는 동일한 모델로 임베딩되어 가장 가까운 이웃을 검색하고, 이후 이미 무료로 얻고 있는 신호들—최신성(recency)(지난주 페이지가 보통 작년 페이지보다 같은 쿼리에 대해 더 우수함), 방문 횟수(visit count)(그곳에 돌아온 데는 이유가 있음), 직접 입력 대 클릭 전환(typed-versus-clicked transitions)(클릭을 통해 이동한 URL보다 사용자가 직접 타이핑한 URL이 목적지일 가능성이 높음)—를 이용해 재순위화(rerank)합니다. 답변 형식은 고정되어 있습니다: 출처 신호(제목만 또는 풍부하게 보강됨, 마지막 방문 날짜)가 레이블링된 제목과 링크의 짧은 목록입니다. LLM은 결과당 한 줄 요약을 작성할 수 있습니다. 메모리에서 답을 생성해서는 안 됩니다. 출력에 포함된 모든 주장은 클릭 가능한 링크로 추적되어야 합니다—이는 우리가 자동화(automation)에 일반적으로 적용하는 규칙과 동일합니다: 기계의 요약은 증거 그 자체가 아니라 증거를 가리키는 포인터여야 합니다.
| 단계 (Stage) | 입력 (Input) | 출력 (Output) | 건너뛸 경우 발생하는 문제점 (What breaks if you skip it) |
|---|---|---|---|
| Ingest | 기록 복사본 (History copy) | 새 방문 기록, 워터마크 처리됨 (watermarked) | 전체 재가져오기(re-imports), 중복 데이터, 통제 불가능한 작업 실행 (runaway job) |
| ... |
4. 대부분의 데모에서 건너뛰는 개인정보 보호 가드레일
로컬 시스템이 사적이기 위해서는 기본 설정(defaults) 자체가 사적이어야 합니다. 네 가지 규칙은 좋은 의도보다는 코드 내에서 강제되어야 합니다:
임베딩 전에 금지 목록을 적용해야 하며, 나중에 적용해서는 안 됩니다. 은행 및 결제 도메인, 건강 포털, 웹 메일, 비밀번호 재설정 및 일회용 코드 페이지, 그리고 토큰(token=, session=, auth=)을 포함하는 모든 URL은 임베더(embedder)에 도달하지 않아야 합니다. 금지 목록을 읽을 수 있는 설정 파일의 패턴으로 작성하고, 실행마다 각 패턴이 몇 개의 행을 제거했는지 기록해야 합니다. 만약 이 카운트가 0이라면, 사용자의 브라우징 습관이나 패턴에 무언가 변화가 생겼다는 뜻이므로 이를 인지해야 합니다.
# 금지 목록 (Denylist): 임베딩이 존재하기 전에 정리 시점에 적용됨.
DENY_PATTERNS = [
r"bank|payment|checkout", # 금융 흐름
...
하나의 기기, 하나의 인덱스(index), 그렇지 않다고 결정할 때까지는 그러하다. 업무용 노트북과 개인용 노트북에서 수집된 기록은 민감도가 다른 별개의 데이터셋입니다. 클라우드 드라이브를 통해 인덱스를 여러 기기에 동기화하는 행위는 로컬 시스템을 조용히 분산형(distributed) 시스템으로 만듭니다. 만약 다중 장치 검색이 필요하다면, 암호화된 데이터베이스 파일을 직접 동기화하고 스스로 그렇게 했다는 것을 알아야 합니다. 어떤 도구가 부수적인 효과로 이를 동기화하도록 내버려 두지 마십시오.
실제로 삭제하는 삭제 경로. 삭제 기능 없는 북마크 브레인은 누군가 당신의 기기를 사용하게 되는 순간부터 위험 요소(liability)가 됩니다. URL을 삭제할 때는 해당 행(row), 벡터(vector), 그리고 풍부화된 텍스트를 하나의 작업으로 제거해야 하며, 이 작업은 문서(documentation)를 읽지 않고도 실행할 수 있는 단일 명령이어야 합니다. 신뢰하기 전에 반드시 하나의 URL로 테스트하여 행이 사라졌는지 확인하십시오.
두 번째 청중을 허용하지 않는다. 인덱스는 당신에게만 응답해야 합니다. 공유 비서(shared assistant), 팀 도구, 또는 '도움이 되는' 브라우저 확장 프로그램에 이를 겨누지 마십시오. 두 번째 소비자가 이 인덱스를 읽는 순간, 모든 블랙리스트 결정은 그 소비자 접근 권한을 위해 재감사되어야 하며, 아무도 그런 감사를 수행하지 않습니다.
5. 2주 차 생존 여부를 결정하는 실패 모드
이 시스템의 대부분 버전은 첫날 작동하지만 열네 번째 날에까지 성능이 저하됩니다. 이러한 성능 저하는 예측 가능한 원인을 가지며, 각각 값싼 해결책을 가지고 있습니다.
둠스크롤(Doomscroll) 오염. 기록은 의도(intention)가 아닌 주의력(attention)을 기록합니다. 일주일 동안 아무것도 하지 않고 스크롤만 하는 것은 실제로 필요한 다섯 페이지보다 훨씬 많은 방문 횟수를 생성할 수 있는데, 이는 인덱스가 무한히 빠지는 구멍(rabbit hole)과 연구를 구분하지 못하기 때문입니다. 해결책은 필터링이 아닌 가중치 부여(weighting)입니다. 몇 초 미만의 체류 시간(dwell time)을 가진 방문이나 다시 방문하지 않은 페이지는 사용자가 돌아왔거나 직접 입력한 페이지보다 낮은 순위를 차지해야 합니다. 만약 데이터 수집 과정에서 체류 시간, 방문 횟수, 그리고 직접 타이핑하여 이동한 전환 기록을 포착하지 못한다면, 이 두 가지를 모두 사용 가능한 대리 지표(proxy)로 활용하십시오.
링크 부패(Link rot) 및 표류(drift). 페이지는 이동하고, 유료 장벽(paywall)이 생기며, 읽은 후 내용이 변경됩니다. 월별 데드 링크 스윕(dead-link sweep)을 수행하여 저장된 URL을 다시 가져오고, 404 오류가 나거나 홈페이지로 리디렉션되는 것을 표시하면 인덱스를 정확하게 유지할 수 있습니다. 단순히 삭제하지 말고 표시하세요. 읽었던 페이지의 데드 링크는 여전히 유용한 기억이지만, 기사를 기대하며 클릭하지 않도록 '데드'로 라벨링되어야 합니다.
제목 모호성(Title ambiguity). "대시보드(Dashboard)", "홈(Home)", 그리고 "제목 없음(Untitled)"은 수많은 다른 페이지의 제목입니다. 오직 제목만으로 하는 검색은 바로 이런 경우에 실패합니다. 해결책은 섹션 3의 풍부화(enrichment) 단계와 폴백(fallback) 메커니즘을 추가하는 것입니다. 즉, 결과물의 제목이 일반적일 경우, 컨텍스트를 통해 모호성을 해소할 수 있도록 도메인과 방문 날짜를 눈에 잘 띄게 보여주는 것입니다. 식별할 수 없는 결과는 잘못되었다는 것을 발견하기 위해 클릭 비용이 들기 때문에, 아무 결과가 없는 것보다 더 나쁩니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기