Claude Code에 모든 것을 맡기는 것을 그만두었다. tmux 탭을 대화시켜 컨텍스트를 분할하기
요약
Claude Code 사용 시 발생하는 컨텍스트 윈도우 비대화 문제를 해결하기 위해 tmux 탭을 활용한 컨텍스트 분할 전략을 소개합니다. 각 탭을 독립된 관심사로 격리하고, 오케스트레이터 탭을 통해 요약된 정보만 주고받으며 효율적으로 작업을 관리하는 방법을 다룹니다.
핵심 포인트
- tmux 탭을 활용해 독립된 컨텍스트 윈도우를 물리적으로 격리
- 오케스트레이터 탭은 상세 정보 대신 판단에 필요한 요약만 수신
- 실행 탭, 오케스트레이터, 감시 데몬으로 구성된 워크플로우 구축
- 컨텍스트 혼입으로 인한 모델의 정밀도 저하 방지
안녕하세요, TOKIUM에서 엔지니어로 일하고 있는 니시모토입니다.
본론으로 들어가서, Claude Code를 깊게 사용할수록 하나의 벽에 부딪히게 됩니다. 바로 컨텍스트 (Context)입니다. 하나의 세션에서 구현도, 리뷰도, 조사도, 정형 업무도 전부 시키다 보면 컨텍스트 윈도우 (Context Window)가 점점 부풀어 오릅니다. 내용이 길어지면 응답의 정밀도가 떨어지고, /compact 명령어가 실행되면 직전까지 파악하고 있던 문맥이 요약되어 버리면서 세부 사항이 날아갑니다. "아까 결정한 방침을 벌써 잊어버렸네"라고 투덜거리게 되는, 바로 그런 느낌입니다.
컨텍스트 관리 노하우 자체는 세상에 이미 많이 나와 있습니다. 그래서 새로운 원칙을 설명하기보다는, 제 환경을 소개하는 겸 저만의 적용 방법을 써보려 합니다. 제가 한 일을 한마디로 요약하면, 작업을 나눔으로써 컨텍스트를 나누는 것입니다. tmux의 각 탭에서 별도의 Claude Code 인스턴스를 구동하고, 탭끼리 대화하게 만드는 개발·업무 환경을 구축했습니다. 우선 전체적인 모습부터 소개하겠습니다.
출발점: tmux의 탭 = 독립된 컨텍스트 윈도우
발상 자체는 심플합니다. tmux 탭을 N개 열고, 각각에서 Claude Code를 실행합니다. 그러면 N개의 독립된 컨텍스트 윈도우를 얻을 수 있습니다.
이 방법이 효과적인 이유는 컨텍스트에는 "섞으면 열화된다"는 성질이 있기 때문입니다. 앱 A의 구현을 하고 있는 세션에 무관한 리포트 생성 문맥이나 다른 프로젝트의 디버그 로그가 흘러 들어오면, 모델은 무엇이 현재의 관심사인지 판단하기 어려워집니다. 탭을 나누는 것은 물리적으로 문맥을 격리하는 것을 의미합니다. 1탭 = 1개의 관심사. 이것만으로도 각 탭의 컨텍스트는 마지막까지 "그 업무에 관한 것만"으로 채워집니다.
하지만 탭을 분할하는 것만으로는 단순한 병렬 작업에 불과합니다. 탭끼리 연계되지 않으면 인간이 모든 탭을 오가며 상태를 전달해야 하는 상황이 발생합니다. 그래서 탭 간의 통신 프로토콜과, 이를 하나로 묶는 오케스트레이터 (Orchestrator) 역할의 탭을 준비했습니다. 전체 구조는 다음과 같습니다.
- 실행 탭: 1탭 1관심사. 앱 개발, 리포트 생성, 정형 업무 등 전담하여 하나의 업무를 수행 -
- 오케스트레이터 역할의 탭: 각 실행 탭에 업무를 할당하고, 결과의 요약만 받아서 전체를 운영 -
- 감시 데몬 (Daemon): tmux의
capture-pane으로 각 탭의 화면을 정기적으로 읽고, 상태 변화(완료·에러·승인 대기)를 감지하여 통지 -
포인트는 이것들을 한꺼번에 설계한 것이 아니라는 점입니다. "세션이 무겁다", "저 탭의 진행 상황을 모르겠다"와 같이 고통을 느낄 때마다, 쉘 스크립트(Shell Script)와 Claude Code의 훅 (Hook, 특정 타이밍에 자동 실행되는 스크립트)을 하나씩 추가하며 키워왔습니다.
이 글에서는 특히 효과가 있었던 3가지에 집중하겠습니다. 오케스트레이터는 요약만을 가진다, 통신은 구조화하여 압축한다, compact를 넘어 기억을 남긴다의 세 가지 포인트입니다.
1. 오케스트레이터는 요약만을 가진다
세 가지 중 가장 효과가 컸던 것이 이것입니다. 오케스트레이터 역할의 탭은 각 프로젝트의 모든 것을 이해하려고 하지 않습니다.
흔히 하는 실수는 요약 담당자에게 모든 정보를 집약시키는 것입니다. 이렇게 하면 요약 담당자의 컨텍스트가 가장 먼저 파괴됩니다. 각 탭의 작업 로그, 차이점(Diff), 테스트 결과가 전부 흘러 들어와서, 정작 중요한 "다음에 무엇을 해야 하는가"를 판단할 여지가 없어집니다.
그래서 반대로 했습니다. 오케스트레이터는 업무를 실행 탭에 위임하고, 판단에 필요한 수준의 요약만을 받습니다. 실행 탭은 작업 마지막에 정해진 형식의 리포트를 반환합니다.
type: done
summary: 기능 X의 테스트 추가 완료, PR #463 생성
details:
...
오케스트레이터가 받는 것은 이 몇 줄뿐입니다. 100줄의 차이점도, 긴 테스트 로그도 실행 탭의 컨텍스트 안에 그대로 둡니다. next는 "다음에 무엇을 해야 하는가"를 실행 탭 스스로가 제안하는 칸이며, 오케스트레이터는 이를 읽고 다음 할당을 결정합니다.
하고 있는 일은 인간의 매니지먼트와 같습니다. 매니저가 모든 멤버의 코드를 한 줄씩 읽고 있다면 운영될 수 없습니다. "끝났습니다, 다음은 이것을 해야 한다고 생각합니다"라는 보고를 받고 배분합니다. 컨텍스트의 압박을 방지하기 위해, 의도적으로 상세 정보를 갖지 않는다. 이 결단이 전체를 장시간 운영하는 데 결정적이었습니다.
2. 통신은 구조화하여 압축한다
탭끼리 직접 주고받는 상황도 있습니다. 예를 들어, 앱 개발 탭이 업무 조사 탭에게 "이 평가 기준의 판정 로직, 다른 탭에서 조사했던 내용을 알려줘"라고 묻는 식의 케이스입니다.
여기서도 의식하고 있는 것은 전문을 다 밀어 넣지 않는 것입니다. 탭 간 통신에는 용도에 따라 접두사(Prefix)를 정해 두었습니다.
| 접두사 | 용도 | 수신 측의 동작 |
|---|---|---|
tab:ask | 질문·상담 | 답변을 tab:reply로 반환 |
tab:share | 정보 공유 | 받아서 활용 (응답 불필요) |
tab:request | 작업 의뢰 | 실행 후 결과 보고 |
보내는 쪽은 자신의 컨텍스트에서 "결론 부분"만 추출하여 상대에게 전달합니다. 받는 쪽도 상대의 세션 전체가 아니라, 이 메시지 하나만을 자신의 문맥에 포함합니다. **컨텍스트의 압축 전송 (Context Compression Transfer)**입니다. 인간 팀에서 "모든 경위를 공유하는" 것이 아니라 "요점만 Slack으로 던지는" 것과 비슷합니다.
참고로 통신은 비동기(Asynchronous)입니다. 질문을 던지면 상대의 응답을 기다리지 않고 자신의 작업을 계속합니다. 동기적으로 기다리면 탭이 굳어버려 병렬성(Parallelism)이 죽기 때문에, "던져두고, 돌아오면 줍는다"를 원칙으로 하고 있습니다.
여담: 보냈다고 생각했는데 전달되지 않았던 이야기
이 통신 부분과 관련하여 솔직하게 적어두고 싶은 실패담이 있습니다. 처음에는 탭으로 메시지를 보내는 것을 tmux send-keys로 단순하게 구성했는데, Enter 키 누락이나 특수 문자 이스케이프(Escape) 오류로 인해, 보냈다고 생각한 메시지가 입력창에 남은 채 전송되지 않는 일이 빈번하게 발생했습니다. 게다가 Claude Code 측에서는 "전송했습니다"라고 보고하기 때문에 한동안 알아채지 못했습니다. 전송 명령이 성공하는 것과 메시지가 실제로 도달하여 처리되는 것은 별개의 문제였던 것입니다.
지금은 전송을 tab-send와 같은 전용 스크립트로 집약하여, 이러한 종류의 결함을 재현 가능한 형태로 해결했습니다. 나아가 전송 직후에 상대 탭의 입력창이 제대로 비워졌는지(=전송이 통했는지) 확인하도록 하고 있습니다. "성공 응답 = 배포 성공"이라고 단정 짓지 마세요. AI에게 작업을 맡길수록 이러한 종류의 검증을 프로토콜에 내장하는 가치가 높아진다고 느낍니다.
3. compact를 넘어 기억을 남기기
탭을 나누더라도 하나의 탭이 장시간 작동하면 해당 탭 자체는 /compact에 직면하게 됩니다. compact로 인해 방침이나 진척도가 요약(Summary)으로 뭉뚱그려지면, 이어지는 작업이 불투명해집니다.
여기서 효과적인 것은 todo.md를 "디스크 위의 체크리스트"로 가지고 있는 것입니다. 여러 단계의 태스크(Task)에서는 작업 중인 탭이 todo.md를 자율적으로 관리합니다. 태스크 시작 시 생성하고, 단계가 완료될 때마다 체크를 업데이트하며, 완료되면 삭제합니다. 컨텍스트가 요약으로 뭉뚱그려지더라도, 파일로 남아 있는 todo.md를 다시 읽으면 "지금 어디까지 했고, 다음에 무엇이 남았는지"라는 골격은 되찾을 수 있습니다. 요약 안에 기억을 남기려 노력하는 것이 아니라, 요약의 외부(파일)로 빼두는 이미지입니다.
나아가, compact 직전에 실행되는 훅(Hook, PreCompact)에 안전장치를 넣어 두었습니다. todo.md가 최근에 업데이트되었다면 = 아직 작업이 한창인 상태라고 판단하여, compact가 실행되려 할 때 일단 멈추고 "먼저 상태를 저장한 뒤에 진행하세요"라고 권고합니다. 요약되어 곤란해지는 타이밍에는 애초에 요약되지 않도록 하는 것입니다. 이를 통해 장시간의 태스크에서도 compact를 넘어 작업을 계속할 수 있게 되었습니다.
이와 함께 스테이터스 라인(Status Line)에 컨텍스트 사용률 바를 표시하여, 임계값을 넘으면 알림이 가도록 설정했습니다. 컨텍스트 잔량계라고 할 수 있습니다. "알아차렸을 때는 이미 늦었다"는 상황을 방지하고 싶을 뿐인데, 이것이 은근히 효과적입니다.
개발에서도 업무에서도 같은 메커니즘이 통한다
이 환경의 장점은 앱 개발과 일상 업무를 동일한 메커니즘 위에서 돌릴 수 있다는 점입니다.
개발 측면에서는 여러 브랜치(Branch) 작업을 탭별로 나누어 병행합니다. 구현 탭과 리뷰 탭을 별도로 운영하며, 구현이 끝나면 리뷰 탭에 tab:request를 던지는 식의 연계가 가능합니다. 각 탭의 컨텍스트는 자신의 브랜치에 대한 내용으로만 채워지므로 혼선이 생기지 않습니다.
업무 측면에서는 리포트 생성, 진척도 수집, 정형화된 집계 등을 전용 탭(dedicated tab)에 맡기고 있습니다. 이것들은 "정해진 절차를 정해진 타이밍에 실행한다"는 성질을 가지고 있으므로, 전용 탭에 스킬(절차를 정리한 호출 가능한 정의)로서 갖춰두고, 오케스트레이터(Orchestrator)가 시각이나 이벤트에 따라 실행(kick)합니다. 인간은 나온 요약본을 훑어보기만 하면 됩니다.
개발 탭과 업무 탭이 혼재되어 있어도 각각이 독립된 컨텍스트(Context)를 가지며, 오케스트레이터는 요약만을 취합합니다. 이런 형태로 바꾼 뒤로 문맥 전환 비용(context switching cost)이 상당히 낮아졌습니다.
얻은 효과
가장 큰 것은 개별 세션이 가벼운 상태로 유지된다는 점입니다. 각 탭은 자신의 관심사로만 컨텍스트가 채워지므로, 무관한 문맥에 의한 성능 저하가 일어나기 어렵습니다. 장시간 작업에서도 정확도가 떨어지지 않게 되었습니다.
정확도뿐만 아니라 소비 토큰의 최적화로도 이어지고 있습니다. LLM은 매 턴(turn)마다 그때까지의 전체 컨텍스트를 다시 읽기 때문에, 문맥이 커질수록 1턴당 입력 토큰도 늘어납니다. 각 탭을 작게 유지하고 오케스트레이터에는 요약만 쌓는 구성이라면, 이 부분이 불필요하게 커지지 않습니다. 하나의 거대한 세션에 모든 것을 쌓아 올리는 경우와 비교했을 때, 같은 일을 더 적은 토큰으로 처리할 수 있습니다. 오래 돌릴수록 이 차이는 효과를 발휘합니다.
병렬로 실행할 수 있게 된 것도 큽니다. 지금까지 자신이 1개 세션에 매달려 순차적으로 처리하던 작업이 여러 탭에서 동시에 진행됩니다. 인간은 오케스트레이터의 요약을 보고 판단이 필요한 부분에만 개입합니다. 손을 움직이는 양보다 보고 결정하는 양이 늘어난 느낌입니다.
그리고 은근히 고마운 점은 전체를 관측할 수 있다는 것입니다. 모니터링 데몬(monitoring daemon)이 각 탭의 상태를 수집해 주므로, "어느 탭이 막혀 있는지", "승인 대기 중으로 멈춰 있는 것은 무엇인지"를 일람으로 알 수 있습니다. 탭을 늘려도 인간이 전부를 감시할 필요가 없습니다.
남은 과제
그렇다고는 해도, 운용해 보니 허점도 보입니다.
먼저, **통신의 신뢰성과 오버헤드(overhead)**입니다. 앞서 언급한 송신 사고는 구조적으로 해결했지만, 비동기 통신인 이상 "던진 질문의 답이 돌아오지 않는다"거나 "상대 탭이 다른 작업에 몰두하고 있어 응답이 느리다"와 같은 일은 발생합니다. 인간 팀과 마찬가지로 탭이 늘어날수록 조정 비용도 증가합니다.
다음은, 과도한 분할의 함정입니다. 무엇이든 탭으로 분리하면 좋은 것은 아닙니다. 탭의 기동, 통신, 상태 관리에는 각각 비용이 따르며, 1분이면 끝날 작업을 굳이 별도 탭으로 던지면 통신 오버헤드가 더 커집니다. "이 작업이 독립된 컨텍스트를 가질 가치가 있는가"를 매번 물어야 합니다. YAGNI(You Ain't Gonna Need It)와 마찬가지로, 불필요한 분할은 하지 않습니다.
마지막으로, 오케스트레이터의 판단 품질입니다. 요약만으로 지휘하는 이상, 요약의 질이 나쁘면 판단을 그르칩니다. 실행 탭이 next에 엉뚱한 제안을 적으면 오케스트레이터는 그에 끌려갑니다. 요약의 포맷과 무엇을 요약에 포함해야 하는지에 대한 설계가 그대로 전체의 품질을 결정합니다.
향후 전망
지금은 통신 프로토콜과 모니터링을 수제 스크립트와 hook으로 지탱하고 있습니다. 다만, 이 부분은 tmux send-keys라는 힘을 쓰는 방식에 가까운 구현에 의존하고 있는 부분이기도 합니다. 최근에는 매니지드(managed) 에이전트 실행이나 에이전트 간 연계 메커니즘이 공식적으로 갖춰지고 있으므로, 이런 종류의 토대(인프라)는 조만간 그쪽으로 옮겨가고 싶습니다. 직접 파이프라인을 계속 닦는 것에는 큰 의미가 없다고 생각합니다.
그 위에서 정말 주력해야 할 것은 AI에게 맡길 수 있는 판단의 영역을 어디까지 넓힐 수 있는지, 그리고 그 정확도를 어떻게 높일 것인가라고 생각합니다. "어느 탭에 무엇을 위임해야 할지", "이 보고는 인간에게 올려야 할지 자동으로 진행해도 될지"와 같은 판단을 과거 로그로부터 학습시켜 조금씩 맡길 수 있는 범위를 넓혀가는 것입니다. 통신이나 모니터링은 조만간 기성품에 맡기고, 자신은 이 판단의 질에 시간을 쓰고 싶다는 것이 현재의 방향성입니다.
결국 컨텍스트 관리는 "한정된 창에 무엇을 비추고 무엇을 비추지 않을 것인가"의 설계라고 생각합니다. 하나의 거대한 컨텍스트에 모든 것을 떠안기는 것이 아니라, 관심사별로 창을 나누고 그 사이를 요약으로 잇는 것. tmux라는 소박한 도구 위에서도 이 생각은 제대로 기능했습니다.
요약
Claude Code의 컨텍스트는 유한합니다. 그렇기에 작업을 탭으로 나누어 컨텍스트를 분할하고, 탭끼리 구조화된 메시지(structured message)로 대화하게 하는 설계가 효과적이었습니다.
- 탭(Tab) = 독립된 컨텍스트 윈도우 (Context Window). 1개 탭당 1개의 관심사만 다루어 혼선을 방지
- 오케스트레이터 (Orchestrator)는 의도적으로 요약 정보만 보유하며, 상세 내용은 각 탭에 그대로 둠
- 통신은 전체 문장이 아닌 구조화된 방식으로 압축하며, 컴팩트 (compact)를 넘나드는 기억은 파일 (todo.md)로 분리
화려한 메커니즘은 아니지만, "하나의 세션에 모든 것을 몰아넣지 않는다"라는 발상 하나만으로 개발과 업무를 병행하며 오랫동안 지속할 수 있게 되었습니다. 이와 유사하게 컨텍스트 (Context)의 벽에 부딪히고 있는 분들에게 도움이 된다면 좋겠습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기