Engineering · Delivery · Operations

xgen-seepage: 인터넷 없이 설치되는 Excel 에이전트 커넥터와 Office.js를 걷어낸 이유

XGEN 에이전트가 사용자가 열어 둔 Excel 통합문서를 직접 읽고 편집하게 하는 독립 커넥터입니다. Office 애드인 대신 COM 직결과 단일 실행 파일 배포를 택해 폐쇄망 설치를 가능하게 했고, 실제 서버와 실제 Excel에서 검증하며 압축 협상 충돌, 실행 파일의 번들 누락, Office.js 대기 멈춤을 차례로 해결했습니다.

기간
2026.08 – 2026.08
소속
개인 프로젝트
역할
설계·구현

Key Results

57 → 40개
노출 도구 수파라미터가 균질한 조작을 action 도구로 응집, 전 액션을 macOS에서 call_tool 경유로 실동작 검증
0건
패널 로드 시 외부 요청office.js 제거 후 Playwright 계측, XGEN 커넥터 연결에만 의존
A1 → 42
별도 프로세스의 라이브 셀 편집실제 Excel 머신, 사용자가 연 통합문서를 커넥터 프로세스가 편집 후 read-back

상용 Excel용 AI 애드인은 Office.js 태스크 페인과 클라우드 LLM 호출을 전제로 합니다. 외부 인터넷이 막힌 폐쇄망에서는 이 경로가 성립하지 않습니다. xgen-seepage는 "사내에 설치된 XGEN 서버에만 닿으면 인터넷 없이도 에이전트가 사용자의 Excel과 CSV를 읽고 편집한다"를 목표로 만든 독립 커넥터입니다. 핵심은 사용자가 지금 열어 둔 통합문서를 저장 없이 그 자리에서 고치는 것입니다.

일주일 남짓한 개발 기간의 상당 부분은 "됐다"고 생각한 것이 실제로는 동작하지 않던 지점을 찾는 데 들어갔습니다. 가짜 서버로 검증한 브리지는 실제 서버에서 첫 메시지 직후 끊겼고, 빌드에 성공한 실행 파일은 실행하면 그 자리에서 종료됐고, Office 관례를 따라 넣은 초기화 코드가 채팅 패널을 로딩 상태에 묶어 두었습니다. 이 글은 연결 경로를 고른 근거와, 그 실패들을 좁혀 간 과정을 정리합니다.

상용 Excel AI가 폐쇄망에서 성립하지 않는 이유

먼저 실제 제품의 동작 방식을 조사했습니다. Claude for Excel은 Microsoft 마켓플레이스로 배포되는 Office.js 태스크 페인 애드인입니다. 패널의 JavaScript가 Excel API로 시트를 조회·수정하고, 시트 전체를 매번 보내는 대신 필요할 때 범위를 CSV로 가져오는 tool call을 씁니다. 백엔드는 클라우드이고 유료 계정 로그인이 필요합니다. ChatGPT 계열 애드인도 같은 태스크 페인 패턴에 셀 수식처럼 호출하는 사용자 정의 함수를 더한 형태이며, 역시 클라우드 LLM 호출이 전제입니다.

플랫폼 자체가 오프라인을 막는 것은 아닙니다. 태스크 페인은 HTTPS로 호스팅되는 웹 페이지라 루프백 주소에서 자체 서명 인증서로 서빙하면 Office 내장 WebView2가 로드하고, 마켓플레이스 없이 네트워크 공유 폴더 카탈로그로 매니페스트를 사이드로드할 수도 있습니다. 하지만 인증서 신뢰, 매니페스트 사이드로드, 공유 런타임 설정 같은 설치 마찰이 "인터넷 없이 설치"라는 목표에 비해 컸습니다. 태스크 페인은 UI 셸일 뿐이고, 필요한 것은 셀 입출력이었습니다.

선택지 장점 단점 판단
Office.js 태스크 페인 + 로컬 웹 서버 시트 옆에 고정된 패널, 웹 Excel까지 지원 인증서 신뢰와 매니페스트 사이드로드 등 설치 마찰 셀 입출력 경로로는 기각
xlwings (Windows COM, macOS AppleScript) 실행 중인 Excel 프로세스에 직접 연결, 웹 서버·인증서·매니페스트 불필요 별도 UI 필요, 데스크톱 Excel만 지원 채택
닫힌 xlsx 파일 편집 라이브러리 Excel 없이 파일을 직접 편집 지금 열려 있는 통합문서를 다룰 수 없음 범위 밖

값과 수식 API가 거의 1:1로 대응해(range.values와 Range.value, range.formulas와 Range.formula) 편집 시맨틱을 옮기는 데 문제가 없었습니다. 잃은 것은 시트 옆 패널이었고, 이는 아키텍처가 아니라 UI 계층의 문제로 보고 따로 다루기로 했습니다.

기존 커넥터에 올라타려던 첫 설계를 버리다

첫 버전은 사내 데스크톱 커넥터 앱의 로컬 MCP 브리지에 도구를 등록해 쓰는 방향이었고, 실제 산출물도 도구 14개를 stdio MCP 서버로 노출하는 형태였습니다. 같은 날 요구사항을 다시 확인하면서 방향을 바꿨습니다. 폐쇄망에서 쓰는 독립 설치 프로그램이어야 했고, 기존 커넥터 앱과 사내 문서 처리 라이브러리들은 로직을 참고할 대상이지 런타임 의존성이면 안 됐습니다. 그 앱이 설치돼 있지 않아도, 그 앱의 MCP 설정을 건드리지 않아도 동작해야 했습니다.

그래서 XGEN 서버에 직접 로그인하고 에이전트-도구 브리지를 상주시키는 구조를 독립적으로 다시 구현했습니다. 기존 커넥터에서는 코드를 가져오지 않고 와이어 프로토콜만 확인했습니다. 로그인은 비밀번호 평문이 아니라 SHA-256 hex 다이제스트를 보내야 합니다. 게이트웨이가 해시를 그대로 비교하므로 이 규약을 지키지 않으면 로그인은 항상 실패합니다. 브리지는 WebSocket 위에서 아래 프레임을 주고받습니다.

클라이언트 → 서버  {"type":"hello", "catalog_id":..., "tools":[{server, name, description, inputSchema}]}
                   {"type":"ping"}                                   (20초 간격)
                   {"type":"mcp_result", "request_id":..., "ok":..., "result" | "error":...}
서버 → 클라이언트  {"type":"ready", "catalog_id":..., "tool_count":...}
                   {"type":"mcp_call", "request_id":..., "server":..., "tool":..., "args":...}

커넥터가 hello로 도구 카탈로그를 광고하면 서버는 그 사용자의 에이전트 세션에 도구를 자동으로 주입하고, 에이전트가 도구를 호출하면 mcp_call이 커넥터로 내려옵니다. 커넥터는 대상에 따라 열린 Excel용 어댑터(xlwings)나 CSV 어댑터에 위임하고 결과를 mcp_result로 반환합니다. 사용자가 XGEN 어디서 어떤 에이전트와 채팅하든 커넥터가 연결돼 있으면 그 에이전트가 Excel 도구를 쓸 수 있습니다.

참고한 로직은 세 가지였습니다. 사내 문서 어댑터 라이브러리(Apache-2.0)에서 병합 셀의 anchor 리다이렉트와 숫자처럼 보이는 문자열의 판정 휴리스틱만 이식해 출처를 NOTICE에 남겼고, 사내 문서 청킹 라이브러리에서는 국내 CSV(EUC-KR, CP949)로 검증된 인코딩 감지 순서(BOM 우선, chardet 신뢰도 임계, 후보 인코딩 순차 시도, latin-1 최종 폴백)를 이식했습니다. 배포는 pip 설치가 불가능한 환경을 전제로 했습니다. 기존 폐쇄망 배포 문서에도 내부망에서는 pip·npm·cargo 빌드가 불가하니 개방망에서 빌드를 끝내고 반입하라고 적혀 있었습니다. 그래서 인터넷이 되는 머신에서 PyInstaller로 단일 실행 파일을 만들고 그 파일만 반입하는 모델을 택했습니다.

가짜 서버로는 증명되지 않은 것들

첫 검증은 직접 만든 가짜 XGEN 서버(aiohttp)를 실제 로컬 소켓에 띄우고 로그인부터 hello, ready, mcp_call, mcp_result까지 왕복하는 통합 테스트였습니다. 돌이켜 보면 이것은 "내가 이해한 프로토콜대로 내가 짠 코드가 동작한다"까지만 증명한 것이었습니다. 그래서 실제 개발 서버와 실제 계정으로 다시 검증했습니다.

로그인은 성공해 해시 규약이 실제 게이트웨이와 일치함을 확인했습니다. 문제는 브리지에서 나왔습니다. WebSocket 핸드셰이크와 인증은 성공하는데 hello를 보낸 직후 close 프레임 없이 연결이 끊겼습니다. 원인을 세 단계로 좁혔습니다. 먼저 도구를 하나도 넣지 않은 최소 hello로도 같은 증상이 재현돼 도구 스키마 문제를 배제했습니다. 다음으로 실제 백엔드의 핸들러 소스를 열어 메시지 형식과 필드명이 구현과 일치함을 확인해 메시지 스키마 문제를 배제했습니다. 남은 차이는 전송 계층이었고, websockets 라이브러리가 기본으로 협상하는 permessage-deflate 압축 확장을 의심했습니다. compression=None으로 접속하자 즉시 ready가 돌아왔습니다. 이 설정은 회귀 테스트로 고정했고, 도구 14개가 서버에 등록됐습니다.

다음은 실제 에이전트였습니다. 에이전트 노드 하나로 된 최소 워크플로우를 저장하려 하자 422가 났습니다. 저장 API의 본문 형식을 추측으로 짠 탓이라, 프론트엔드의 API 클라이언트 코드를 읽어 content 래핑이 필요함을 확인하고 고쳤습니다. 첫 실행에서는 서버 로그에 커넥터 도구 14개가 자동 주입됐다는 기록이 남아 주입 자체는 확인됐지만, 개발 서버의 로컬 vLLM 모델이 떠 있지 않아 503 No model loaded로 생성이 실패했습니다. provider를 Anthropic(Claude Haiku 4.5)으로 바꿔 다시 실행하자 에이전트가 CSV 조회 도구를 실제로 호출해 "81행(헤더 포함), 8열, 헤더 목록, UTF-8, 쉼표 구분"이라고 답했고, CSV 어댑터로 직접 확인한 값과 정확히 일치했습니다. 도구 없이는 알 수 없는 수치였습니다. 검증에 쓴 워크플로우는 서버에서 삭제했습니다.

같은 과정에서 CLI 결함도 두 개 나왔습니다. OS 키체인 라이브러리의 Windows 백엔드가 비대화형 로그온 세션에서 감싸지 않은 WinError 1312를 던져 CLI가 스택트레이스와 함께 종료됐고, argparse 도움말에 들어 있던 em 대시 문자가 Windows 한글 콘솔(cp949)에서 UnicodeEncodeError를 냈습니다. 앞의 것은 전용 예외로 감싸 안내 메시지로 바꿨고, 뒤의 것은 저장소 전체에서 해당 문자를 제거했습니다.

Excel 없는 머신과 얼린 실행 파일의 함정

개발 머신에는 Excel이 없었고 설치할 관리자 권한도 없었습니다. 열린 Excel 어댑터를 바로 검증할 수 없는 대신, 폐쇄망 배포 대상에도 Excel 라이선스가 없는 환경이 흔하다는 점을 고려해 LibreOffice(UNO) 백엔드를 세 번째 어댑터로 추가했습니다. uno 모듈은 LibreOffice에 번들된 Python 전용 네이티브 확장이라 커넥터가 실행되는 Python에서는 import할 수 없습니다. 그래서 번들 Python으로 워커를 상주 서브프로세스로 띄우고 stdin/stdout JSON 라인으로 명령과 결과를 주고받게 했습니다. UNO 연결은 워커 생애 동안 한 번 맺어 재사용하고, 재시도와 타임아웃 후 워커 재기동 같은 견고성 처리는 워커 안에 캡슐화했습니다. 이 백엔드의 도구 10개가 더해져 도구는 24개가 됐습니다.

얼린 실행 파일에서는 "빌드는 성공하고 실행하면 실패하는" 결함이 세 번 나왔습니다. 첫째, 어댑터가 워커 스크립트를 import가 아니라 파일 경로로 찾아 서브프로세스로 띄우는데, PyInstaller의 정적 분석은 import되는 모듈만 번들에 넣습니다. 실행 파일 안에 워커가 없었고, 폐쇄망 배포를 위해 만든 기능이 정작 폐쇄망 배포 산출물에서 동작하지 않았습니다. 스펙 파일에 그 파일을 데이터로 명시해 고쳤고, 얼린 실행 파일로 실제 LibreOffice에서 셀 쓰기, 읽기, 저장 후 디스크 반영까지 확인했습니다.

둘째, 스펙 파일의 pathex가 비어 있어 packaging 디렉터리에서 빌드하면 프로젝트 루트의 패키지가 보이지 않았고, 실행 파일의 CLI가 ModuleNotFoundError로 즉시 종료됐습니다. 그때까지의 검증은 모두 pathex를 따로 지정한 별도 probe 실행 파일로 했기 때문에 실제 배포 파일의 CLI를 직접 실행한 적이 없었고, 그래서 놓쳤습니다. 스펙 파일 위치 기준 절대 경로로 고정했습니다.

셋째, conda Python은 OpenSSL DLL을 별도 디렉터리에 두는데 PyInstaller가 그곳을 찾지 않아 DLL이 번들에서 빠졌고, 첫 HTTPS 관련 import에서 _ssl 로드가 실패했습니다. 폐쇄망 설치 파일이 XGEN에 아예 접속하지 못하는 상태였습니다. 스펙이 해당 DLL을 명시적으로 번들하게 하고, 재빌드한 파일로 실제 로그인(HTTPS 200)까지 확인했습니다. 이 세 번을 거치며, 얼린 실행 파일은 실제로 실행해 HTTPS 로그인까지 통과해야 검증으로 인정한다는 기준을 세웠습니다.

TLS도 같은 관점에서 다시 봤습니다. httpx는 기본으로 certifi의 고정 공개 CA 목록만 보고 OS 신뢰 저장소를 보지 않습니다. 폐쇄망의 내부 CA가 PC에 설치돼 있어도 XGEN 인증서를 거부하고, 우회 방법은 검증 전면 해제뿐이었습니다. truststore로 OS 신뢰 저장소에 검증을 위임하고, 없으면 표준 라이브러리 기본 컨텍스트로 폴백하게 했습니다. 검증 해제 옵션은 CA가 OS에 없는 예외적인 경우의 최후 수단으로만 남겼습니다.

채팅 패널에서 Office.js를 걷어낸 판단

다음 요구는 Excel 안에 실제 채팅 패널을 띄우는 것이었습니다. 셀 입출력 경로로는 기각했던 태스크 페인을 UI 셸로 다시 꺼냈습니다. 상주 중인 커넥터가 같은 이벤트 루프에서 로컬 HTTPS 서버(Starlette, uvicorn)를 하나 더 띄워 패널 정적 자산을 서빙하고 XGEN의 실행 스트림을 SSE로 프록시합니다. 에이전트의 도구 호출은 기존 WebSocket 브리지를 그대로 타므로, 새로 만든 것은 UI 셸과 그 앞단 프록시뿐이고 셀 입출력 경로는 건드리지 않았습니다.

처음에는 채팅용 워크플로우가 없으면 커넥터가 서버에 자동으로 만드는 단계를 계획했습니다. 그런데 개발 서버에서는 방금 만든 워크플로우가 직후 조회에서 보이지 않았고, 실행은 매번 의미 없는 일반 오류 코드로 끝났습니다. 서버 로그를 직접 확인하니 "워크플로우를 찾을 수 없음" 예외를 사용자 메시지로 바꾸는 함수가 None으로 바인딩돼 TypeError로 한 번 더 실패하며 진짜 원인을 가리고 있었고, 같은 부류의 실패가 기존 스케줄 작업에서도 나고 있었습니다. 서버 쪽 문제였지만 이 실측이 설계를 바로잡았습니다. 커넥터가 전용 워크플로우를 서버에 만들어 소유할 것이 아니라, 사용자가 이미 가진 워크플로우 중 하나에 연결하는 것이 맞았습니다. 존재가 확인된 워크플로우만 다루므로 "방금 만든 것이 안 보이는" 문제도 구조적으로 피해 갑니다.

패널에서도 판단을 한 번 뒤집었습니다. Office 애드인 관례대로 Office.onReady()를 넣었더니 패널이 Office 호스트를 기다리며 로딩 상태에서 멈췄습니다. 처음에는 폐쇄망 대응으로 office.js를 CDN 대신 로컬에서 서빙하는 데서 그쳤는데, 다시 보니 이 패널은 Office JavaScript API를 하나도 쓰지 않았습니다. 셀 편집은 패널이 아니라 커넥터의 브리지가 하기 때문입니다. office.js와 onReady 게이팅을 모두 걷어내고, 로드 시점에 커넥터 연결만 확인하는 순수 웹 UI로 바꿨습니다. Playwright로 측정하니 상태가 바로 "XGEN 연결됨"으로 바뀌었고 페이지 로드 중 외부(Microsoft 포함) 요청은 0건이었습니다. 목표는 "Office 비의존"이 아니라 "XGEN 의존"이었다는 점을 이때 분명히 했습니다.

리본 버튼에서는 틀린 결론을 한 번 냈습니다. 검증 머신의 Office 2021 볼륨 빌드가 웹 애드인을 띄우지 못한다고 보고 Microsoft 365가 필요하다고 판단했는데, 클라우드 구독은 폐쇄망에서 어차피 쓸 수 없으니 결론이 될 수 없었습니다. 클라우드를 거치지 않는 개발자 레지스트리 사이드로드를 다시 파고들자 원인이 나왔습니다. 레지스트리 값의 데이터뿐 아니라 이름도 매니페스트 전체 경로여야 하는데, 이름에 다른 문자열을 넣었던 것입니다. 이름을 경로로 고치자 Excel 홈 탭에 버튼이 나타났고, 인증서 생성과 신뢰 등록, 매니페스트 포트 패치, 레지스트리 등록을 install-excel-addin 명령 하나로 자동화했습니다. 리본 버튼을 지원하지 않는 Office에서도 패널은 순수 웹 UI이므로 브라우저로 열 수 있게 panel 명령을 함께 두었습니다.

실제 Excel에서 닫은 전체 경로와 도구 재설계

열린 Excel 어댑터는 실제 Office가 설치된 다른 Windows PC에 원격으로 접속해 처음 검증했습니다. live 도구 8개를 실제 통합문서 상대로 왕복하며 결함 세 개를 찾았습니다. 사용 중인 xlwings 버전의 Book 래퍼에는 saved 속성이 없어 첫 호출 도구인 통합문서 목록 조회가 즉시 실패했고(raw COM의 Saved로 접근하도록 수정), 다중 셀 범위의 수식이 list가 아니라 tuple로 와서 이미 2차원인 배열을 한 번 더 감쌌고, Excel의 숫자가 모두 float으로 와서 정수값이 3.0처럼 표시됐습니다. 별도로 브리지의 stop()이 진행 중인 WebSocket 연결을 닫지 않아 Ctrl+C 정상 종료가 끝나지 않던 hang도 고쳤습니다.

패널 채팅도 처음에는 매번 503으로 실패했습니다. 워크플로우 에이전트 노드의 provider가 비어 있으면 서버 기본값으로 떨어지는데, 그 기본값이 떠 있지 않은 로컬 모델이었습니다. 실행할 때 provider를 에이전트 노드 파라미터로 주입하고 패널에서 provider를 고를 수 있게 하자 바로 동작했습니다.

마지막으로 실제 배포 형태를 재현했습니다. 한 프로세스가 Excel로 통합문서를 열어 두고(A1 = "USER_BEFORE"), 별도 커넥터 프로세스가 열린 통합문서 목록에서 그 Excel의 PID와 파일을 찾아 A1을 "42"로 바꾼 뒤 같은 라이브 통합문서에서 다시 읽어 확인했습니다. 패널이 쓰는 채팅 스트림 경로로도 "A1을 42로 바꿔"를 보내, 에이전트가 통합문서 목록 도구와 셀 쓰기 도구를 호출하고 A1이 42.0으로 읽히는 것까지 확인했습니다. 에이전트 응답이 이전 값을 정확히 언급해 라이브 셀을 실제로 읽었음을 보여 줬습니다. 이 과정에서 커넥터가 서비스 세션 0에, Excel이 콘솔 세션 1에 있으면 COM 교차 세션 접근이 막혀 열린 통합문서 목록이 비는 배포 제약도 실측했습니다.

이후 서식, 시트, 구조 편집 요구가 이어지며 도구가 24개에서 57개까지 늘었습니다. 도구 수를 줄이려고 모드 토글 같은 임시방편을 두는 대신, 파라미터가 균질한 조작을 action 파라미터를 가진 도구 하나로 묶었습니다.

통합 도구 흡수한 개별 도구 수 포함한 조작
format_live_range 9 채우기, 글꼴, 표시 형식, 테두리, 줄바꿈, 병합, 병합 해제, 유효성, 지우기
manage_live_sheet 5 시트 목록, 추가, 이름 변경, 삭제, 이동
edit_live_rows_columns 4 행·열 삽입, 삭제
manage_live_name 3 이름 정의 목록, 추가, 삭제
set_live_visibility 2 행·열 숨김, 표시
resize_live 2 열 너비, 행 높이

조회, 값 편집, 조건부 행 강조처럼 성격이 뚜렷한 도구는 단일 도구로 남겼고, 구현돼 있었지만 광고되지 않던 정렬과 범위 지우기를 정식으로 노출했습니다. 노출 도구는 57개에서 40개가 됐고, 모든 액션을 macOS에서 도구 디스패처 경유로 실동작 검증했습니다. 도구를 늘리는 동안 지킨 원칙은 비파괴 편집이었습니다. 초기 정렬 도구가 값을 다시 써 넣으며 수식과 서식을 날리던 것을 Excel 네이티브 정렬로 바꿨고, 여러 줄 헤더가 있는 대장에서 에이전트가 행마다 색칠 도구를 부르다 행 오프셋을 틀리던 사고는 조건으로 한 번에 강조하는 도구로 막았습니다.

결과와 남은 과제

지표 이전 이후 측정 조건
WebSocket 브리지 등록 hello 직후 연결 종료 ready 수신, 도구 14개 등록 실제 개발 서버, compression=None 적용 전후
에이전트 CSV 조회 503(로컬 모델 미기동) 81행·8열·헤더 정확 같은 최소 워크플로우, provider를 Anthropic으로 변경
패널 로드 Office.js 로딩 중 멈춤 즉시 연결, 외부 요청 0건 office.js 제거 전후, Playwright 계측
에이전트 셀 편집 503 실패 A1 read-back 42.0 실제 Excel 머신, 패널 채팅 경로, provider 주입 후
노출 도구 수 57개 40개 action 도구로 응집, 전 액션 실동작 검증

남은 과제는 배포 쪽에 몰려 있습니다. 설치 마법사(NSIS 등)는 아직 없고 단일 실행 파일 단계입니다. 회사의 기존 폐쇄망 반입은 모두 컨테이너 이미지 경로였고 데스크톱 실행 파일이 그 경로를 탄 전례가 없어, 실제로 반입할 때는 새 반입 유형을 운영 측과 협의해야 합니다. OS 신뢰 저장소 기반 TLS는 공개 인증서에서 회귀가 없음을 확인했을 뿐, 사설 CA 환경에서의 실측은 남아 있습니다. macOS는 단일 바이너리 빌드와 로그인까지 확인했지만, Excel 자동화 권한이 GUI 승인으로만 가능해 원격으로는 셀 편집을 검증하지 못했습니다. 활성 서버를 바꾸면 커넥터를 재시작해야 반영된다는 한계도 명시해 두었습니다.

돌아보면 이 프로젝트의 실패는 대부분 "증명했다고 생각한 범위"와 "실제 범위"의 차이였습니다. 가짜 서버 테스트, probe 실행 파일, 빌드 성공은 각자의 범위만 증명했고, 실제 서버와 실제 Excel, 실제 배포 파일로 끝까지 실행했을 때 남은 결함이 드러났습니다.