개인 프로젝트

MonoStart - 커스터마이징 가능한 새 탭 Chrome 확장

링크, 타이머, 리마인더를 오직 기기에만 저장하는, 새 탭마다 개인 대시보드로 바꿔주는 프라이버시 우선 Chrome 확장.

MonoStart는 기본 브라우저 새 탭 페이지를 빠르고 커스터마이징 가능한 개인 대시보드로 바꾼다. 북마크, 할 일 목록, 빠른 메모, 집중 타이머를 구성 가능한 작업 공간으로 정리한다. Manifest V3 Chrome 확장으로 만들어졌으며, 모든 것을 기기에 로컬로 저장한다: 계정도 없고, 서버 데이터베이스도 없고, 분석 도구도 없다.

MonoStart - 커스터마이징 가능한 새 탭 Chrome 확장

내가 만든 것

  • 커스터마이징 가능한 대시보드: 위젯(빠른 링크, 분류된 폴더, 메모, 할 일 목록, 카운트다운 타이머, 리마인더, 음성 검색, Google 렌즈 시각 검색)을 어떤 워크플로우에도 맞게 배치할 수 있는, 중첩된 위젯 그룹을 포함한 드래그 앤 드롭 그리드.
  • 빠른 접근 레이아웃: 헤더의 고정 링크와 그룹화된 폴더가 깊은 북마크 메뉴를 대체해, 자주 방문하는 사이트를 한 클릭 거리에 둔다.
  • 테마 커스터마이징: Chrome 자체의 "Customize Chrome" 팔레트에서 역설계한 15가지 내장 컬러 테마, 커스텀 헥스 피커, 라이트/다크 모드 지원, 그라디언트 또는 배경화면 옵션.
  • 생산성 도구: 로컬 오디오 알림과 데스크톱 알림이 있는 내장 카운트다운 타이머 및 예약된 리마인더.
  • 로컬 우선 저장: 메모, 할 일, 레이아웃, 이미지는 chrome.storage.local과 IndexedDB에 기기에 저장된다. 사용자 계정도 서버 데이터베이스도 없다. 유일한 네트워크 호출은 검색 자동완성을 위한 Google의 공개 제안 API로, Chrome 자체 주소창이 쓰는 것과 동일하다.

왜 중요했는가

새 탭 페이지는 하루에도 수십 번 열린다. MonoStart는 그 빈 공간을 즉각적이고 로컬 우선의 생산성 대시보드로 바꾸며, 프라이버시를 존중한다: 데이터는 절대 기기를 벗어나지 않는다.

이것이 존재하는 이유

나는 하루에 약 200번 새 탭을 연다. 그리고 몇 년 동안 그것은 아무것도 돌려주지 않았다: 검색창과 Google이 추측한 사이트들의 그리드. 진짜 문제는 빈 페이지가 아니라, 그것에 대처하기 위해 내가 했던 모든 것이었다. URL을 다시 입력하지 않으려고 세 개의 Chrome 프로필에 걸쳐 40개 이상의 탭을 열어둔 채로 두었는데, 각각이 Chrome이 계속 살려두는 살아있는 프로세스였다. 나는 내 작업을 정리하고 있던 것이 아니라, 페이지가 아니라 포인터를 붙잡아두기 위해 RAM과 팬 비용을 지불하고 있던 것이었다. 내가 시도해본 모든 기존 새 탭 확장은 그 빈 페이지를 더 나쁜 것과 맞바꿨다: 손에 익은 습관을 깨는 낯선 UI, 계정과 클라우드 동기화, 스폰서 링크, 텔레메트리. 나는 유일한 기여자이며, 2026년 6월 이후 222건의 커밋이 있고, 지금도 활발히 유지보수되고 있으며, 내 브라우저를 고치기 위해 먼저 만들어졌고 그다음에야 공개 확장으로 배포되었다.

단 하나의 규칙: 앱이 아니라 Chrome처럼 보일 것

이 코드베이스의 모든 결정은 하나의 제약에 답한다: Chrome이 끝나고 대시보드가 시작되는 지점을 구분할 수 없어야 한다. 그래서 진짜 Google 검색창이 진짜 Chrome 기록 제안과 함께 그대로 남아 있으며, 자체 기록 인덱스가 필요한 커스텀 검색 UI를 만들지 않았다. 온보딩 흐름도 없고, 로그인 화면도 없고, 계정 시스템도 없는 이유도 여기에 있다: 새 탭을 여는 것은 아무것도 설치되지 않은 것처럼 느껴져야 한다. 페이지를 별개의 앱처럼 느껴지게 만드는 기능은, 아무리 유용해도 출시되지 않았다.

오디오를 재생할 수 없는 서비스 워커 안에서의 오디오

Manifest V3 백그라운드 스크립트는 서비스 워커이며, 서비스 워커는 오디오를 안정적으로 직접 재생할 수 없다. Chrome은 메모리를 회수하고 싶을 때마다 서비스 워커를 중단시키고 종료하는데, 재생 도중이라도 예외가 아니다. 리마인더에는 여전히 실제로 울리는 소리가 필요하다. 이 해결책은 chrome.alarms를 사용해 리마인더를 예약하는데, 알람은 종료된 서비스 워커에서도 살아남아 다시 깨울 수 있기 때문이며, 재생은 서비스 워커 자체가 아니라 chrome.offscreen 문서를 통해 이루어지는데, 이는 Chrome이 바로 이런 작업을 위해 살려두는 숨겨진 페이지다. MV3에는 더 이상 지속되는 백그라운드 페이지가 존재하지 않기 때문에, 대부분의 MV3 확장이 결국 부딪히는 것과 같은 종류의 우회책이다.

Chrome 자체에서 역설계한 테마

임의의 컬러 팔레트를 만드는 대신, MonoStart의 15개 내장 테마는 Chrome 자체의 "Customize Chrome" 시드 컬러에서 추출되었으며, 그 추출 과정은 저장소 안에 단계별로 문서화되어 있다. 초기 버전의 추출에는 색상 편차(hue-shift) 버그가 있었다. 그 수정은 v1.1.1에서 출시되었고, 이전 수식으로 이미 테마를 선택했던 사용자를 위한 마이그레이션 경로도 함께 제공되어, 기존 사용자가 자신이 고른 색과 다른 색으로 깨어나지 않도록 했다.

보안: 살균 라이브러리만 믿지 않고 임베드 살균하기

이미지와 임베드 위젯은 임의의 HTML을 받아들이며, 렌더링 전에 DOMPurify를 거쳐 살균된다. DOMPurify만으로는 충분하지 않았다: 이 라이브러리는 srcdoc 속성을 제거하지 않으며, srcdoc가 설정된 iframe은 확장 자체의 출처를 상속하는데, 이는 확장의 CSP가 느슨해지면 실제 XSS 경로가 된다. srcdoc는 DOMPurify가 자체적으로 허용하는 것과 무관하게 살균기에서 명시적으로 제거되며, 하나의 라이브러리를 완전히 신뢰하는 대신 두 번째 독립적인 계층을 둔 것이다.

데이터 모델과 규모

  • 텍스트 데이터(메모, 할 일, 레이아웃, 리마인더, 설정)는 chrome.storage.local에 저장된다.
  • 배경화면과 배경 이미지는 별도의 IndexedDB 데이터베이스에 저장되며, 삭제되거나 교체된 이미지가 조용히 쌓이지 않도록 고아 참조 가비지 컬렉션이 적용된다.
  • chrome.storage.local의 약 10MB 한도는 unlimitedStorage 권한으로 해제되는데, 이미지가 그 할당량에서 완전히 벗어난 뒤 필요해졌다.
  • 216개의 테스트(Vitest, jsdom, fake-indexeddb)가 25개 파일에 걸쳐 이 저장 계층 자체를 직접 커버하며, 단지 그 위의 UI만이 아니다.

로컬 전용 저장은 간과가 아니라 의도적인 트레이드오프다: 이는 기기나 프로필 간 동기화가 없다는 뜻이지만, 동시에 세 개의 Chrome 프로필이 공유 계정 시스템을 만들고 지킬 필요 없이 각자 독립적이고 격리된 대시보드를 거저 갖게 되는 바로 그 메커니즘이기도 하다. 검색창 자동완성은 전체 앱에서 유일한 네트워크 호출이며, Chrome 자체 주소창과 같은 방식으로 Google의 공개 제안 API를 사용한다. 사용자가 실제로 만드는 모든 것은 로컬에 남는다.

Build vs. Buy

커스텀 드래그 앤 드롭 엔진 없음: 충돌 감지와 크기 조절 핸들을 처음부터 다시 구현하는 대신 react-grid-layout이 중첩된 그룹을 포함한 그리드를 처리한다. 커스텀 컴포넌트 라이브러리 없음: shadcn/ui의 방식대로 Radix 기반 컴포넌트를 직접 복사해 들여오면(불투명한 UI 패키지를 설치하는 대신), 모든 컴포넌트의 코드와 그로부터의 어떤 일탈도 눈에 보이고 수정 가능하게 유지된다. 커스텀 로컬라이제이션 파이프라인 없음: 직접 만든 문자열 조회 대신 react-i18next와 계속 늘어나는 로케일 파일(50개 이상 언어).

Stack

React 19, Vite, TypeScript, Tailwind CSS, Radix UI 프리미티브 위의 shadcn/ui, 대시보드 그리드를 위한 react-grid-layout, 로컬라이제이션을 위한 react-i18next, 임베드 살균을 위한 DOMPurify, 테스트를 위한 Vitest, Chrome의 Manifest V3 확장 플랫폼(chrome.alarms, chrome.offscreen, chrome.storage, IndexedDB), PolyForm Noncommercial 1.0.0 라이선스.