홈시리즈멘토링

© 2026 정기창. All rights reserved.

본 블로그의 콘텐츠는 CC BY-NC-SA 4.0 라이선스를 따릅니다.

☕후원하기소개JSON Formatter러닝 대기질개인정보처리방침이용약관

© 2026 정기창. All rights reserved.

콘텐츠: CC BY-NC-SA 4.0

☕후원하기
소개|JSON Formatter|러닝 대기질|개인정보처리방침|이용약관

Playwright MCP 19개 프로세스 사고, LaunchAgent HTTP 상주로 통합한 회고

정기창·2026년 5월 13일

저는 평소 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-codemcpplaywrightlaunchagentmacos

관련 글

Claude Code, 메모리 관점에서 mcp 바라보기

맥북 32GB의 상당량을 평상시에 잡아먹고 있던 원인 중 하나는 모든 MCP를 켜둔 Claude Code 세션이었습니다. 세션별 alias로 필요한 MCP만 활성화하는 방식으로 메모리 압박을 줄여본 기록입니다.

관련도 94%

Claude Code MCP 서버 정리 — 불필요한 도구 제거로 토큰 35% 절약하기

Claude Code의 MCP 서버 10개를 분석해 불필요한 4개를 제거했습니다. MCP 도구 정의가 매 메시지마다 시스템 프롬프트에 들어가 71K 토큰을 소비하고 있었다는 사실, 그리고 실제 정리 과정을 공유합니다.

관련도 93%

Claude Code MCP 를 REST 로 떼어내면 토큰이 절약될까 — blog MCP 27 tool 회고

coolify 자동 배포를 REST 로 떼어낸 다음, blog MCP 27개 tool 도 같은 방식으로 갈아탈 수 있을지 자문해 보았습니다. 며칠 들여다본 결론은 도구 단위 분할이었습니다.

관련도 93%