본문으로 건너뛰기

"dotfiles" 태그로 연결된 2개 게시물개의 게시물이 있습니다.

모든 태그 보기

CCS 설정과 라우팅

· 약 14분

1편의 잔상

Claude Code Switcher (CCS) — 프로바이더 전환, 멀티 계정, 로컬 모델까지 한 명령으로표면의 글이었어요. CCS가 등장했는지, 무엇을 풀고 있는지, 누구에게 언제 맞고 안 맞는지를 다뤘어요. 그 글의 한 단락을 그대로 옮기면 이렇습니다.

CCS는 Claude Code가 환경변수 두 개로 정의되는 도구라는 사실을 그대로 받아들인 도구다. 그 두 변수를 프로파일 이라는 단위로 정리하고, OAuth 우회와 다중 계정까지 한 명령 표면 위로 올려놓았다.

2편은 그 표면 아래를 다룹니다. ~/.ccs/ 한 디렉토리에 무엇이 어떻게 사는지, config.yaml의 어느 키가 무엇을 결정하는지, instance와 shared가 어떻게 갈리는지, 로컬 프록시는 정확히 어떤 변환을 거치는지 살펴봅니다. 1편에서 표 한 줄로만 끝낸 부분을 시스템의 그림으로 풀어 봅니다.

이 글의 모든 디렉토리/키 이름은 사용자 머신의 실제 CCS 설치를 직접 들여다본 결과입니다. 토큰/세션 같은 민감한 값은 보지 않고 구조만 인용합니다.

~/.ccs/ 한 장 지도

CCS가 머신에 자리 잡으면 홈 디렉토리에 ~/.ccs/ 한 폴더가 생깁니다. 그 안의 트리가 사실상 CCS의 운영 모델 전부인데, 진짜 그림은 심볼릭 링크 체인입니다.

~/.claude/ ← 사용자의 기존 Claude Code 글로벌 (canonical)
├── settings.json
├── commands/, skills/, agents/, plugins/
└── projects/

~/.ccs/ ← CCS 영역
├── config.yaml ← 메인 설정 (YAML, 11K 정도)
├── .session-secret ← 세션 비밀 (64B, 절대 공유 금지)
├── .claude/ ← CCS *번들* (ccs.md, ccs-delegation 스킬 등 자체 주입)
├── cliproxy/bin/ ← CLIProxyAPI 바이너리 (자동 다운로드)
├── cache/, completions/, logs/

├── shared/ ← 사용자 정의 공유 영역
│ ├── settings.json → ~/.claude/settings.json (symlink)
│ ├── commands → ~/.claude/commands (symlink)
│ ├── skills → ~/.claude/skills (symlink)
│ ├── agents → ~/.claude/agents (symlink)
│ ├── plugins → ~/.claude/plugins (symlink)
│ └── context-groups/default/projects/ (실 디렉토리)

└── instances/ ← per-account 격리 영역
├── team/ ← 회사 Team account
│ ├── settings.json → ~/.ccs/shared/settings.json (symlink)
│ ├── commands → ~/.ccs/shared/commands (symlink)
│ ├── skills → ~/.ccs/shared/skills (symlink)
│ ├── agents → ~/.ccs/shared/agents (symlink)
│ ├── projects → ~/.ccs/shared/context-groups/default/projects/ (symlink)
│ ├── plugins/, .anthropic/, sessions/, session-env/ ← 실 디렉토리
│ ├── todos/, file-history/, shell-snapshots/, logs/ ← 실
│ ├── backups/, cache/, debug/, image-cache/, paste-cache/, plans/ ← 실
│ └── .claude.json, history.jsonl, .session-stats.json,
│ mcp-needs-auth-cache.json, policy-limits.json, remote-settings.json
└── enterprise/ ← 회사 Enterprise account (구조 동일)

여기서 눈여겨볼 것은 두 가지입니다. 하나는 2단계 symlink 체인입니다. instances/team/settings.json을 따라가면 ~/.ccs/shared/settings.json으로, 다시 ~/.claude/settings.json으로 이어집니다. 사용자의 기존 Claude Code 글로벌 설정이 모든 인스턴스에 자동 반영된다는 뜻이며, 한 번 만든 슬래시 커맨드, 스킬, MCP가 team과 enterprise 양쪽에서 살아 있는 이유도 여기에 있습니다.

다른 하나는 projects/만 체인이 한 단계라는 점입니다. 대화 히스토리는 instances/team/projects에서 ~/.ccs/shared/context-groups/default/projects/(실 디렉토리)까지만 가고 ~/.claude/projects/까지는 이어지지 않습니다. CCS가 자기 context-group 단위로 대화를 모아 사용자의 평소 Claude Code 대화와 자연히 분리해 두는 것으로, 회사 대화가 개인용 Claude Code에 섞이지 않도록 의도한 끊음입니다.

이 2단계 체인이 이중 구조라고 표현했던 것의 실제 메커니즘이며, 1편의 "공유 vs 분리"가 단일 축이 아니라 어느 단위까지 체인을 잇느냐의 문제였다는 뜻입니다. 5~6절에서 이 단위들을 풀어 봅니다.

config.yaml의 9개 영역

설정의 거의 전부는 ~/.ccs/config.yaml 한 파일에 모입니다. JSON이 아니라 YAML인 점이 의외인데, 사람이 손으로 읽고 고치기 좋은 쪽을 택한 것으로 보입니다.

키 구조만 추리면 9개 영역으로 나뉩니다.

version: # 메타
default: # 기본 프로파일

accounts: # *instance 단위* 메타
team:
context_mode: # isolated | shared
context_group: # shared 일 때 그룹 이름
continuity_mode: # 추가 공유 정책
enterprise:
...

profiles: # 프로바이더 프로파일 (glm, kimi, ollama, ...)

cliproxy: # OAuth 프록시 백엔드 설정
backend:
oauth_accounts:
providers:
routing:
strategy: # round-robin / fill-first
session_affinity:
session_affinity_ttl:

proxy: # 로컬 OpenAI-호환 프록시
profile_ports: # 프로파일별 포트 매핑
routing:
longContextThreshold:

cliproxy_server: # 원격/로컬 CLIProxy 서버
remote: { enabled, host, protocol, auth_token }
fallback: { enabled, auto_start }
local: { port, auto_start }

logging: # 로그 회전·보관
preferences: # theme, telemetry, auto_update

websearch: # ★ 8개 fallback 프로바이더
providers:
exa, tavily, brave, searxng, duckduckgo, gemini, opencode, grok

# --- 통합 (Claude Code 외부 도구들) ---
copilot: # GitHub Copilot 라우팅
cursor: # Cursor 통합 (ghost_mode 포함)
channels: # Telegram/Discord/iMessage
thinking: # opus/sonnet/haiku 별 thinking 기본값
global_env: # DISABLE_TELEMETRY 등 전역 env 자동 주입

이 9개를 한 번 훑으면 CCS가 단순 스위처가 아니라 Claude Code 주변의 운영 표면 전체를 흡수해 가고 있다는 인상이 분명해집니다. 12절에서 이 중 안 알려진 부분을 따로 추립니다.

Instance 시스템, symlink와 실 상태의 갈림

instances/<name>/ 디렉토리는 언뜻 보면 Claude Code의 모든 상태를 담은 통 같지만, ls -la로 열어 보면 절반은 shared/로의 symlink이고 나머지 절반만 인스턴스별 실 상태입니다. 둘을 갈라 두면 멘탈 모델이 명확해집니다.

먼저 모든 인스턴스가 공유하는 symlink 항목입니다.

항목가는 곳
settings.json~/.ccs/shared/settings.json~/.claude/settings.json
commands/~/.ccs/shared/commands~/.claude/commands/
skills/~/.ccs/shared/skills~/.claude/skills/
agents/~/.ccs/shared/agents~/.claude/agents/
projects/~/.ccs/shared/context-groups/default/projects/ (한 단계만)

settings.json의 최상위 키 9개는 체인을 따라가 본 결과 다음과 같습니다. 사용자의 기존 ~/.claude/settings.json 스키마 그대로입니다.

alwaysThinkingEnabled
enabledPlugins
env
extraKnownMarketplaces
hooks
permissions
skipAutoPermissionPrompt
skipDangerousModePermissionPrompt
statusLine

다음은 인스턴스별로 격리된 실 상태입니다.

분류항목
인증 (절대 안 섞임).anthropic/ (OAuth), .claude.json (사용자 메타)
세션/실행sessions/, session-env/, shell-snapshots/, history.jsonl
작업 상태todos/, file-history/, plans/ (team만), backups/
로깅/캐시logs/, cache/, debug/, image-cache/, paste-cache/
플러그인plugins/ — 인스턴스별 (claude-hud 같은 설치)
메타/정책policy-limits.json, remote-settings.json, mcp-needs-auth-cache.json, .session-stats.json
외부 통합.omc/ (oh-my-claudecode 상태)

한 가지 눈에 띄는 사실은 이 글의 plan 파일이 instances/team/plans/ccs-hashed-llama.md에 있다는 점입니다(이 항목은 real이고 symlink가 아닙니다). 이 글을 쓰는 동안 사용자의 Claude Code 세션이 team 인스턴스에서 돌고 있었다는 뜻입니다. CCS는 보이지 않게 CLAUDE_CONFIG_DIR 같은 환경변수로 인스턴스 컨텍스트를 끼워 넣습니다.

enterprise 인스턴스는 team보다 가볍게 비어 있는 상태였는데, 활성 사용 중인지 아닌지가 디렉토리 충실도에 그대로 드러납니다. CCS는 인스턴스를 만들 때 symlink와 빈 디렉토리만 만들어 두고, 실제로 사용해야 실 데이터가 채워지는 lazy 모델입니다.

shared/와 context-groups로 1편의 빈칸 메우기

1편의 "설정/대화는 공유하면서 계정만 분리하고 싶다면" 단락의 진짜 답이 여기 있습니다. CCS는 이 시나리오를 shared/라는 별도 영역과 context_mode/context_group 설정으로 정식 지원합니다.

config.yamlaccounts: 섹션:

accounts:
team:
context_mode: isolated # 또는 shared
context_group: default # shared 일 때만 의미
continuity_mode: deeper # 또는 default
enterprise:
context_mode: shared
context_group: default
continuity_mode: deeper

세 키의 역할:

context_mode: isolated | shared는 인스턴스가 자기 디렉토리에만 사는지, 같은 그룹의 다른 인스턴스와 공유하는지를 정합니다. context_groupshared일 때 쓸 그룹 이름이며, ~/.ccs/shared/context-groups/<group>/가 그 그룹의 공유 데이터 자리입니다. continuity_mode: deeper는 공유의 깊이를 정하며, deeper는 더 많은 디렉토리를 공유 대상에 포함합니다.

이 셋을 조합해 보면 1편에서 "공유 vs 분리"라고 단순화한 구도가 사실 세 축의 조합임이 드러납니다.

무엇이 공유 가능하고 무엇이 항상 격리되는가

CCS의 공식 docs는 이 부분을 명시해 두지 않습니다.

CCS only shares workspace context paths (project/session context files). It does not merge or copy authentication credentials between accounts.

docs/session-sharing-technical-analysis.md

요약 표:

분류대상동작
shared (조건부)session-env/shared + deeper 일 때 그룹 안에서 공유
shared (조건부)file-history/동일
shared (조건부)shell-snapshots/동일
shared (조건부)todos/동일
항상 격리.anthropic/ (OAuth)인증은 어떤 모드에서도 인스턴스별
항상 격리인증 토큰 / 자격증명동일
수동 공유commands/, skills/, agents/, plugins/shared/<dir>/에 두면 모든 인스턴스가 사용

인증은 어떤 경우에도 섞이지 않습니다. 한 인스턴스의 OAuth 토큰이 실수로 다른 인스턴스의 요청에 흘러갈 일이 구조적으로 없으므로, 회사 환경에서 가장 큰 사고 가능성을 시스템 자체가 막아 둡니다.

ccs -r (resume)는 현재 활성 lane만 이어가고, ccs <account> -r은 그 인스턴스의 lane만 이어갑니다. 두 인스턴스 모두 다른 continuity 인벤토리를 가질 수 있다는 점을 의식하고 운영해야 합니다.

4가지 진입점: Target Adapter System

ccs가 Claude Code만 호출하는 게 아닙니다. CCS는 runtime 자체를 바꾸는 4가지 바이너리를 노출합니다.

바이너리runtime
ccsClaude Code (기본)
ccsd / ccs-droidFactory Droid
ccsx / ccs-codexCodex CLI (네이티브)
ccsxpCodex CLI + CLIProxy 프로바이더 오버라이드

내부적으로는 각 바이너리가 CCS_INTERNAL_ENTRY_TARGET 환경변수를 세팅한 후 target resolver에 위임합니다. resolver의 우선순위:

  1. CLI 플래그 (--target)
  2. 진입 바이너리 자체
  3. argv[0] 이름 검출
  4. 프로파일별 config
  5. 기본값

이 추상화 덕분에 "Claude Code로 GLM 돌리기"와 "Droid로 GLM 돌리기"가 같은 명령 표면 위에서 가능합니다(ccs glm vs ccs --target droid glm). Codex의 경우 자기 ~/.codex/ 상태를 별도 보존하려고 환경변수만 임시로 덮어쓰는 식으로 신경을 더 씁니다.

로컬 프록시 (127.0.0.1:포트)의 5단계 흐름

ccs glm 같은 OpenAI-호환 프로바이더 명령이 들어오면 다음 5단계를 거칩니다(docs/openai-compatible-providers.md 정리).

  1. 127.0.0.1에서 그 프로파일의 로컬 포트 바인딩 (포트는 proxy.profile_ports에서 할당)
  2. Claude Code가 보내는 Anthropic 형식 /v1/messages 요청 수신
  3. OpenAI chat-completions 형식으로 변환
  4. 업스트림 프로바이더로 포워드
  5. 스트리밍 응답을 다시 Anthropic SSE로 역변환해 Claude Code에 돌려줌

정리하면 Claude Code는 자기가 Anthropic과 이야기하는 줄 알고, 변환은 프록시 한 점에서만 일어나므로 디버깅 지점도 한 곳으로 모입니다.

Anthropic-호환 엔드포인트는 프록시를 우회합니다. https://api.anthropic.com이나 Z.AI의 /api/anthropic으로 가는 요청은 변환이 필요 없어 그냥 직통하고, 이 분기 덕분에 GLM 사용은 추상화 비용이 거의 0입니다.

CLIProxyAPI 서버 자체는 기본적으로 port 8317에서 동작하는데, cliproxy_server.local.port로 바꿀 수 있고 remote.host를 켜면 다른 머신의 CLIProxy를 쓸 수도 있습니다.

시나리오 라우팅 4종

CCS가 수동으로 프로바이더를 명시하지 않아도 자동으로 라우팅하는 4가지 시나리오가 있습니다.

시나리오트리거
background요청이 Haiku를 포함 (가벼운 백그라운드 작업)
thinkAnthropic extended thinking 활성화
longContext토큰 추정치가 임계값 초과 (proxy.routing.longContextThreshold)
webSearchweb_search 툴 호출

config.yamlproxy.routing.longContextThreshold 키가 실제로 있어 임계값을 손으로 조절할 수 있습니다. 자동화의 깊이는 워크로드에 따라 잘 맞기도 하고 헷갈리기도 하므로, 처음부터 라우팅 룰을 복잡하게 짜기보다 한 주 동안 단순하게 운영한 뒤 패턴을 보고 추가하는 편이 낫습니다(1편 팁 7절 참조).

CLIProxy 서브시스템, OAuth 프로바이더의 자리

API 키가 없는 OAuth 기반 프로바이더(Gemini, GitHub Copilot, AWS Kiro 등)는 별도 서브시스템이 책임집니다. ~/.ccs/cliproxy/bin/에 자동 다운로드되는 CLIProxyAPI 바이너리가 그 핵심입니다.

이 서브시스템의 Account manager는 OAuth 토큰의 lifecycle(발급, 갱신, 만료)을 관리하고, Quota manager와 Quota fetcher는 각각 프로바이더별 quota 추적과 자동 failover, 사용량 실시간 동기화를 맡습니다. Auth handler는 Anthropic, Gemini, Copilot, Kiro 등 각 프로바이더의 OAuth 흐름을 처리합니다. Model catalogs에는 프로바이더별 모델 목록과 compatibility 가드가 있으며, codex-plan-compatibility.ts 같은 파일이 plan별로 맞지 않는 모델 조합을 막아 줍니다. Hybrid quota strategy는 round-robin 또는 fill-first(cliproxy.routing.strategy) 방식으로 같은 프로바이더의 여러 OAuth 계정을 어떻게 분산할지 정합니다.

config.yamlcliproxy.oauth_accounts가 등록된 OAuth 계정 목록을 담는데, 이 부분은 토큰을 포함하므로 git에 올리면 안 되는 영역입니다. 다음 11절에서 정리합니다.

파일 관리 실전: symlink가 dotfiles의 답입니다

2절의 symlink 체인을 보고 나면 자연스레 이런 질문이 나옵니다. 내 CCS 환경을 다른 머신에 어떻게 옮기는지, ~/.ccs/를 통째로 git에 넣어도 되는지 궁금해집니다. ~/.ccs/를 versioning하지 않고 ~/.claude/를 versioning해 CCS가 그쪽을 끌어들이게 하면 됩니다.

멘탈 모델: ~/.claude/가 source of truth

체인의 시작점은 ~/.claude/입니다. CCS의 shared/와 instance들은 그쪽으로 가는 포인터일 뿐입니다. 따라서 dotfiles의 운용 원칙은 하나로 압축됩니다. ~/.claude/를 git으로 관리해 두면, 새 머신에 CCS를 설치하더라도 shared/만 같은 symlink로 다시 걸어 모든 인스턴스가 같은 환경을 보게 됩니다.

git에 올리기 좋은 자산 (~/.claude/ 쪽)

~/.claude/commands/, ~/.claude/skills/, ~/.claude/agents/에는 각각 슬래시 커맨드, 스킬, 에이전트 정의가 들어가므로 git에 올리기 좋습니다. 글로벌 메모리인 ~/.claude/CLAUDE.md도 관리 대상입니다. ~/.claude/settings.json은 토큰을 직접 적어 두지 않은 경우에만 포함해야 하며, env 필드에 비밀이 있다면 settings.local.json 패턴으로 분리합니다. 실 디렉토리인 ~/.ccs/shared/context-groups/default/에서는 projects/를 빼고 CCS의 그룹 메타만 versioning하는 식으로 관리합니다.

~/.ccs/.claude/ (CCS 번들)은 npm 패키지 일부라 install 시 자동으로 따라옵니다. 직접 versioning 할 필요 없습니다.

절대 git 금지 (자격증명, 세션, 트랜스크립트)

~/.ccs/config.yaml에는 OAuth 토큰/refresh_token이 있으므로 머신별로 보관하고, 세션 비밀인 ~/.ccs/.session-secret도 제외합니다. ~/.ccs/instances/*/.anthropic/의 Anthropic OAuth와 계정 식별자를 포함한 ~/.ccs/instances/*/.claude.json도 올리면 안 됩니다.

~/.ccs/instances/*/sessions/, session-env/에는 세션과 세션별 env가 있어 토큰 환경변수가 들어갈 가능성이 있습니다. ~/.ccs/instances/*/history.jsonl, file-history/에는 명령과 파일 history가 남아 회사 코드의 흔적이 섞일 수 있습니다. 대화 히스토리 본체~/.ccs/shared/context-groups/*/projects/도 PII나 회사 코드를 포함할 가능성이 있으므로 제외합니다. 바이너리와 휘발성 데이터가 담긴 ~/.ccs/cliproxy/, cache/, logs/, 평소 Claude Code 대화 히스토리인 ~/.claude/projects/도 git에 올리지 않습니다.

새 머신 부트스트랩 4단계

dotfiles가 ~/.claude/만 들고 있다고 할 때 새 머신에서 같은 환경을 만들려면:

  1. dotfiles를 clone해 ~/.claude/를 채웁니다.
  2. npm install -g @kaitranntt/ccs로 CCS를 설치합니다.
  3. ccs config로 초기 ~/.ccs/ 골격을 생성합니다.
  4. symlink를 재구성합니다. ~/.ccs/shared/{commands,skills,agents,plugins,settings.json}~/.claude/의 동명 항목으로 link하며, 한 줄 스크립트로 자동화하기를 권장합니다.
ln -sf ~/.claude/commands ~/.ccs/shared/commands
ln -sf ~/.claude/skills ~/.ccs/shared/skills
ln -sf ~/.claude/agents ~/.ccs/shared/agents
ln -sf ~/.claude/plugins ~/.ccs/shared/plugins
ln -sf ~/.claude/settings.json ~/.ccs/shared/settings.json

이 다섯 줄로 새 머신에서도 1편의 instance들이 평소 환경을 그대로 봅니다. OAuth 인증만 각 인스턴스에서 새로 하면 끝입니다.

격리의 운영 이점

한 인스턴스가 망가져도(plugins/ 깨짐, policy-limits.json 누락 등) 다른 인스턴스는 그대로이므로 단일 장애점이 줄어드는 부수 효과가 있습니다. 인스턴스 단위 백업에는 tar czf team-backup.tgz -C ~/.ccs/instances team을 씁니다. symlink는 그대로 보존하며, 옵션 -h를 추가하면 링크가 가리키는 파일까지 따라가 archive합니다. 복원할 때 같은 dotfiles 환경을 가정하므로 보통은 symlink를 그대로 두는 편이 의미 있습니다.

config.yaml의 비공식 영역들

CCS의 README가 강조하지 않지만 config.yaml 키만 봐도 드러나는, 단순 스위처라기엔 풍부한 통합이 여럿입니다.

가장 눈에 띄는 것은 WebSearch fallback입니다. websearch.providers 아래에 exa, tavily, brave, searxng, duckduckgo, gemini, opencode, grok 여덟 곳이 등록돼 있어, Claude Code의 WebSearch가 Anthropic 밖 8개 프로바이더로 넘어갈 수 있습니다. docs/websearch.md가 별도 문서로 있을 만큼 일급 통합입니다. 외부 에디터/툴 통합도 넓어서, copilot.account_type, rate_limit, model로 GitHub Copilot을 프로바이더 한 슬롯처럼 다루고 cursor.ghost_mode/cursor.port로 Cursor의 ghost-mode까지 건드립니다. channels.selected, channels.unattended를 통해 Telegram, Discord, iMessage로 Claude에게 일을 시키는 통합도 있습니다(1편의 free-claude-code 텔레그램 봇 사례와 같은 결입니다).

세밀한 정책 키도 있습니다. thinking.tier_defaults는 opus, sonnet, haiku별로 thinking 기본 모드를 다르게 잡게 해주고(Opus는 항상 deep thinking, Haiku는 끄는 식), global_env.envDISABLE_BUG_COMMAND, DISABLE_ERROR_REPORTING, DISABLE_TELEMETRY는 보안, 프라이버시에 민감한 회사 환경에서 텔레메트리를 자동으로 꺼줍니다. cliproxy.routing.session_affinitysession_affinity_ttl은 같은 세션을 같은 OAuth 계정으로 sticky하게 묶는 정책입니다. 이런 영역들은 CCS가 단순 스위처에서 운영 플랫폼으로 진화 중이라는 신호입니다.

한 줄로

1편이 CCS의 표면이었다면 2편은 안쪽입니다. 안쪽을 보고 나면 "프로파일로 옮겨 다닌다"는 1편의 단순한 설명이 사실은 instance 격리, shared 공유, OAuth 분리, OpenAI와 Anthropic 사이의 변환, 시나리오 라우팅, 여러 통합 지점이라는 축들로 짜여 있다는 게 보입니다. 추상화가 두껍다는 1편의 단점 평가는 그래서 정확하지만, 그 두께가 풀고 있는 문제도 같이 두껍다는 사실이 디렉토리 한 통에 적나라하게 드러납니다.

다음 편이 있다면 두 갈래 중 하나일 가능성이 커요. proxy.routing 시나리오를 실제 워크로드로 맞춰 보는 실측 운영 글이거나, CCS와 dotfiles, Anthropic Workspaces를 엮는 멀티-머신 계정 운영 글일 거예요. 어느 쪽이 먼저 나올지는 다음 글에서 정할게요.

참고

나의 Neovim 설정 전체 공개

· 약 5분

이 글은 Neovim 시리즈의 마지막 글이에요.

  1. Neovim 입문: Vim을 넘어서는 첫걸음
  2. Neovim 중급: 생산성을 높이는 기능들
  3. Neovim 고급: 플러그인과 LSP로 IDE처럼 쓰기
  4. 나의 Neovim 설정 전체 공개 ← 현재 글

설정 철학

설정은 Lua 기반으로 VimScript 대신 Lua로 모두 작성하고, 모듈화를 위해 기능별로 파일을 분리해서 관리합니다. 최소주의에 따라 꼭 필요한 플러그인만 23개로 유지하며, 일관된 키매핑을 위해 커스텀 keyMapper 유틸리티로 통일했습니다.

디렉토리 구조

~/.config/nvim/
├── init.lua # 진입점 (1줄)
├── lazy-lock.json # 플러그인 버전 고정
└── lua/
├── config/
│ ├── init.lua # config 모듈 진입점
│ ├── globals.lua # 전역 변수 (leader 키 등)
│ ├── options.lua # Neovim 옵션
│ └── keymaps.lua # 글로벌 키매핑
├── plugins/
│ ├── alpha.lua # 시작 화면
│ ├── comment.lua # 주석 토글
│ ├── conform.lua # 코드 포매팅
│ ├── indent-blankline.lua # 들여쓰기 가이드
│ ├── kanagawa.lua # 컬러스킴
│ ├── lsp.lua # LSP 설정
│ ├── lualine.lua # 상태줄
│ ├── neo-tree.lua # 파일 탐색기
│ ├── nvim-autopairs.lua # 자동 괄호
│ ├── nvim-cmp.lua # 자동완성
│ ├── nvim-treesitter.lua # 구문 하이라이팅
│ ├── nvim-ufo.lua # 코드 폴딩
│ ├── render-markdown.lua # 마크다운 렌더링
│ ├── telescope.lua # 퍼지 파인더
│ └── vim-floaterm.lua # 플로팅 터미널
└── utils/
└── keyMapper.lua # 키매핑 헬퍼

핵심은 init.lua가 단 1줄이라는 점입니다.

require("config")

config/init.lua에서 globals, options, keymaps, lazy.nvim 순서로 로드합니다.

핵심 옵션

-- lua/config/options.lua
opt = vim.opt

-- 2칸 탭
opt.tabstop = 2
opt.shiftwidth = 2
opt.softtabstop = 2
opt.expandtab = true
opt.smartindent = true
opt.wrap = false

-- 검색
opt.incsearch = true
opt.ignorecase = true
opt.smartcase = true -- 대문자가 포함되면 대소문자 구분

-- 줄 번호
opt.number = true
opt.relativenumber = true -- 상대 줄 번호 (이동에 유용)

-- 기타
opt.termguicolors = true
opt.signcolumn = "yes"
opt.scrolloff = 10 -- 커서 위아래 10줄 여유
opt.mouse:append("a")

-- 클립보드
opt.clipboard = "unnamedplus" -- y/p가 시스템 클립보드와 자동 동기화

-- 마크다운 전용 설정
vim.api.nvim_create_autocmd("FileType", {
pattern = "markdown",
callback = function()
vim.opt_local.wrap = true -- 줄 바꿈 활성화
vim.opt_local.linebreak = true -- 단어 단위로 줄 바꿈
vim.opt_local.conceallevel = 2 -- 문법 마커 숨기기
end,
})

relativenumber는 처음에는 어색하지만, 5j, 12k 같은 상대 이동을 할 때 줄 수를 바로 알 수 있어서 매우 편합니다.

시스템 클립보드 연동

기본 Neovim에서 y는 내부 무명 레지스터에만 저장되고 macOS 시스템 클립보드와는 분리돼 있습니다. 시스템 클립보드로 복사하려면 매번 "+y처럼 접두사를 붙여야 하는데, 한글 IME 상태나 tmux 안에서 "(Shift+') 입력이 종종 씹혀서 번거롭습니다.

opt.clipboard = "unnamedplus" 한 줄로 해결됩니다. 비주얼 모드에서 y만 눌러도 macOS 클립보드에 바로 들어가고, 다른 앱에서 Cmd+C 한 내용도 p로 바로 붙여넣어집니다.

LazyVim 배포판에서는 기본값이지만, lazy.nvim만 직접 쓰는 수동 구성에서는 이 한 줄을 빼먹기 쉽습니다.

값 선택

의미
unnamed* 레지스터 (Linux X11의 primary selection)
unnamedplus+ 레지스터 (시스템 클립보드) — 권장

macOS에서는 *+가 실질적으로 같지만, 크로스 플랫폼 호환성 측면에서 unnamedplus가 표준입니다. LazyVim 기본값도 이것.

확인

:set clipboard?

clipboard=unnamedplus가 출력되면 OK.

:checkhealth provider

→ clipboard provider 섹션에 에러가 없어야 합니다.

안 될 때

  • which pbcopy/usr/bin/pbcopy 확인 (macOS 기본 탑재).
  • Neovim 빌드에 클립보드 지원이 있는지: :echo has('clipboard')1.
  • tmux 안에서만 안 되는 경우, ~/.tmux.confset -g set-clipboard on. 다만 macOS 로컬 tmux + pbcopy 조합은 대부분 이 설정 없이도 동작합니다.

keyMapper 유틸리티

모든 키매핑에 일관되게 noremapsilent를 적용하기 위해 만든 헬퍼입니다.

-- lua/utils/keyMapper.lua
local keyMapper = function(from, to, mode, opts)
local options = { noremap = true, silent = true }
mode = mode or "n"

if opts then
options = vim.tbl_extend("force", options, opts)
end

vim.keymap.set(mode, from, to, options)
end

return { mapKey = keyMapper }

사용법:

local mapKey = require("utils.keyMapper").mapKey

mapKey("<leader>e", ":Neotree toggle<cr>") -- Normal 모드 (기본)
mapKey("<", "<gv", "v") -- Visual 모드 지정

전체 키매핑

글로벌 키매핑

동작모드
SpaceLeader 키-
<leader>eNeo-tree 파일 탐색기 토글N
<leader>h검색 하이라이트 제거N
Ctrl-h/j/k/l분할 창 이동N
< / >들여쓰기 유지하며 인덴트V
Ctrl-;플로팅 터미널 토글N

Telescope 키매핑

동작
<leader>ff파일 이름 검색
<leader>fg파일 내용 검색 (grep)
<leader>fb버퍼 목록
<leader>fh도움말 검색

LSP 키매핑

동작
K호버 문서
gd정의로 이동
<leader>ca코드 액션

플러그인 전체 목록 (23개)

핵심

플러그인역할
lazy.nvim플러그인 매니저
telescope.nvim퍼지 파인더 (파일/텍스트/버퍼 검색)
neo-tree.nvim사이드바 파일 탐색기
nvim-cmp자동완성 엔진
nvim-lspconfigLSP 클라이언트 설정
mason.nvim언어 서버/포매터 설치 관리
nvim-treesitter구문 파싱 & 하이라이팅
conform.nvim저장 시 자동 포매팅

자동완성 소스

플러그인소스
cmp-nvim-lspLSP 자동완성
cmp-buffer버퍼 텍스트
cmp-path파일 경로
cmp_luasnip스니펫
LuaSnip스니펫 엔진
friendly-snippetsVS Code 스니펫 모음

UI & 외관

플러그인역할
kanagawa.nvim컬러스킴 (dragon 테마)
lualine.nvim하단 상태줄
alpha-nvim시작 화면 대시보드
nvim-web-devicons파일 아이콘
indent-blankline.nvim들여쓰기 시각 가이드

편집 보조

플러그인역할
Comment.nvimgcc로 주석 토글
nvim-autopairs괄호/따옴표 자동 닫기
nvim-ufoLSP 기반 코드 폴딩
vim-floaterm플로팅 터미널
render-markdown.nvim마크다운 실시간 렌더링

테마: Kanagawa Dragon

Kanagawa의 Dragon 변형을 사용합니다. 일본 전통 색상에서 영감을 받은 다크 테마로, 눈의 피로가 적습니다.

커스터마이징 포인트:

overrides = function(colors)
local theme = colors.theme
return {
-- 플로팅 윈도우 배경 투명화
NormalFloat = { bg = "none" },
FloatBorder = { bg = "none" },
FloatTitle = { bg = "none" },

-- Telescope UI 커스터마이징
TelescopePromptNormal = { bg = theme.ui.bg_p1 },
TelescopeResultsNormal = { fg = theme.ui.fg_dim, bg = theme.ui.bg_m1 },
TelescopePreviewNormal = { bg = theme.ui.bg_dim },

-- 자동완성 팝업
Pmenu = { fg = theme.ui.shade0, bg = theme.ui.bg_p1 },
PmenuSel = { fg = "NONE", bg = theme.ui.bg_p2 },
}
end,
theme = "dragon",

상태줄(lualine)은 Gruvbox 테마를 사용해서 본문과 미묘하게 다른 톤을 줍니다.

마크다운 작성 환경

블로그를 Neovim으로 작성하기 때문에 마크다운 환경을 신경 썼습니다.

마크다운 환경에서는 render-markdown.nvim으로 헤딩, 코드 블록, 테이블, 체크박스를 시각적으로 렌더링하고, 줄 바꿈 활성화를 위해 마크다운 파일에서만 wrap = true로 설정합니다. conceallevel 2**bold** 같은 마커를 숨기고 bold 형태로 표시합니다.

LSP 구성

Mason으로 3개 언어 서버를 관리합니다.

서버언어포매터
lua_lsLuastylua
ts_lsTypeScript/JavaScriptprettierd
goplsGogofmt (내장)

새 언어를 추가하려면:

  1. :Mason에서 언어 서버 설치
  2. lsp.luaensure_installed에 추가
  3. conform.lua에 포매터 추가 (필요 시)

정리

이 설정은 계속 발전 중이에요. Neovim의 장점은 제 워크플로우에 맞게 모든 것을 조정할 수 있다는 점이에요. 처음에는 남의 설정을 복사하더라도, 하나씩 이해하면서 자기 것으로 만들어가는 과정이 중요해요.

가장 좋은 Neovim 설정은 제가 이해하고 있는 설정입니다.