에디터는 만들 땐 잘 되는 것 같은데, 사용자 손에 들어가면 희한한 버그가 쏟아지는 물건이에요. 저도 에디터를 몇 번 만들면서 별별 사고를 다 쳐봤어요. 분명 저장했는데 글이 날아가고, 붙여넣기 한 번에 화면이 깨지고, 한글이 자꾸 씹히고, 이미지가 감쪽같이 사라지고요. 오늘은 그 단골 사고 네 가지를 골라, 왜 그런지와 어떻게 막는지를 제 삽질담과 함께 풀어볼게요. 미리 알아두면 며칠씩 날리는 삽질을 통째로 건너뛸 수 있어요.

28.1 분명 저장했는데 왜 날아갔죠?

제가 처음 자동 저장을 붙였을 때 이야기예요. 테스트할 땐 완벽했어요. 글 쓰고 기다리면 "저장됨"이 뜨고, 새로고침해도 멀쩡했죠. 그런데 사용자한테서 "글이 날아갔다"는 문의가 왔어요. 재현이 안 돼서 한참 헤맸죠. 범인은 사용자가 글을 다 쓰자마자 탭을 닫은 것이었어요.


자동 저장은 보통 디바운스로 동작해요. 손을 0.8초 멈춘 뒤에야 저장이 나가죠. 그런데 사용자는 마지막 글자를 치자마자 0.8초를 못 기다리고 탭을 닫아버린 거예요. 예약된 저장은 실행되기도 전에 페이지와 함께 사라졌고요. 테스트할 땐 제가 저장을 확인하느라 항상 기다렸으니 재현이 안 됐던 거죠.


해결은 나가기 직전에 마지막 저장을 강제로 밀어 넣는 거예요.


window.addEventListener("beforeunload", function () {
clearTimeout(timer);
localStorage.setItem("draft", editor.innerHTML);
});


beforeunload(떠나기 직전) 이벤트에서, 예약된 타이머를 clearTimeout으로 지우고 기다릴 것 없이 localStorage에 지금 즉시 초안을 박아요. 서버 요청은 페이지가 닫히는 중이라 실패할 수 있지만, 로컬 저장은 인터넷이 필요 없어 순식간에 끝나거든요. 이렇게 하니 "날아갔다" 문의가 딱 끊겼어요. 교훈은 "기다렸다 저장"에는 항상 못 기다리는 순간이 있다는 거예요.

28.2 붙여넣기 했더니 이상한 게 딸려와요?

두 번째 단골은 붙여넣기예요. 사용자가 워드나 다른 웹페이지에서 글을 복사해 붙이면, 눈엔 글자만 보이지만 실제론 지저분한 서식과 태그가 잔뜩 딸려와요. 폰트, 배경색, 심지어 남의 사이트 스타일까지 통째로 들어와 에디터가 엉망이 되죠.


더 무서운 건 보안이에요. 누군가 악의적으로 script(스크립트) 태그가 섞인 내용을 붙이도록 유도하면, 그게 그대로 저장됐다가 다른 사용자 화면에서 실행될 수 있어요. 이걸 XSS(Cross-Site Scripting, 사이트 간 스크립트 삽입 공격)라고 해요. 남의 로그인 정보를 훔치는 데 쓰이는 대표적인 공격이라 절대 가볍게 보면 안 돼요.


가장 간단한 방어는 붙여넣기를 가로채 서식을 버리고 글자만 넣는 거예요.


editor.addEventListener("paste", function (e) {
e.preventDefault();
const text = e.clipboardData.getData("text/plain");
document.execCommand("insertText", false, text);
});


paste(붙여넣기) 이벤트를 잡아 e.preventDefault로 브라우저의 기본 붙여넣기를 막아요. 그리고 clipboardData.getData("text/plain")으로 순수 글자만 꺼내 다시 넣죠. 서식이 필요한 에디터라면 붙여넣은 HTML을 정제해 허용된 태그만 남기는데, 직접 하면 빈틈이 생기기 쉬워서 DOMPurify 같은 검증된 정제 라이브러리를 쓰는 게 정석이에요. 핵심 원칙은 "사용자가 넣은 건 절대 그대로 믿지 않는다"예요.

28.3 한글 입력이 자꾸 씹혀요?

이건 한국에서 에디터 만들면 반드시 만나는 사고예요. 영어로 테스트할 땐 멀쩡한데, 한글을 치면 글자가 씹히거나 중복되거나 마지막 글자가 사라져요. 저도 이걸로 하루를 통째로 날렸어요.


원인은 IME(Input Method Editor, 입력기)예요. 한글, 일본어, 중국어는 여러 번의 키 입력을 조합해 한 글자를 완성해요. "한"을 치려면 ㅎ, ㅏ, ㄴ이 순서대로 들어와 조합 중인 상태를 거치죠. 이 조합이 끝나기 전에 우리가 input 이벤트마다 화면을 다시 그리거나 값을 건드리면, 조합이 강제로 끊겨 글자가 깨지는 거예요. 영어는 한 번에 한 글자라 조합 과정이 없어서 이 문제가 안 보였던 거고요.


브라우저는 이 조합 상태를 알려주는 compositionstart(조합 시작)와 compositionend(조합 끝) 이벤트를 줘요. 조합 중일 땐 우리 로직을 건드리지 말고 기다렸다가, 조합이 끝난 뒤에 처리하면 돼요.


let composing = false;
editor.addEventListener("compositionstart", function () {
composing = true;
});
editor.addEventListener("compositionend", function () {
composing = false;
onChange();
});
editor.addEventListener("input", function () {
if (composing) return;
onChange();
});


composing이라는 표시로 지금 한글 조합 중인지를 기억해요. 조합이 시작되면 true가 되고, input 이벤트가 와도 if (composing) return으로 그냥 넘어가죠. 조합이 끝나는 순간 false로 바꾸고 그때 onChange를 한 번 실행해요. 이렇게 하면 조합을 끊지 않아 한글이 안 씹여요. 자동 저장이든 글자 수 세기든, 입력 중에 뭔가 하는 로직이라면 이 조합 상태를 꼭 챙겨야 한글 사용자가 고통받지 않아요.

28.4 이미지가 저장 후에 사라졌어요?

네 번째 사고. 사용자가 이미지를 복사해 에디터에 붙이면 화면엔 이미지가 잘 떠요. 그래서 저장하고 창을 닫죠. 그런데 나중에 다시 열면 이미지 자리에 깨진 그림 아이콘만 덩그러니 남아요. 분명 잘 보였는데 왜죠?


범인은 blob URL(블롭 URL, 브라우저가 임시로 만든 주소)이에요. 이미지를 붙이면 브라우저는 그걸 blob:로 시작하는 임시 주소로 화면에 보여줘요. 문제는 이 주소가 이 페이지, 이 순간에만 유효하다는 거예요. 페이지를 새로고침하거나 닫으면 주소가 즉시 무효가 돼요. 그런데 저장할 때 에디터의 HTML을 그대로 보내면, 그 안엔 곧 죽어버릴 blob 주소가 박혀 있죠. 다시 열면 그 주소가 아무것도 안 가리켜 이미지가 깨지는 거예요.


해결은 이미지를 진짜로 서버에 올리고, 임시 주소를 진짜 주소로 바꿔치기하는 거예요. 흐름은 이래요.


1. 사용자가 이미지를 붙이거나 선택함
2. 임시 blob 주소로 화면에 미리 보여줌 (사용자는 바로 봄)
3. 그 사이 이미지 파일을 서버에 업로드
4. 서버가 돌려준 진짜 주소(https://...)로 교체
5. 이제 저장하면 진짜 주소가 저장됨


핵심은 저장되는 HTML 안에 blob 주소가 남으면 안 된다는 거예요. 저는 예전에 업로드가 끝나기 전에 저장이 먼저 나가버려서, 절반은 진짜 주소, 절반은 죽은 blob 주소인 글이 저장되는 사고를 낸 적이 있어요. 그래서 "모든 이미지 업로드가 끝날 때까지 저장을 잠깐 미루는" 장치를 두는 게 안전해요.

28.5 사고를 미리 막는 습관은 뭐예요?

지금까지 네 사고를 보면 공통점이 있어요. 전부 얌전히 천천히 쓸 때는 안 나타나고, 사용자가 빠르게 쓰거나 예상 밖으로 행동할 때 터진다는 거예요. 그래서 에디터를 테스트할 땐 얌전한 사용만으로는 부족해요. 제가 몸으로 배운 점검 목록을 남길게요.


에디터 만들면 꼭 해볼 테스트
1. 글 치자마자 곧바로 탭 닫기 (저장 경쟁)
2. 워드, 남의 웹페이지에서 복사해 붙여넣기 (서식, XSS)
3. 한글을 빠르게 길게 입력하기 (IME 조합)
4. 이미지 붙이고 바로 새로고침 (blob 유실)
5. 인터넷 끊고 글 쓰다 다시 연결하기 (저장 실패 복구)


이 다섯 가지만 습관처럼 돌려봐도 사용자가 겪을 사고의 대부분을 미리 잡아낼 수 있어요. 개발자는 자기 코드를 얌전하게 쓰는 경향이 있어서, 일부러 거칠게 굴려보는 게 중요해요.

28.6 오늘 정리

에디터의 단골 사고와 방어법을 모았어요.
1. 저장 경쟁: 디바운스가 못 나가고 탭이 닫혀요. 나가기 직전 로컬에 강제 저장으로 막아요.
2. 붙여넣기: 지저분한 서식과 XSS가 딸려와요. 글자만 받거나 DOMPurify로 정제해요.
3. IME 유실: 한글 조합 중 화면을 건드리면 씹혀요. composition 이벤트로 조합이 끝날 때까지 기다려요.
4. 이미지 유실: 임시 blob 주소는 곧 죽어요. 서버에 올려 진짜 주소로 바꾼 뒤 저장해요.
5. 이 사고들은 거칠게 테스트해야 드러나니, 일부러 험하게 굴려보세요.


에디터는 사용자와 가장 오래 마주 앉는 화면이에요. 그만큼 작은 사고 하나가 신뢰를 크게 갉아먹죠. 오늘 본 네 지뢰만 미리 밟아 치워둬도, 여러분의 에디터는 훨씬 믿음직한 물건이 될 거예요. 사용자가 아무 사고 없이 글에만 집중할 수 있다면, 그게 잘 만든 에디터의 진짜 모습이에요.