탭 2~3개만 쓰는 나를 위한 저메모리 브라우저 만들기
크롬이 왜 그렇게 무거운지 파고들다 WebView2로 개인용 미니 브라우저를 만들었습니다. 탭 서스펜드, 메모리 모니터, EasyList 광고 차단의 실제 구현 기록입니다.
목차
크롬 탭을 두세 개밖에 안 켜는데도 메모리가 800MB씩 나가는 게 늘 거슬렸습니다. 확장 프로그램, 동기화, 추천 피드, 백그라운드 서비스 — 제가 안 쓰는 기능들이 메모리를 먹고 있었죠. 그래서 "내가 실제로 쓰는 것만 남긴" 개인용 미니 브라우저를 직접 만들어봤습니다.
목표는 처음부터 좁게 잡았습니다. 크롬을 모든 면에서 대체하는 범용 브라우저가 아니라, 탭 2~3개 환경에서 메모리·CPU 낭비를 줄이는 개인 도구. 브라우저 엔진을 새로 만들 생각은 당연히 없었습니다.
이 글에도 "데모 열어보기" 버튼은 없습니다. Windows 네이티브 앱(C# + WPF + WebView2)이라 웹사이트로 이식할 수 있는 물건이 아니거든요. 대신 만들면서 알게 된 것들을 정리합니다.
엔진은 빌려 쓰고, 정책만 내가 짠다
브라우저의 진짜 무게는 렌더링 엔진(Chromium)과 웹페이지 자체가 만듭니다. 그건 내가 C#으로 다시 짤 수 있는 게 아니고, 그럴 필요도 없습니다. Windows에는 WebView2라는, 엣지의 Chromium 엔진을 앱에 끼워 쓰는 컴포넌트가 있습니다.
그래서 구조를 이렇게 나눴습니다.
- 엔진: WebView2(Chromium) — 빌려 쓴다
- 껍데기 + 정책: C#/WPF로 직접 — 창, 탭바(최대 3개), 주소창, 상태바, 그리고 "무엇을 차단하고 언제 탭을 재울지"
핵심 통찰은 "메모리 대부분은 내 앱이 아니라 렌더러가 쓴다"는 것이었습니다. 그러니 내가 할 일은 앱을 가볍게 짜는 것보다 렌더러가 일을 덜 하게 만드는 것 — 즉 불필요한 요청을 막고, 안 보는 탭의 렌더러를 재우는 쪽이었습니다.
탭을 "숨기는 것"과 "재우는 것"은 다르다
가장 크게 배운 지점입니다. 비활성 탭을 그냥 Visibility.Collapsed로 숨기면 화면에서 사라질 뿐, WebView2 렌더러 프로세스는 그대로 살아서 메모리를 계속 붙들고 있습니다. 눈에만 안 보이는 거죠.
WebView2에는 TrySuspendAsync()라는, 렌더러를 실제로 잠재우는 API가 있습니다. 그래서 탭을 전환할 때 비활성 탭은 숨김과 동시에 서스펜드하고, 백그라운드 목표 메모리도 MemoryUsageTargetLevel = Low로 낮춥니다. 다시 그 탭으로 돌아오면 Resume() + Normal로 되살립니다.
// 비활성 탭: 화면에서 숨기는 것만으로는 부족 — 렌더러를 재워야 메모리가 준다
await tab.WebView.CoreWebView2.TrySuspendAsync();
tab.WebView.CoreWebView2.MemoryUsageTargetLevel = CoreWebView2MemoryUsageTargetLevel.Low;
단, 무조건 재우면 안 됩니다. 음악이나 영상을 재생 중인 탭은 뒤로 보내도 계속 들려야 하니까, IsDocumentPlayingAudio를 확인해서 재생 중인 탭은 서스펜드에서 제외했습니다.
스스로를 감시하는 메모리 모니터
"가볍게 만들었다"는 느낌만으로는 부족해서, 앱이 자기 메모리를 상태바에 직접 띄우게 했습니다. 3초마다 프로세스의 WorkingSet을 재고, 구간별로 색을 바꿉니다.
var mb = proc.WorkingSet64 / 1024 / 1024;
var level = mb switch
{
> 1000 => MemoryLevel.Critical, // 비활성 탭 강제 서스펜드
> 700 => MemoryLevel.High,
> 500 => MemoryLevel.Warning,
_ => MemoryLevel.Normal
};
1000MB를 넘으면(Critical) 모니터가 알아서 비활성 탭들을 강제로 재웁니다. 숫자가 눈에 보이니까 "어떤 사이트가 메모리를 폭발시키는지"도 바로 감이 오고, 최적화가 실제로 효과가 있는지 확인하는 척도도 됩니다.
요청 차단 — 스레드 안전한 규칙 스냅샷
광고·트래커 차단은 두 층으로 만들었습니다. 하나는 자체 blocklist.txt(도메인·경로), 다른 하나는 EasyList / EasyPrivacy를 다운로드해 7일 캐시하며 병합하는 것.
여기서 신경 쓴 게 스레드 안전성입니다. EasyList는 수만 줄이라 백그라운드에서 로드되는데, 그 사이에도 페이지 요청은 계속 들어옵니다. 그래서 규칙을 불변 스냅샷(RuleSet)으로 만들어 참조를 통째로 원자 교체하는 구조를 썼습니다. 로드가 끝나면 새 RuleSet을 만들어 volatile 참조 하나만 바꾸면 되고, 요청 처리 쪽은 언제 읽어도 완결된 규칙 묶음을 봅니다.
private volatile RuleSet _rules; // 갱신 = 새 인스턴스로 참조 교체(원자적)
또 경로 규칙은 host별로 인덱싱해서, 요청 하나당 수만 개 규칙을 전부 훑지 않고 해당 host의 규칙만 보게 했습니다. 코스메틱 차단(광고 자리를 CSS로 숨기기)은 EasyList의 ## 규칙을 파싱해서 페이지에 주입합니다. 여기에 자동 재생 미디어 차단(HTMLMediaElement.autoplay 무력화)까지 더하면, 페이지가 로드하는 것 자체가 줄어들어 렌더러 부담이 내려갑니다.
어디까지 만들고 멈췄나
정직하게 말하면 이건 "완성된 제품"이 아니라 "내가 쓰기에 충분한 도구"입니다. 앱 셸, 탭 3개, 메모리 모니터·서스펜드, 세션 복원(종료 시 열린 탭 저장), 요청·코스메틱 차단까지는 실제로 동작합니다. 반면 사이트별 정책 모드, 북마크·히스토리, 설정 화면, 그리고 장기 구상이던 Rust 정책 엔진과 모바일 앱은 설계 단계로 남겨뒀습니다.
개인 도구의 좋은 점이 이겁니다 — "내가 겪는 문제가 해결되는 지점"에서 멈춰도 됩니다. 크롬을 이기려던 게 아니라 내 탭 두세 개를 가볍게 굴리려던 거였고, 그 목적은 이뤘습니다. 만들면서 배운 WebView2 제어, 렌더러 서스펜드의 함정, 스레드 안전한 규칙 교체 같은 것들이 이 글의 수확입니다.
관련 글
원격 데스크톱을 직접 설계해보니 — '직접 연결 먼저, 안 되면 릴레이'의 정체
상대 PC를 보고 조작하는 원격 제어 도구를 설계하며 정리한 연결 전략(WebRTC ICE), 입력 주입, 그리고 보안 우선 설계 이야기입니다.
2026.07.12아이패드를 노트북 보조 모니터로 — 웹으로는 못 만드는 도구를 만든 이야기
Windows 가상 디스플레이 생성, 화면 캡처, H264 하드웨어 인코딩, WebRTC 전송까지 — 브라우저 밖에서만 가능한 화면 확장 도구의 개발 기록입니다.
2026.07.11
데모사내 고객 화상회의 앱을 만들고 정적 사이트에 붙여봤습니다
WebSocket 시그널링 + WebRTC mesh로 동작하는 화상회의 데모를 만들고, 정적으로 배포되는 블로그에 실시간 백엔드를 어떻게 연결했는지 정리했습니다.
2026.07.10