한국어 · English
지금 보고 있는 사이트가 내 컴퓨터를 얼마나 쓰고 있는지, 우측 상단 패널에서 실시간으로 보여주는 Chrome 확장.
CPU(메인 스레드 점유), 메모리(JS 힙), 네트워크 수신·송신 — 네 가지를 1초 간격으로 샘플링해 그래프로 그립니다. 브라우저 작업 관리자를 열지 않아도, 어떤 탭이 팬을 돌리고 있는지 바로 보입니다.
다크 · 라이트 (실제 측정 화면. 스모크 테스트가 헤드리스 Chrome에서 캡처한 것)
확장을 설치하기 전에 디자인만 보고 싶다면
design/panel-mockup.html을 브라우저로 열면 됩니다. 가짜 데이터로 도는 목업이라 시스템/라이트/다크·접힘·표 보기를 전부 눌러 볼 수 있습니다.
| 실시간 그래프 4종 | CPU 점유, JS 힙, 네트워크 수신·송신. 네트워크는 단위가 같으므로 한 차트에 두 계열로 겹쳐 그립니다 (축은 항상 하나) |
| 시간 범위 3종 | 1분(1초 간격) · 5분(5초 평균) · 1시간(1분 평균). 세 버퍼가 동시에 쌓이므로 범위를 바꿔도 이력이 유지됩니다 |
| 테마 3종 | 시스템 / 라이트 / 다크. 시스템은 OS 설정을 실시간으로 따라갑니다 |
| 언어 2종 | 한국어 / English, 그리고 브라우저 언어를 따르는 시스템. 바꾸면 열려 있는 패널이 그 자리에서 다시 그려집니다 — 새로고침이 필요 없고 측정 이력도 끊기지 않습니다 |
| 크로스헤어 툴팁 | 그래프 위를 지나가면 세 차트가 같은 시점을 가리킵니다. 키보드 포커스 + 방향키로도 같은 값을 읽을 수 있습니다 |
| 표 보기 | 그래프에 그려진 모든 샘플(현재 범위 기준 최대 60개)과 최소·평균·최고를 표로. 색을 구분하기 어려운 환경에서도 모든 값에 접근할 수 있습니다 (예시) |
| CSV / JSON 내보내기 | 푸터 ⤓. CSV는 현재 범위의 샘플만 담은 순수한 표, JSON은 세 범위 전부와 누적 합계·측정 조건까지. 저장 전에 미리보기가 나옵니다 |
| 접힌 상태 | CPU 12% · 148MB · 34.2KB/s 한 줄 필로 축소. 페이지를 거의 가리지 않습니다 |
| 드래그 이동 | 헤더를 잡아 옮기면 위치가 저장돼 다음 페이지에서도 유지됩니다 |
| 끄면 진짜 끕니다 | 패널을 닫으면 옵저버·타이머를 해제하고, 측정 중인 탭이 하나도 없으면 webRequest 리스너까지 전부 떼어냅니다 |
아직 웹스토어에 올리지 않았으므로 개발자 모드로 로드합니다.
git clone <이 저장소> webWorks
cd webWorks
node scripts/make-icons.mjs # 아이콘 PNG 생성 (저장소에 이미 있으면 생략 가능)- Chrome에서
chrome://extensions열기 - 우측 상단 개발자 모드 켜기
- 압축해제된 확장 프로그램을 로드 → 이 폴더 선택
Chrome 116 이상이 필요합니다 (adoptedStyleSheets, deliveryType, MV3 서비스 워커).
툴바의 webWorks 아이콘을 누르면 팝업이 열립니다.
| 조작 | 하는 일 |
|---|---|
| 팝업 → 이 탭에서 측정 | 현재 탭에 패널을 띄우고 측정을 시작합니다 |
| Alt+Shift+W | 현재 탭에서 패널 켜기/끄기 |
| 팝업 → 언어 | 시스템 / 한국어 / English. 열려 있는 패널에 즉시 반영됩니다 |
| 팝업 ✕ | 팝업 닫기 (Esc·바깥 클릭으로도 닫힙니다) |
| 팝업 → 모든 사이트에서 자동 시작 | 새 페이지가 로드될 때마다 알아서 켭니다 |
| 패널 ⏸ | 화면을 얼립니다. 수집은 계속되므로 재개하면 이력이 이어집니다 |
| 패널 ⌄ | 한 줄 필로 접기 |
| 패널 ✕ | 이 탭의 측정 종료 |
| 패널 ⤓ | CSV / JSON 내보내기. Esc·바깥 클릭으로도 닫힙니다 |
| 그래프 위 포인터 / ←→ | 해당 시점의 값 읽기 |
측정은 켠 순간부터 셉니다. 페이지를 이동하면 카운터가 초기화됩니다.
두 형식이 담는 것이 다릅니다.
| 담는 것 | 왜 | |
|---|---|---|
| CSV | 현재 범위의 샘플만 — timestamp,cpu_pct,mem_mb,in_kbs,out_kbs |
헤더 한 줄 + 데이터 행뿐인 순수한 표라야 스프레드시트·pandas·R에 그대로 들어갑니다. 모양이 다른 누적 합계를 같은 파일에 섞지 않습니다. 앞에 BOM을 붙여 Excel이 UTF-8로 엽니다 |
| JSON | 전부 — 메타데이터·단위·세 범위의 샘플·누적 합계 | 중첩이 되므로 잘라낼 이유가 없습니다 |
시각은 샘플에 기록된 값입니다. 마지막 샘플에서 간격을 역산하지 않습니다 — 백그라운드 탭에서 버려진 구간이 있으면 실제와 어긋나기 때문입니다. units 항목에 "송신은 과대 추정"이라는 사실까지 적어 내보냅니다.
저장은 Blob + <a download>로 합니다. chrome.downloads를 쓰면 권한이 하나 더 붙는데, 그만한 값어치가 없습니다.
무엇을 어떻게 세는지, 그리고 무엇을 못 세는지를 분명히 적어 둡니다. 브라우저 확장이 접근할 수 있는 범위에는 실제 한계가 있고, 그 한계를 숨기는 계측기는 믿을 수 없습니다.
| 지표 | 데이터 소스 | 실제로 세는 값 | 한계 |
|---|---|---|---|
| CPU | PerformanceObserver('longtask') + 타이머 지연 프로브(100ms) |
두 신호 중 큰 값을 샘플 구간으로 나눈 메인 스레드 점유율(%) | 프로세스 CPU 사용률이 아닙니다. Web Worker·GPU·컴포지터·다른 프레임의 부하는 빠집니다. longtask는 50ms 이상만 잡히므로, 짧은 작업이 촘촘한 경우는 타이머 지연 쪽이 실제에 가깝습니다 |
| 메모리 | performance.memory.usedJSHeapSize (없으면 performance.measureUserAgentSpecificMemory()) |
렌더러의 JS 힙 사용량(MB) | DOM 노드·이미지·GPU 텍스처는 포함되지 않습니다. 같은 프로세스를 공유하는 다른 프레임의 힙이 섞이며, 값은 100KB 단위로 양자화됩니다 |
| 네트워크 수신 | Resource Timing transferSize (+ chrome.webRequest의 Content-Length 보충) |
헤더까지 포함한 실제 수신 바이트 | 캐시 적중은 0으로 셉니다(맞는 동작). Timing-Allow-Origin이 없는 교차 출처 응답은 transferSize가 가려지므로 서비스 워커가 본 Content-Length로 보충하는데, 이건 압축 후 본문 크기 추정치입니다 |
| 네트워크 송신 | chrome.webRequest — 요청 라인 + 요청 헤더 + requestBody |
업로드 바이트 근사치 | HTTP/2·3의 헤더 압축(HPACK/QPACK)과 TLS·TCP 오버헤드를 반영하지 않아 실제보다 크게 나옵니다. Resource Timing에는 업로드 크기가 아예 없어서 이 방법이 유일한 창구입니다 |
| 요청 수 / 실패 / 캐시 | chrome.webRequest onCompleted · onErrorOccurred |
탭에 귀속된 요청 건수 | 페이지의 Service Worker가 낸 요청은 tabId가 없어 빠질 수 있습니다 |
flowchart LR
subgraph PAGE["측정 대상 페이지 (탭 최상위 프레임)"]
RT["PerformanceObserver<br/>resource · navigation"]
LT["PerformanceObserver<br/>longtask"]
LAG["타이머 지연 프로브<br/>100ms"]
MEM["performance.memory"]
end
subgraph CS["콘텐츠 스크립트 · isolated world"]
MET["metrics.js<br/>1초 샘플러 · 링 버퍼 3개"]
PAN["panel.js<br/>Shadow DOM 패널"]
CH["charts.js<br/>SVG 렌더러"]
end
SW["service-worker.js<br/>chrome.webRequest 탭별 집계"]
POP["popup<br/>on/off · 테마 · 기본 범위"]
RT --> MET
LT --> MET
LAG --> MET
MEM --> MET
MET <-->|"1초 틱 — 송신 바이트 · 요청 수<br/>opaque URL 바이트 보충"| SW
MET -->|onSample| PAN
PAN --> CH
POP -->|ww:setEnabled| SW
SW -->|"탭 메시지"| PAN
프로세스 단위의 정확한 CPU·메모리를 주는 chrome.processes API는 확장에서 쓸 수 없습니다. 남은 선택지는 두 개였습니다.
chrome.debugger(CDPPerformance.getMetrics) — 정확하지만 탭마다 "디버깅 중" 배너가 붙고 권한이 무거워집니다.- 웹 표준 API — 정확도를 조금 내주는 대신, 조용하고 가볍고 언제든 껐다 켤 수 있습니다.
webWorks는 후자를 골랐습니다. 그래서 CPU 지표의 이름이 그냥 "CPU"가 아니라 "CPU · 메인 스레드" 이고, 패널 안에서 그 라벨에 마우스를 올리면 추정 방식이 그대로 나옵니다.
색약(1형·2형·3형) 시뮬레이션 검증을 통과한 조합입니다. 라이트와 다크는 자동 반전이 아니라 각 표면에 맞춰 따로 고른 단계입니다.
| 슬롯 | 지표 | 라이트 | 다크 |
|---|---|---|---|
| 1 | CPU | #2a78d6 |
#3987e5 |
| 2 | 메모리 | #eb6834 |
#d95926 |
| 3 | 네트워크 수신 | #1baf7a |
#199e70 |
| 4 | 네트워크 송신 | #eda100 |
#c98500 |
- 인접쌍 최악 CVD ΔE 9.1(라이트) / 8.4(다크) — 기준 8 이상
- 일반 시야 최악 ΔE 22.9 / 19.8 — 기준 15 이상
- 라이트 모드의 청록·노랑은 표면 대비가 3:1 미만입니다. 그래서 범례에 현재값을 직접 찍고 표 보기를 항상 제공합니다 — 색만으로 읽어야 하는 상황을 만들지 않습니다.
- 상태색(정상
#0ca30c/ 주의#fab219/ 과부하#d03b3b)은 CPU 배지 전용으로 예약되어 있고, 계열 색으로 재사용하지 않습니다. 배지는 언제나 아이콘 + 글자를 함께 달고 나옵니다.
선 2px round · 면 채움은 계열 색 10% · 끝점 마커 r4에 표면색 2px 링 · 그리드와 축은 표면에서 한 단계 벗어난 회색 실선 hairline. 데이터만 진하고, 나머지는 전부 물러납니다.
:host { /* 라이트 — 기본값 */ }
@media (prefers-color-scheme: dark) {
:host(:not([data-theme="light"])) { /* 시스템이 다크일 때 */ }
}
:host([data-theme="dark"]) { /* 사용자가 다크를 고정했을 때 */ }시스템을 고르면 data-theme 속성을 지워 CSS가 OS 설정을 직접 따르게 하고, 라이트/다크는 OS와 반대여도 선택이 이깁니다. 팝업도 같은 규칙을 씁니다.
문자열은 chrome.i18n이 아니라 common/i18n.js의 자체 표를 씁니다. chrome.i18n은 브라우저 UI 언어를 따르고 런타임에 바꿀 수 없어서, 팝업의 언어 선택을 구현할 수 없기 때문입니다. 테마와 똑같이 시스템 / 한국어 / English 세 값을 storage.sync에 두고, 패널은 storage.onChanged로 받아 마크업을 다시 만듭니다.
예외는 매니페스트에 박히는 문자열입니다 — 확장 설명, 툴바 툴팁, 단축키 설명은 크롬이 직접 그리므로 _locales/와 __MSG_*__를 쓸 수밖에 없고, 브라우저 언어를 따릅니다. 팝업에서 언어를 바꿔도 이 세 개는 바뀌지 않습니다.
숫자도 함께 따라갑니다 — toLocaleString에 넘기는 로케일이 ko-KR/en-US로 갈리므로 자릿수 구분자와 소수점이 언어에 맞습니다.
패널은 닫힌 Shadow DOM 안에 있습니다. 스타일은 panel.css를 fetch해 CSSStyleSheet.replaceSync()로 adopt합니다 — CSSOM 삽입이라 페이지의 style-src CSP와 무관하고, 페이지 CSS는 shadow 경계를 넘어오지 못합니다. connect-src가 확장 리소스 fetch까지 막는 사이트에서는 서비스 워커가 CSS 텍스트를 대신 전달합니다. 상속으로 새어 들어오는 폰트·색은 :host { all: initial }로 끊습니다.
| 권한 | 왜 필요한가 |
|---|---|
webRequest |
업로드 바이트와 요청 수를 세는 유일한 창구입니다. 요청을 차단하거나 수정하지 않습니다 (MV3에서는 관측만 가능) |
storage |
테마·언어·기본 범위·자동 시작·패널 위치 저장 |
scripting |
확장을 설치·리로드하기 전부터 열려 있던 탭에 계측 스크립트를 그 자리에서 넣기 위해 씁니다. 넣는 파일은 패키지 안 네 개로 고정이고, 원격 코드는 실행하지 않습니다 |
host_permissions: <all_urls> |
어느 사이트에서든 측정하려면 필요합니다. 사용할 사이트가 정해져 있다면 이 항목을 원하는 도메인으로 좁혀 쓰는 걸 권합니다 |
밖으로 나가는 데이터는 없습니다. 네트워크 요청도, 원격 전송도, 애널리틱스도 없습니다. 모든 수치는 탭 메모리에만 있고 패널을 닫으면 사라집니다. 저장하는 것은 위 표의 설정값뿐입니다.
webWorks/
├─ manifest.json MV3 매니페스트
├─ _locales/{ko,en}/ 매니페스트 문자열만 (설명 · 툴팁 · 단축키 설명)
├─ common/
│ └─ i18n.js UI 문자열 표 · 언어 해석 · 로케일 숫자 포맷
├─ background/
│ └─ service-worker.js 탭별 webRequest 집계 · on/off 중계 · 배지
├─ content/
│ ├─ metrics.js 1초 샘플러 · 링 버퍼 · 서비스 워커 동기화
│ ├─ charts.js SVG 렌더러 (마크 규격이 여기 다 들어 있음)
│ ├─ export.js CSV / JSON 직렬화 · 파일명 · 저장
│ ├─ panel.js Shadow DOM 패널 · 상호작용 · 테마/언어 적용
│ └─ panel.css 패널 토큰과 스타일 (테마 3종)
├─ popup/
│ ├─ popup.html / .css / .js on/off · 언어 · 테마 · 기본 범위 · 자동 시작
├─ icons/ 16 / 32 / 48 / 128 PNG (생성물)
├─ scripts/
│ ├─ lib/harness.mjs 테스트 공용 — Chrome 실행 · CDP · shadow DOM 순회
│ ├─ make-icons.mjs zlib만으로 PNG를 직접 인코딩하는 아이콘 생성기
│ ├─ smoke-test.mjs 헤드리스 Chrome에 실제로 로드해 보는 종단 테스트
│ ├─ test-injection.mjs 확장보다 먼저 열려 있던 탭에 주입되는지
│ ├─ test-table.mjs 표 보기가 버퍼 전체를 싣는지
│ ├─ test-i18n.mjs 한/영 전환
│ ├─ test-export.mjs CSV / JSON 내보내기
│ └─ test-all.mjs 전부 차례로 실행
└─ design/
├─ panel-mockup.html 가짜 데이터로 도는 디자인 목업
├─ roadmap-mockup.html 「앞으로」 5개 항목이 들어간 목업 (아직 구현 전)
└─ screenshots/ 스모크 테스트가 캡처한 실제 화면
node scripts/test-all.mjs # 테스트 전부 차례로
node scripts/smoke-test.mjs # 종단 확인 + 스크린샷만
node scripts/make-icons.mjs # 아이콘 재생성 (도형은 스크립트 상단 상수)
node --check content/metrics.js # 문법 검사 (빌드 과정이 없습니다)
start design/panel-mockup.html # 디자인만 빠르게 확인빌드 단계도 번들러도 없습니다. 파일을 고치고 chrome://extensions에서 새로고침하면 끝입니다. 콘텐츠 스크립트를 고쳤다면 대상 페이지도 새로고침해야 합니다 — 이미 주입된 구 버전이 남아 있어서, 다시 켜는 것만으로는 새 코드가 올라가지 않습니다.
모두 진짜 Chrome을 헤드리스로 띄우고 CDP로 조작합니다. 목 객체가 없고, 외부 네트워크도 쓰지 않습니다. 공용 부분(Chrome 실행·CDP 클라이언트·닫힌 shadow root 순회)은 scripts/lib/harness.mjs에 있습니다.
| 파일 | 무엇을 지키는가 |
|---|---|
smoke-test.mjs |
종단. 샘플이 쌓이는지, 수신·송신·요청 수가 채워지는지, 패널이 Shadow DOM에 마운트됐는지, 테마 전환이 열려 있는 패널에 반영되는지. 다크·라이트·표 보기 스크린샷을 남깁니다 |
test-injection.mjs |
페이지를 먼저 열고 확장을 나중에 로드합니다. 선언형 content_scripts는 소급 주입되지 않으므로, chrome.scripting 폴백이 없으면 여기서 실패합니다. 중복 주입해도 패널이 하나인지도 함께 봅니다 |
test-table.mjs |
표가 버퍼 전체를 싣는지. 25초를 모아 옛 상한(14행)을 넘기고, 요약의 '최고'가 표 안에 실제로 있는지까지 확인합니다 |
test-i18n.mjs |
한/영 전환. 표 보기를 켜 둔 채 언어를 바꿔 상태가 살아남는지, 마크업을 다시 만든 뒤에도 클릭이 붙어 있는지, 숫자 로케일이 함께 바뀌는지 |
test-export.mjs |
CSV가 실제로 파싱 가능한 표인지(헤더·열 수·시간순), 시각이 역산이 아니라 샘플에 기록된 값인지, JSON에 세 범위와 합계가 다 들어 있는지. 다운로드 경로를 지정해 파일이 진짜 떨어지는 것까지 확인합니다 |
smoke-test.mjs가 10초만 돌고 확장을 항상 먼저 로드하는 탓에 놓치는 회귀가 있어서, 나머지를 따로 두었습니다.
Chrome 151부터
--load-extension커맨드라인 플래그가 무시됩니다. 그래서 테스트는--enable-unsafe-extension-debugging과 CDPExtensions.loadUnpacked를 씁니다.
콘텐츠 스크립트들은 같은 isolated world를 공유하며 globalThis.WW 한 곳에서만 만납니다 (WW.I18n → WW.Metrics → WW.Charts → WW.Panel, 이 순서로 주입됩니다). 각 파일은 IIFE로 감싸 최상위 이름을 만들지 않고, 선두의 재진입 가드 덕분에 두 번 주입돼도 안전합니다. common/i18n.js는 팝업에서도 같은 파일을 그대로 불러 씁니다.
- 백그라운드 탭은 그래프가 멈춥니다. Chrome이 보이지 않는 탭의 타이머를 강하게 조이기 때문입니다. 샘플 간격이 3배 이상 벌어지면 그 구간을 버립니다 — 쓰레기 값으로 그래프를 채우는 것보다 낫습니다.
- 송신 바이트는 과대 추정입니다. HTTP/2 헤더 압축을 계산에 넣지 못합니다. 절대량보다 추이를 보는 용도로 쓰세요.
- iframe은 포함되지 않습니다. 최상위 프레임만 계측합니다.
- 서비스 워커가 잠들면 그 순간의 네트워크 카운트가 어긋날 수 있습니다. 패널이 열려 있는 동안은 1초 틱이 워커를 깨워 두므로 실사용에서는 드뭅니다.
chrome://, Chrome 웹스토어, 다른 확장 페이지에서는 동작하지 않습니다 (브라우저가 스크립트 주입을 금지).
- CSV / JSON 내보내기
- 정확한 프로세스 CPU·메모리를 원하는 사용자를 위한
chrome.debugger선택 모드 - 사이트별 이력 저장과 세션 비교
- 임계값 알림 (CPU가 N초 이상 X%를 넘으면 알림)
- 요청 상위 도메인 분해 (무엇이 대역폭을 먹는지)
남은 네 항목의 화면 설계는 design/roadmap-mockup.html에 목업으로 만들어 두었습니다 — 도메인 분해는 패널의 세 번째 뷰로, 이력·세션 비교는 새 표면(대시보드)으로 들어갑니다. 내보내기는 그 목업대로 푸터 ⤓에 붙었고, 여러 세션을 한꺼번에 내보내는 것만 대시보드 몫으로 남았습니다.
MIT

