Playwright MCP 19개 프로세스 사고, LaunchAgent HTTP 상주로 통합한 회고
저는 평소 Claude Code 대화 세션을 디폴트로 Playwright MCP 가 연결된 상태로 열어왔습니다. 풀스택 모노레포 작업을 병렬로 돌리느라 동시에 8개 안팎의 세션을 띄워두는 일이 잦은데, 그때마다 새 세션이 자기 Playwright MCP 를 끼고 들어오는 구성이었습니다.
솔직히 가정하고 있던 게 있었습니다. "대화 세션을 닫으면, 거기에 붙어있던 Playwright MCP 도 알아서 같이 정리되겠지" 라는 가정이었습니다. 그 가정이 실제로는 맞지 않다는 것을, 오늘 Claude 와 대화하다가 뒤늦게 알게 됐습니다. 세션은 닫혔는데 자식으로 spawn 됐던 MCP 프로세스는 그대로 머물러 있을 수 있다는 사실이었습니다.
한편으로 우려는 있었습니다. Playwright MCP 가 무거운 편이라는 사실은 알고 있었고, 8개 세션을 띄워두면 단순한 곱셈으로 메모리가 잡힐 수밖에 없겠다는 산수도 머리 어딘가에는 있었습니다. 그런데 그게 "느낌" 에서 멈춰있었고, 실제로 ps 한 번을 돌려본 적이 없었다는 게 핵심 문제였습니다. 안 쓰는 세션을 닫지 않고 방치하는 습관까지 더해지면, 곱셈은 계속 쌓이고 있을 거라는 막연한 불안은 늘 있었던 셈입니다.
이 글은 그 미뤄둔 측정 — ps aux 한 줄에서 19개라는 결과 — 을 마주한 순간에서 출발해, LaunchAgent 와 HTTP MCP 상주 모델로 19개를 2개로 줄이기까지의 운영 회고입니다. 결과적으로 약 1.6GB 메모리를 회수했지만, 그보다도 "세션을 닫으면 다 정리될 것" 이라는 막연한 가정을 직접 깨준 메타 교훈이 더 묵직하게 남았습니다.
ps aux 한 줄에서 시작된 의문
오늘 Claude 와 대화하던 중에, "세션을 닫아도 stdio MCP 의 자식 프로세스는 자동으로 정리되지 않는다" 는 사실을 명시적으로 듣는 순간이 있었습니다. 줄곧 미뤄두던 측정을 그제야 처음으로 돌려본 셈이었습니다.
ps aux | grep -E "playwright|mcp" | grep -v grep | wc -l
# 19
19라는 숫자 자체보다도, 그 안을 들여다본 구성이 더 인상적이었습니다. Claude Code 대화 세션이 4개 떠있는 상태였는데, 제 .mcp.json 에는 dogfood 6 페르소나 격리용으로 playwright-p1 부터 playwright-p6 까지 stdio MCP 가 등록되어 있었습니다. 세션마다 이걸 전부 spawn 한다면 산술적으로 4 × 6 = 24개가 되는 셈입니다. 거기에 자식 Chrome 인스턴스까지 더해지면 19라는 숫자가 오히려 적게 느껴지는 정도였습니다.
곰곰이 생각해보니, 그동안 "Claude Code 가 알아서 lazy load 하겠지" 라고 막연히 가정해 왔습니다. 그런데 적어도 stdio MCP 의 프로세스 spawn 만큼은 그 가정이 통하지 않는다는 것이 첫 발견이었습니다.
메모리 노트가 거짓말을 하고 있었습니다
의문이 생기자, 가장 먼저 한 일은 제 옛 메모리 노트를 다시 펴보는 것이었습니다. 거기엔 이렇게 적혀 있었습니다.
"MCP 도구는 로드만으로 매 메시지 시스템 프롬프트에 풀 스키마 포함 → 토큰 소비. deferred/ToolSearch 전환은 사용자 제어 불가 (Claude Code 내부 자동 판단)."
이 문장을 적었던 시점에는 맞는 이야기였습니다. 그런데 2026년 5월 현재 시점에서 다시 검증해보니, 부분적으로 outdated 였습니다. Claude Code v2.1.7 (2026년 1월 릴리스) 부터 ENABLE_TOOL_SEARCH=auto 가 Sonnet 4+ / Opus 4+ 모델 기본값으로 ON 입니다. 즉 MCP 도구의 schema 가 매 메시지 시스템 프롬프트에 upfront 로 박히는 게 아니라, ToolSearch 호출 시점에만 inline expand 되도록 자동 전환되어 있었던 것입니다.
실측치도 명확했습니다. 어느 측정에서는 일반적인 50개 도구 환경에서 약 77K 토큰이 8.7K 로 줄었다는 보고가 있었고, Scott Spence 의 케이스에서는 66K → 5.6K (약 60% 절감), Pramod Dutta 가 측정한 Playwright MCP 단독 시나리오에서는 114K 가 비교 대상이 되었습니다. 어느 쪽이든 토큰 비용 측면에서 이미 자동으로 큰 폭의 절감이 일어나고 있었다는 사실은 분명했습니다.
다행이라는 안도와 동시에, 솔직히 부끄러운 마음도 들었습니다. 제 메모리 노트가 거짓말을 하고 있는 동안, 저는 그걸 근거로 의사결정을 해왔다는 뜻이기 때문입니다. AI 도구 생태계는 6개월 단위로 바뀐다는 점을, 운영자 입장에서도 정기적으로 검증해야 한다는 첫 번째 메타 교훈이 여기서 나왔습니다.
Tool Search 는 토큰만 해결할 뿐, 메모리는 별개였습니다
그렇다면 ENABLE_TOOL_SEARCH 가 다 해결해 준 것 아닌가, 라는 안일한 결론으로 흐를 뻔했습니다. 그런데 다시 처음의 19개 프로세스를 떠올려보면 그렇지가 않습니다. Tool Search 가 줄여주는 토큰과, 제 노트북에서 spawn 된 프로세스 메모리는 전혀 다른 비용이었습니다.
| 비용 종류 | Tool Search 가 해결? | 누가 부담? |
|---|---|---|
| 시스템 프롬프트 토큰 | O (자동 lazy load) | Anthropic 서버 |
| MCP 서버 프로세스 spawn + 메모리 | X (무관) | 사용자 맥북 |
한쪽은 똑똑해졌지만 다른 한쪽은 그대로였다는 인식이 여기서 정리되었습니다. 그리고 "그대로인 쪽" 의 비용은 제가 직접 부담하고 있었기 때문에, 결국 운영 차원의 조치가 필요하다는 결론에 다다랐습니다.
stdio 와 HTTP 의 본질 차이를 다시 짚어보았습니다
다음으로 한 일은 MCP 의 transport 두 종류를 다시 차분히 비교해보는 일이었습니다. 이미 알고 있다고 생각했던 내용이었지만, 막상 19 프로세스 사고를 겪고 나니 같은 표가 다르게 읽혔습니다.
| 항목 | stdio | HTTP 외부 서버 |
|---|---|---|
| 프로세스 개수 | 세션마다 1개씩 spawn | 1개 (몇 세션이든 공유) |
| 사용자 통제 | X (Claude Code 자동 spawn) | O (사용자가 켜고 끔) |
| 자동 재연결 | X | O (HTTP/SSE 백오프) |
| 다중 세션 동시 작업 | profile lock 충돌 | sessionId 분리 |
이 비교는 spec 차원에서도 명확합니다. MCP Spec 2025-06-18 의 §Transports / Streamable HTTP 절은 이렇게 적습니다.
"the server operates as an independent process that can handle multiple client connections"
요약하면, Mcp-Session-Id 헤더로 server 가 client 들을 구분하는 방식으로 동시 접속이 정식 지원된다는 뜻입니다. 즉 HTTP MCP 서버 한 개에 Claude Code 세션 N개가 붙는 것은 어떤 hack 이 아니라, spec 차원에서 의도된 모델이라는 점이 새삼 명확해졌습니다.
Playwright MCP 의 profile lock 함정
다만 Playwright MCP 의 경우에는 한 가지 함정이 있었습니다. default 모드 (persistent profile) 에서는 userDataDir 가 lock 되기 때문에 spec 상 multi-client 가 가능하더라도, 사실상 동시 1 client only 로 동작합니다. 두 번째 client 가 connect 하면 즉시 fail 하고, 자동 takeover 도 없습니다.
이걸 풀어주는 옵션이 두 가지 있었습니다.
--isolated— 각 client 가 in-memory profile 을 가집니다. 격리는 깔끔하지만 로그인 상태가 매번 휘발됩니다.--shared-browser-context— 모든 client 가 한 context 를 공유합니다. 로그인 같은 영구 상태는 같이 쓸 수 있지만, 한쪽이 logout 하면 다른 쪽도 영향을 받습니다.
제 평소 패턴이 "여러 세션이 동시에 Playwright 를 격하게 쓰는" 상황이 거의 없고, 같은 사이트의 로그인을 매번 다시 하는 비용이 크다는 점을 고려해 --shared-browser-context 를 선택했습니다. 단점이 없는 결정이 아니라, 제 사용 패턴 위에서 균형이 맞는 결정이라는 점은 솔직히 명시해 두고 싶습니다.
dogfood 6 페르소나 격리를 깨지 않으면서 통합하기
여기서 한 가지 충돌이 있었습니다. playwright-p1 부터 p6 까지의 stdio 서버는 단순히 "성능을 위해" 분리해둔 게 아니라, dogfood 6 페르소나의 로그인과 세션 상태를 격리하기 위해 각자 다른 user-data-dir 를 쓰도록 짜둔 것이었습니다. 그런데 모든 stdio 를 1개의 HTTP shared-context 로 옮기면, 이 격리가 깨져버립니다.
그래서 모든 걸 통합하기보다는 하이브리드 구조를 택했습니다.
- 메인 작업용 — HTTP MCP 1개 (이름은
playwright). 평소 페이지 확인, 시각 검증, 일상적인 자동화는 전부 여기로 모읍니다. - dogfood 페르소나 격리용 — 기존 stdio 6개 (
playwright-p1~p6) 를 그대로 유지합니다. dogfood 하네스를 돌릴 때만 작동하고, 평소에는 spawn 되지 않도록 별도 워크트리·세션 단위로 관리합니다.
"하나의 완벽한 정답" 으로 통합하지 않고, 둘로 나눠 둔 데에는 이유가 있었습니다. dogfood 의 격리 요구는 양보할 수 없는 도메인 제약이고, 그 외 평상시 작업의 메모리 절약은 별개의 운영 목표이기 때문입니다.
LaunchAgent 와 .mcp.json — 구현
설계가 정해지자 구현 자체는 단순했습니다. 우선 LaunchAgent plist 의 핵심 블록입니다.
<!-- ~/Library/LaunchAgents/com.kichang.playwright-mcp.plist -->
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/npx</string>
<string>-y</string>
<string>@playwright/mcp@latest</string>
<string>--port</string>
<string>8931</string>
<string>--host</string>
<string>127.0.0.1</string>
<string>--shared-browser-context</string>
<string>--user-data-dir</string>
<string>/Users/jeongkichang/.cache/playwright-mcp/shared</string>
</array>
<key>KeepAlive</key>
<dict>
<key>Crashed</key><true/>
<key>SuccessfulExit</key><false/>
</dict>
여기서 작게 신경 쓴 부분은 KeepAlive 의 Crashed 만 true 로 둔 점입니다. 의도해서 launchctl bootout 으로 끄는 상황까지 자동 부활시키면, 정작 사용자 본인이 잠시 끄고 싶을 때 일이 꼬입니다. "비정상 종료만 자동 복구, 정상 종료는 그대로 존중" 이라는 기준이 운영 단의 디폴트로 자연스럽다는 생각이 들었습니다.
그다음은 .mcp.json 의 playwright 항목을 stdio 에서 HTTP 로 바꿔주는 일이었습니다.
{
"mcpServers": {
"playwright": {
"type": "http",
"url": "http://localhost:8931/mcp"
},
"playwright-p1": {
"command": "npx",
"args": ["@playwright/mcp@latest", "--isolated", "--user-data-dir", "/tmp/playwright-p1"]
}
}
}
마지막으로 launchctl bootstrap 으로 띄우고, smoke test 로 정말 떠있는지 확인했습니다.
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.kichang.playwright-mcp.plist
curl -s -o /dev/null -w "HTTP %{http_code}\n" -X POST http://localhost:8931/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"1.0"}}}'
# HTTP 200
HTTP 200 한 줄을 보고서야 마음이 놓였습니다. JSON-RPC initialize 가 정상 응답한다는 것은, Claude Code 세션이 붙었을 때도 같은 절차로 핸드셰이크가 통한다는 의미이기 때문입니다.
부수 효과 — 다른 세션의 stdio 연결이 끊겼습니다
전환을 마치고 다시 ps aux 를 돌려보니, 결과는 19개에서 2개로 줄어 있었습니다. LaunchAgent 가 띄운 npm exec 와 그 자식인 Node 서버 (포트 8931 LISTEN) 두 개만 남았습니다. 메모리 측정에서는 약 1.6GB 정도가 회수되었습니다.
다만, 부수 효과가 한 가지 있었습니다. 별도 터미널에 띄워둔 다른 Claude Code 세션들에서 기존 stdio MCP 의 client 연결이 일제히 끊겼습니다. 정리 과정에서 10개의 stdio npm exec 와 8개의 Chrome 자식 프로세스를 SIGTERM 으로 정리했기 때문입니다. stdio 는 자동 재연결이 없다는 사실을, 책으로 읽었을 때보다 훨씬 실감 나게 확인하는 순간이었습니다.
해당 세션들은 /mcp 패널에서 reconnect 하거나, 세션 자체를 재시작해야 했습니다. 처음부터 알고 한 일이라 큰 문제는 아니었지만, "stdio 는 끊기면 그걸로 끝" 이라는 점은 앞으로 운영 단의 결정에 매번 끌어들여야 할 사실이라는 생각이 들었습니다.
두 가지 교훈으로 마무리합니다
운영 단의 교훈은 비교적 단순합니다. stdio MCP 는 대화 세션을 닫는다고 자동으로 같이 종료되지 않습니다. 그리고 Tool Search 가 토큰을 자동으로 줄여주는 와중에도, 프로세스 개수와 메모리는 여전히 client 쪽 (제 노트북) 이 부담하는 영역이었습니다. 8개 안팎의 세션을 일상적으로 띄워두고, 안 쓰는 것까지 방치하는 환경이라면 이 비용은 그대로 곱셈으로 쌓입니다.
메타 교훈은 운영 단의 결과보다 조금 더 묵직했습니다. 제 메모리 노트는 6개월 전 기준이었고, 그 사이 Claude Code 의 default 동작이 바뀌어 있었습니다. "내가 알고 있는 것" 이 outdated 일 수 있다는 가능성을 운영의 한 축으로 두지 않으면, 잘못된 전제 위에서 새로운 결정이 쌓이게 됩니다. 앞으로는 운영 단의 결정을 내리기 전에, 관련 메모리 노트를 짧게라도 재검증하는 절차를 두는 게 좋겠다는 결론에 다다랐습니다.
다음에 해보고 싶은 일은 두 가지입니다. 하나는 다른 stdio MCP 서버들 가운데 HTTP 상주가 의미 있는 후보를 더 추려보는 것입니다. 다만 모든 도구가 그럴 가치가 있는 건 아닙니다. 짧게 한 번 도는 1회성 도구라면 stdio 가 오히려 합리적입니다. 다른 하나는, 메모리 노트의 정기 검증 자체를 자동화 가능한 영역인지 천천히 들여다보는 것입니다. 어느 쪽이든, 19개에서 2개로 줄인 이번 일이 그 다음 결정의 작은 발판이 되어 주리라는 기대가 있습니다.
관련 글
Claude Code, 메모리 관점에서 mcp 바라보기
맥북 32GB의 상당량을 평상시에 잡아먹고 있던 원인 중 하나는 모든 MCP를 켜둔 Claude Code 세션이었습니다. 세션별 alias로 필요한 MCP만 활성화하는 방식으로 메모리 압박을 줄여본 기록입니다.
Claude Code MCP 서버 정리 — 불필요한 도구 제거로 토큰 35% 절약하기
Claude Code의 MCP 서버 10개를 분석해 불필요한 4개를 제거했습니다. MCP 도구 정의가 매 메시지마다 시스템 프롬프트에 들어가 71K 토큰을 소비하고 있었다는 사실, 그리고 실제 정리 과정을 공유합니다.
Claude Code MCP 를 REST 로 떼어내면 토큰이 절약될까 — blog MCP 27 tool 회고
coolify 자동 배포를 REST 로 떼어낸 다음, blog MCP 27개 tool 도 같은 방식으로 갈아탈 수 있을지 자문해 보았습니다. 며칠 들여다본 결론은 도구 단위 분할이었습니다.