티스토리 뷰
Monaco Editor "TextModel got disposed" 에러 완벽 해결 가이드
pak2251 2025. 10. 27. 14:46작성일: 2025년 10월 27일
카테고리: React, Monaco Editor, 디버깅
난이도: 중급
TL;DR
- 문제:
TextModel got disposed before DiffEditorWidget model got reset에러 발생 - 원인: @monaco-editor/react의 DiffEditor가 props 변경 시 모델을 재생성하면서 dispose 충돌 발생
- 해결:
keepCurrentOriginalModel={true}+keepCurrentModifiedModel={true}props 추가 (단 2줄!) - 결과: 복잡한 cleanup 로직 없이 깔끔하게 해결, 코드 45% 감소
들어가며
imprun.dev는 Kubernetes 기반 서버리스 Cloud Function 플랫폼입니다. 웹 콘솔에서 함수 배포 히스토리 비교 기능을 구현하면서 Monaco Editor의 DiffEditor를 사용했는데, 다음과 같은 에러가 지속적으로 발생했습니다:
Uncaught Error: TextModel got disposed before DiffEditorWidget model got reset이 에러는:
- ❌ Dialog(모달)를 닫을 때마다 발생
- ❌ 파일이나 버전을 변경할 때 발생
- ❌ 사용자 경험을 크게 해침 (콘솔 에러 폭탄)
- ❌ 메모리 누수 가능성 존재
문제 상황: 언제, 왜 발생하나?
🔍 에러 발생 시나리오
// FunctionHistoryModal.tsx
function FunctionHistoryModal({ open, onOpenChange }) {
const [selectedFile, setSelectedFile] = useState<string | null>(null)
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DiffEditor
original={previousVersion?.files[selectedFile] || ''}
modified={currentVersion?.files[selectedFile] || ''}
language="typescript"
theme="vs-dark"
/>
</DialogContent>
</Dialog>
)
}
에러 발생 타이밍:
- 사용자가 파일을 선택 →
selectedFile변경 → DiffEditor props 변경 → 에러! - 사용자가 다른 버전 선택 →
previousVersion/currentVersion변경 → 에러! - 사용자가 모달 닫기 → DiffEditor unmount → 에러!
원인 분석: Monaco Editor의 내부 동작
📚 Monaco Editor의 Model 관리
Monaco Editor는 내부적으로 TextModel을 사용하여 코드를 관리합니다:
DiffEditor
├─ OriginalEditor
│ └─ TextModel (original)
└─ ModifiedEditor
└─ TextModel (modified)문제의 핵심:
@monaco-editor/react의 DiffEditor는 props가 변경되면 기존 모델을 dispose하고 새 모델을 생성합니다- 하지만 dispose 순서가 비동기적으로 처리되면서 race condition 발생:
1. TextModel.dispose() 시작 2. DiffEditorWidget이 아직 모델을 참조 중 3. TextModel이 먼저 dispose 완료 4. DiffEditorWidget이 dispose된 모델 접근 시도 → 💥 에러!
🔬 실제 에러 스택 트레이스
Error: TextModel got disposed before DiffEditorWidget model got reset
at lR.value (monaco-editor.js:388:50203)
at P._deliver (monaco-editor.js:7:2719)
at tl.dispose (monaco-editor.js:243:68815)
at commitHookEffectListUnmount (react-dom.js:9449:160)
React의 unmount phase에서 Monaco Editor의 내부 dispose 메커니즘과 충돌하는 것을 확인할 수 있습니다.
시도한 해결 방법들
❌ 시도 1: 수동 cleanup (실패)
useEffect(() => {
return () => {
if (editorRef.current) {
const editor = editorRef.current
const originalModel = editor.getOriginalEditor().getModel()
const modifiedModel = editor.getModifiedEditor().getModel()
// 모델 분리
editor.getOriginalEditor().setModel(null)
editor.getModifiedEditor().setModel(null)
// Editor dispose
editor.dispose()
// 모델 dispose
originalModel?.dispose()
modifiedModel?.dispose()
}
}
}, [])
결과: ❌ 여전히 에러 발생
- dispose 순서를 보장해도 @monaco-editor/react 내부에서 이미 dispose 시도
❌ 시도 2: setValue()로 수동 갱신 (실패)
useEffect(() => {
if (!editorRef.current) return
const originalModel = editor.getOriginalEditor().getModel()
const modifiedModel = editor.getModifiedEditor().getModel()
// props 변경 시 setValue()로 내용만 갱신 (모델 재생성 방지)
if (originalModel) originalModel.setValue(original)
if (modifiedModel) modifiedModel.setValue(modified)
}, [original, modified])
결과: ❌ 여전히 에러 발생
- DiffEditor가 props 변경을 감지하면 setValue() 이전에 모델 재생성 시도
❌ 시도 3: Props 고정 + 수동 갱신 (실패)
// 초기값을 ref로 저장하여 DiffEditor props는 절대 변경하지 않음
const initialOriginalRef = useRef(original)
const initialModifiedRef = useRef(modified)
<DiffEditor
original={initialOriginalRef.current} // 고정!
modified={initialModifiedRef.current} // 고정!
/>
// useEffect에서 setValue()로만 갱신
useEffect(() => {
updateEditorContent(original, modified)
}, [original, modified])
결과: ❌ 여전히 에러 발생
- 150줄의 복잡한 코드, 여전히 unmount 시 충돌
✅ 최종 해결: keepCurrentModel Props
🎯 공식 Props 발견!
@monaco-editor/react 문서를 자세히 읽다가, 공식적으로 제공하는 props를 발견했습니다:
<DiffEditor
original={original}
modified={modified}
keepCurrentOriginalModel={true} // ← 핵심!
keepCurrentModifiedModel={true} // ← 핵심!
/>
이 props의 역할:
keepCurrentOriginalModel={true}: props 변경 시 기존 original 모델 유지, setValue()로 내용만 갱신keepCurrentModifiedModel={true}: props 변경 시 기존 modified 모델 유지, setValue()로 내용만 갱신- 모델 재생성 완전 방지 → dispose 충돌 근본적으로 해결
📝 최종 구현 코드
/**
* Code Diff Viewer Component
*
* keepCurrentOriginalModel, keepCurrentModifiedModel props로
* props 변경 시 모델 재생성 방지
*/
import { useEffect, useRef, memo } from "react"
import { DiffEditor } from "@monaco-editor/react"
import type { editor } from "monaco-editor"
interface CodeDiffViewerProps {
original: string
modified: string
language: string
visible: boolean
}
export const CodeDiffViewer = memo(function CodeDiffViewer({
original,
modified,
language,
visible,
}: CodeDiffViewerProps) {
const editorRef = useRef<editor.IStandaloneDiffEditor | null>(null)
const handleEditorDidMount = (editor: editor.IStandaloneDiffEditor) => {
editorRef.current = editor
}
useEffect(() => {
return () => {
if (editorRef.current) {
try {
editorRef.current.dispose()
} catch (error) {
console.debug('DiffEditor cleanup:', error)
}
editorRef.current = null
}
}
}, [])
if (!visible) {
return null
}
return (
<div className="flex-1 border rounded-lg overflow-hidden">
<DiffEditor
height="100%"
language={language}
original={original}
modified={modified}
theme="vs-dark"
onMount={handleEditorDidMount}
keepCurrentOriginalModel={true} // ← 핵심 해결!
keepCurrentModifiedModel={true} // ← 핵심 해결!
options={{
readOnly: true,
renderSideBySide: true,
minimap: { enabled: false },
fontSize: 13,
automaticLayout: true,
}}
/>
</div>
)
})
코드 개선 효과:
- ✅ 150줄 → 85줄 (45% 감소)
- ✅ 복잡한 cleanup 로직 제거
- ✅ 공식 지원 기능 사용 (유지보수성 향상)
- ✅ 에러 완전 해결
결과 및 검증
🧪 테스트 시나리오
1. 함수 히스토리 모달 열기
2. 다양한 버전 빠르게 전환 (20회)
3. 다양한 파일 빠르게 전환 (20회)
4. 모달 닫기
5. 1-4를 20회 반복✅ Before vs After
| 항목 | Before | After |
|---|---|---|
| 콘솔 에러 | ❌ 매번 발생 | ✅ 완전히 사라짐 |
| 코드 라인 수 | 150줄 | 85줄 (-45%) |
| 복잡도 | 높음 (ref, useEffect 다수) | 낮음 (props 2개 추가) |
| 메모리 누수 | ⚠️ 가능성 존재 | ✅ 없음 |
| 유지보수성 | 낮음 (수동 관리) | 높음 (공식 API) |
핵심 교훈
💡 문제 해결 과정
복잡한 해결책부터 시도하지 말 것
- 150줄의 수동 cleanup 코드 작성
- 실제로는 props 2개로 해결 가능
공식 문서를 끝까지 읽을 것
keepCurrentOriginalModelprops는 문서 하단에 있었음- 초기에 발견했다면 몇 시간 절약 가능
라이브러리 내부 동작 이해의 중요성
- Monaco Editor의 모델 관리 메커니즘 이해
- React lifecycle과의 상호작용 파악
🎯 일반화된 해결 전략
Monaco Editor와 같은 복잡한 외부 라이브러리 사용 시:
공식 문서 정독
- 특히 "Advanced Usage", "Performance", "Troubleshooting" 섹션
Props 탐색
- TypeScript 타입 정의 파일 확인
- JSDoc 주석 읽기
GitHub Issues 검색
- 동일한 문제를 겪은 사람들의 해결책
- Maintainer의 권장 방법
최소한의 코드로 시작
- 복잡한 해결책은 마지막 수단
- 공식 API가 있는지 먼저 확인
참고 자료
마치며
Monaco Editor의 "TextModel got disposed" 에러는 많은 개발자들이 겪는 문제입니다. 복잡한 수동 관리 대신, 공식적으로 제공하는 props를 활용하면 간단하게 해결할 수 있습니다.
핵심은 단 2줄입니다:
keepCurrentOriginalModel={true}
keepCurrentModifiedModel={true}
이 글이 같은 문제로 고민하는 개발자들에게 도움이 되길 바랍니다! 🚀
imprun.dev 팀
Kubernetes 기반 서버리스 Cloud Function 플랫폼
'실제 경험과 인사이트를 AI와 함께 정리한 글' 카테고리의 다른 글
| Cilium 환경에서 API Gateway 배포 시 hostNetwork가 필요한 이유 (0) | 2025.10.29 |
|---|---|
| Kong에서 APISIX로의 험난한 여정: Cilium 환경에서의 시행착오 (0) | 2025.10.29 |
| Kubernetes Gateway API 실전 가이드: Kong Ingress에서 표준 API로 전환하기 (1) | 2025.10.27 |
| Kubernetes에서 특권 포트 피하기: NodePort + iptables 포워딩 패턴 (0) | 2025.10.27 |
| Kubernetes Gateway API 실전 가이드: Kong Ingress에서 표준 API로 전환하기 (0) | 2025.10.27 |
- Total
- Today
- Yesterday
- Next.js
- Rag
- claude code
- frontend
- LLM
- Developer Tools
- Kubernetes
- knowledge graph
- SHACL
- architecture
- Tax Analysis
- Ontology
- ai 개발 도구
- AI
- backend
- AI agent
- Claude
- AI Development
- react
- authentication
- 개발 도구
- api gateway
- authorization
- Go
- workflow
- LangChain
- Tailwind CSS
- PYTHON
- security
- troubleshooting
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
