버전 게이트, 멱등성 패턴, 설정 레이어 규율 — 사용자 설정을 깨지 않고 업그레이드.
Claude Code의 마이그레이션 시스템은 데이터베이스 스키마 마이그레이션이 아닙니다. 마이그레이션 테이블도, 롤백도, 프레임워크도 없습니다. 대신, 모든 마이그레이션 함수는 설계상 멱등이며, 단일 버전 번호로 보호됩니다.
main.tsx에서 CURRENT_MIGRATION_VERSION = 11로 정의되며, 전체 동기 마이그레이션 블록을 게이팅합니다:
const CURRENT_MIGRATION_VERSION = 11;
function runMigrations(): void {
if (getGlobalConfig().migrationVersion !== CURRENT_MIGRATION_VERSION) {
migrateAutoUpdatesToSettings();
migrateBypassPermissionsAcceptedToSettings();
migrateEnableAllProjectMcpServersToSettings();
resetProToOpusDefault();
migrateSonnet1mToSonnet45();
migrateLegacyOpusToCurrent();
migrateSonnet45ToSonnet46();
migrateOpusToOpus1m();
migrateReplBridgeEnabledToRemoteControlAtStartup();
// feature-gated migrations...
saveGlobalConfig(prev => ({ ...prev, migrationVersion: CURRENT_MIGRATION_VERSION }));
}
migrateChangelogFromConfig().catch(() => {});
}
핵심 구조:
migrationVersion !== CURRENT_MIGRATION_VERSION일 때만 동기 블록 진입migrateChangelogFromConfig()는 게이트 밖에서 fire-and-forget으로 실행migrationVersion === CURRENT_MIGRATION_VERSION이면 전체 동기 블록이 건너뛰어지고, 비동기 마이그레이션(migrateChangelogFromConfig)만 실행됩니다. 다운그레이드하면 이미 올라간 마이그레이션 버전이 남아 재실행을 방지하여 미묘한 설정 불일치가 발생할 수 있습니다.
11개 동기 마이그레이션 + 1개 비동기 마이그레이션의 전체 카탈로그:
| # | 함수명 | 카테고리 | 수행 내용 | 멱등성 가드 |
|---|---|---|---|---|
| 1 | migrateAutoUpdatesToSettings |
설정 프로모션 | 자동 업데이트 설정을 GlobalConfig에서 userSettings로 이동 | 완료 플래그 |
| 2 | migrateBypassPermissionsAcceptedToSettings |
설정 프로모션 | 권한 우회 승인 플래그를 userSettings로 이동 | 완료 플래그 |
| 3 | migrateEnableAllProjectMcpServersToSettings |
설정 프로모션 | 프로젝트 MCP 서버 활성화 플래그를 userSettings로 이동 | 완료 플래그 |
| 4 | resetProToOpusDefault |
원샷 리셋 | Pro 사용자의 기본 모델을 Opus로 리셋 | 완료 플래그 |
| 5 | migrateSonnet1mToSonnet45 |
모델 앨리어스 | Sonnet 1M 앨리어스를 Sonnet 4.5로 업그레이드 | 자기멱등 데이터 체크 |
| 6 | migrateLegacyOpusToCurrent |
모델 앨리어스 | 레거시 Opus 식별자를 현재 버전으로 업그레이드 | 자기멱등 데이터 체크 |
| 7 | migrateSonnet45ToSonnet46 |
모델 앨리어스 | Sonnet 4.5를 Sonnet 4.6으로 업그레이드 + 인메모리 상태 업데이트 | 자기멱등 데이터 체크 |
| 8 | migrateOpusToOpus1m |
모델 앨리어스 | Opus를 Opus 1M으로 업그레이드 | 자기멱등 데이터 체크 |
| 9 | migrateReplBridgeEnabledToRemoteControlAtStartup |
설정 키 이름변경 | REPL bridge 설정 키를 remote control at startup으로 변경 | 자기멱등 데이터 체크 |
| 10-11 | feature-gated | 다양 | 기능 플래그 조건부 마이그레이션 | 다양 |
| 비동기 | migrateChangelogFromConfig |
비동기 파일 | 변경 로그를 GlobalConfig에서 별도 파일로 이동 | 파일 존재 여부 체크 |
데이터만 보고 "이미 완료되었는지" 판단할 수 없을 때 사용합니다. 마이그레이션 완료 후 GlobalConfig에 플래그를 기록합니다:
// 패턴 A — 완료 플래그로 멱등성 보장
function migrateAutoUpdatesToSettings(): void {
const config = getGlobalConfig()
if (config.migratedAutoUpdates) return // 이미 완료 — 건너뜀
const currentValue = config.autoUpdatesEnabled
if (currentValue !== undefined) {
saveUserSettings(prev => ({
...prev,
autoUpdatesEnabled: currentValue,
}))
}
// 환경 변수도 설정 — userSettings 쓰기는 다음 실행까지 효과 없으므로
process.env.CLAUDE_AUTO_UPDATES = String(currentValue)
saveGlobalConfig(prev => ({
...prev,
migratedAutoUpdates: true, // 완료 플래그 기록
}))
}
현재 데이터 값 자체가 "마이그레이션이 필요한지" 직접 알려줄 때 사용합니다. 별도 플래그가 불필요합니다:
// 패턴 B — 데이터 값이 직접 조건이 됨
function migrateSonnet45ToSonnet46(): void {
const settings = getUserSettings()
const model = settings.preferredModel
// 현재 값이 'sonnet-4.5'일 때만 마이그레이션 — 이미 '4.6'이면 건너뜀
if (model === 'claude-sonnet-4-5-20250514') {
saveUserSettings(prev => ({
...prev,
preferredModel: 'claude-sonnet-4-6-20250715',
}))
// 인메모리 런타임 상태도 업데이트
setCurrentModel('claude-sonnet-4-6-20250715')
}
}
패턴 A는 원본 값을 삭제하거나, 마이그레이션 후 원본과 결과가 동일할 수 있을 때 사용합니다 (예: 설정 프로모션 — 원본 키를 유지하면서 새 위치에 복사). 패턴 B는 값 자체가 "구 버전"인지 "신 버전"인지 명확히 구분될 때 사용합니다 (예: 모델 앨리어스 — sonnet-4.5는 구 버전, sonnet-4.6은 신 버전).
모든 모델 마이그레이션은 userSettings만 읽기/쓰기합니다. 병합된 설정은 절대 읽지 않습니다.
병합 설정을 읽으면 프로젝트 범위 모델 핀을 전역 기본으로 잘못 승격할 위험이 있습니다. 예를 들어:
.claude/settings.json에 preferredModel: "claude-opus-4-20250918"을 설정userSettings(전역)에 쓰면, 프로젝트 A 전용 설정이 모든 프로젝트의 기본값이 됨userSettings만 읽으면 전역 사용자 설정만 보이므로 이 문제가 발생하지 않습니다.
config.ts의 migrateConfigFields()는 디스크에서 설정을 읽을 때마다 실행됩니다. runMigrations()와는 별개로, 가장 오래된 스키마 변경을 처리하는 인메모리 변환 레이어입니다.
이 함수의 역할:
runMigrations()가 처리하지 않는 레거시 필드 호환성 유지runMigrations()는 프로세스 시작 시 한 번 실행되며 디스크에 쓰기를 합니다. migrateConfigFields()는 설정을 읽을 때마다 실행되며 디스크에 쓰지 않습니다. 전자는 "영구 마이그레이션"이고 후자는 "호환성 어댑터"입니다.
각 마이그레이션은 실행 시 분석 이벤트를 기록합니다:
| 마이그레이션 | 이벤트 | 추적 데이터 |
|---|---|---|
| 모델 앨리어스 업그레이드 | model_migrated |
이전 모델, 새 모델, 마이그레이션 함수명 |
| 설정 프로모션 | setting_promoted |
설정 키, 원본 위치, 대상 위치 |
| 원샷 리셋 | setting_reset |
리셋된 키, 이전 값 |
| 설정 키 이름변경 | setting_renamed |
이전 키, 새 키 |
이 이벤트들은 마이그레이션 도달률과 성공률을 모니터링하는 데 사용됩니다. 특정 마이그레이션이 예상보다 적게 실행되면 배포 문제나 다운그레이드를 의심할 수 있습니다.
userSettings만 읽기/쓰기, 병합 설정 사용 금지runMigrations()에 함수 호출 추가 — saveGlobalConfig 호출 전에 배치CURRENT_MIGRATION_VERSION 증가 — 필수: 버전을 올리지 않으면 기존 사용자에게 마이그레이션이 실행되지 않음userSettings 쓰기는 다음 실행까지 효과 없으므로, 현재 프로세스에서 즉시 적용이 필요하면 인메모리 상태도 업데이트// 새 마이그레이션 템플릿 — 패턴 B (자기멱등 데이터 체크)
function migrateOldValueToNewValue(): void {
const settings = getUserSettings() // userSettings만 — 병합 설정 금지!
if (settings.someKey === 'old-value') {
logEvent('setting_migrated', {
key: 'someKey',
from: 'old-value',
to: 'new-value',
})
saveUserSettings(prev => ({
...prev,
someKey: 'new-value',
}))
// 필요시 인메모리 상태도 업데이트
// setCurrentSomeKey('new-value')
}
}
CURRENT_MIGRATION_VERSION이 일치하면 전체 동기 블록이 건너뛰어지고, 비동기 마이그레이션만 실행userSettings만 읽기/쓰기 — 병합 설정을 읽으면 프로젝트 핀이 전역으로 승격되는 위험migrateConfigFields()는 디스크 읽기 시마다 인메모리 변환을 수행하는 호환성 어댑터로, runMigrations()와 별개migrateAutoUpdatesToSettings는 process.env를 설정 — userSettings 쓰기는 다음 실행까지 효과 없으므로 현재 프로세스에서 즉시 적용CURRENT_MIGRATION_VERSION 증가 필수Q1. migrationVersion === CURRENT_MIGRATION_VERSION일 때 어떻게 되나요?
runMigrations()의 if 문이 !==로 게이팅하므로, 버전이 일치하면 동기 블록 전체가 건너뛰어집니다. 게이트 밖의 migrateChangelogFromConfig()(비동기)만 실행됩니다.Q2. 모델 마이그레이션이 병합 설정 대신 userSettings를 읽는 이유는?
.claude/settings.json의 값을 포함합니다. 이를 읽어 전역 userSettings에 쓰면, 특정 프로젝트에서만 의도한 모델 핀이 모든 프로젝트의 기본값으로 승격됩니다.Q3. 인메모리 런타임 상태도 업데이트하는 마이그레이션은?
migrateSonnet45ToSonnet46은 userSettings에 새 모델을 쓰면서 동시에 setCurrentModel()로 인메모리 런타임 상태도 업데이트합니다. userSettings 쓰기만으로는 현재 실행 중인 프로세스에 즉시 반영되지 않기 때문입니다.Q4. 새 동기 마이그레이션을 추가할 때 필수인 것은?
CURRENT_MIGRATION_VERSION을 증가시키지 않으면 이미 이전 버전으로 마이그레이션된 사용자의 migrationVersion이 일치하여 새 마이그레이션이 실행되지 않습니다.Q5. migrateAutoUpdatesToSettings가 process.env를 설정하는 이유는?
saveUserSettings()는 디스크에 쓰지만, 현재 프로세스의 설정 캐시는 이미 로드된 상태입니다. process.env를 설정하면 현재 프로세스에서 즉시 새 값이 적용됩니다. 다음 실행 시에는 userSettings에서 올바른 값이 읽힙니다.