# 11 · 에디터 증상 → 조치

**언제 읽나**: 에디터가 "에러는 없는데 결과가 이상할" 때.
**사용법**: 아래 표에서 **증상 문구를 그대로 찾는다.** 없으면 추측하지 말고 "이 지식팩에 없는 증상"이라고 답한다.

---

## A. 화면과 출력이 다르다

| 증상 | 원인 | 조치 |
|---|---|---|
| 창 크기를 바꾸면 줄바꿈 위치가 달라진다 | 레이아웃 폭이 창 크기에 종속 | 아트보드 폭을 상수로 고정. 줌은 `transform: scale()`만 사용 |
| 내보낸 파일에 가이드선·선택 핸들이 찍혔다 | 실시간 화면 DOM을 캡처 중 | 오프스크린 렌더로 전환. 임시 조치는 캡처 전 선택 해제 + 숨김 CSS 추가 |
| 내보내기 결과가 빈 페이지거나 이전 내용이다 | 캡처 루프가 잡은 DOM 참조가 remount로 무효화됨 | 캡처 대상 엘리먼트에 `key` 속성을 주지 않는다. 또는 루프마다 다시 조회 |
| 화면에선 글자가 잘리는데 출력에는 안 잘려 나온다 | 화면은 `overflow:hidden`, 출력은 줄바꿈 허용 | 화면의 `overflow:hidden`을 제거한다(자르지 않는다) |
| 좁은 창에서 캔버스 비율이 깨진다 | flex 컨테이너 안에서 압축됨 | 캔버스 요소에 `flex-shrink: 0` 추가 |
| 좁은 창에서 출력 해상도가 낮아진다 | 캡처 배율에 상한을 걸어둠 | 배율 상한을 제거한다 |

---

## B. 조작이 이상하다

| 증상 | 원인 | 조치 |
|---|---|---|
| 드래그할 때마다 요소 크기가 사라진다/줄어든다 | 부분 업데이트가 좌표 객체를 통째로 교체 | 중첩 객체를 분리 병합: `{...el, frame: {...el.frame, ...patch.frame}}` |
| 숫자로 회전시키면 조절 박스가 안 따라 돈다 | 조작 라이브러리가 프로그램적 변경을 모름 | 회전 값 변경 직후 라이브러리의 `updateRect()`를 호출 |
| 드래그 한 번에 Undo가 수십 개 쌓인다 | 커밋 경계가 없음 | 드래그 시작~종료를 하나의 변경으로 묶는다. 목표: 드래그 1회 = Undo 1회 |
| 확대/축소 상태에서 커서와 요소가 어긋난다 | 좌표 계산에 scale을 반영하지 않음 | 마우스 좌표를 scale로 나눠 변환. 목표 오차: 0.4px 이내 |

---

## C. 한글 입력

| 증상 | 원인 | 조치 |
|---|---|---|
| 조합 중 글자가 깨진다 (ㅎ→하→한이 "ㅎ하한") | 조합 중 프레임워크가 입력 영역을 다시 그림 | 편집 중에는 내용을 비제어로 둔다(렌더 시 내용을 넣지 않음). `compositionstart`/`compositionend`로 조합 상태를 추적하고 **blur에서만** 저장한다 |
| 편집 중 Backspace가 요소를 통째로 지운다 | 전역 단축키가 편집 상태를 모름 | 편집 중에는 전역 키 핸들러를 끈다 |
| 편집 중 선택 핸들이 글자를 가린다 | 핸들이 계속 표시됨 | 편집 중 핸들을 숨긴다 |

---

## D. 저장 · 데이터

| 증상 / 에러 | 원인 | 조치 |
|---|---|---|
| `QuotaExceededError` (Firefox: `NS_ERROR_DOM_QUOTA_REACHED`, code 22 또는 1014) | 저장 용량 5MB 초과 | 이미지를 문서에서 분리(에셋+id). 업로드는 긴 변 2048px 리샘플. 초과 시 **화면에 붉게 표시**하고 앱은 계속 동작시킨다 |
| Undo 몇 번에 앱이 느려지고 메모리가 급증 | 스냅샷에 이미지 데이터가 포함됨 | 문서에서 base64를 제거한다 |
| 새로고침하면 작업이 전부 사라진다 | 저장 기능 자체가 없음 | 저장·Undo를 먼저 구현한다 (파일 `10`의 규칙 4) |
| 가끔 마지막 편집만 유실된다 | 디바운스만 있고 강제 저장이 없음 | 디바운스 1.5초 + 10초 강제 저장 + 페이지 이탈 직전 저장, 3중으로 |
| 손상된 데이터 하나 때문에 앱이 아예 안 뜬다 | 읽기 실패를 예외로 올림 | **읽기 실패는 기본값으로 흡수하고, 쓰기 실패는 올린다** |

### 읽기/쓰기 비대칭 — 그대로 복사해 쓸 것

```js
// 쓰기: 실패를 숨기지 않는다. 사용자가 알아야 데이터를 잃지 않는다.
function write(key, value) {
  try {
    localStorage.setItem(key, JSON.stringify(value));
    return true;
  } catch (e) {
    if (e && (e.name === 'QuotaExceededError' ||
              e.name === 'NS_ERROR_DOM_QUOTA_REACHED' ||
              e.code === 22 || e.code === 1014)) {
      throw new Error('저장 공간이 가득 찼습니다.');
    }
    throw e;
  }
}

// 읽기: 실패를 흡수한다. 항목 하나 때문에 앱 전체가 죽으면 더 나쁘다.
function read(key, fallback = null) {
  try {
    const raw = localStorage.getItem(key);
    return raw ? JSON.parse(raw) : fallback;
  } catch {
    return fallback;
  }
}
```

---

## E. 폰트 · 출력 파일

| 증상 | 원인 | 조치 |
|---|---|---|
| 받는 사람 PC에서만 줄이 한 줄 더 넘어간다 | 내 PC에 없는 폰트를 출력 파일에 이름으로 기록 | 재배포 가능 폰트만 포함하고 선택 목록을 그것으로 제한 |
| 캡처 결과에서 글자가 네모(두부)로 나온다 | 폰트가 캡처 시점에 로드되지 않음 | CDN 대신 파일로 포함. unicode-range 분할 사용 금지 |
| 그림자·블러·블렌드가 출력물에서 사라진다 | 캡처 라이브러리가 해당 CSS를 재현 못 함 | 해당 CSS를 스키마에서 제외 |
| PowerPoint에서 회전한 표가 이미지로 나온다 | PowerPoint 제품 자체가 표 회전을 지원하지 않음 | 정상 동작이다. 회전 0°는 표로, 회전≠0만 이미지로 내보내고 사용자에게 알린다 |

---

## 확인 절차

조치 후 반드시 실행한다.

1. **창 크기 3종**(예: 1280 / 1024 / 820 폭)에서 같은 문서를 열어 줄 수와 출력 픽셀 크기가 같은지 본다.
2. 내보낸 파일을 **실제로 열어서** 본다. 파일이 생성됐다는 것은 내용이 맞다는 뜻이 아니다.
3. 새로고침 후 문서가 복원되는지, Undo/Redo를 10회 왕복해도 깨지지 않는지 본다.

확인하지 않았으면 "미검증"이라고 보고한다.
