앞에서 contenteditable(콘텐츠 편집 가능) 한 줄이면 아무 영역이나 편집창이 된다고 했죠. 이걸 처음 알면 에디터 다 만든 것 같은 기분이 들어요. 저도 그랬어요. div 하나에 속성 붙이고 굵게 버튼 달고, 며칠이면 끝나겠지 했죠. 그런데 여기서부터가 진짜 지옥이에요. contenteditable은 겉보기엔 만능인데, 실무에 넣는 순간 함정이 우수수 쏟아져요. 오늘은 제가 하나씩 밟아본 그 함정들을 증상과 원인으로 풀어볼게요. 미리 알면 여러분은 덜 다치니까요.

16.1 contenteditable만 쓰면 진짜 안 되나요?

일단 되긴 돼요. 이거 하나로 글자 입력, 커서 이동, 복사 붙여넣기가 공짜로 따라와요.


<div contenteditable="true">여기에 바로 써보세요</div>


클릭하면 커서가 깜빡이고 글자가 들어가죠. 문제는 여기까지만 쉽다는 거예요. 굵게 하나만 붙이려 해도 선택 영역을 직접 다뤄야 하고, 붙여넣기 한 번에 이상한 태그가 딸려 오고, 브라우저마다 결과가 달라요. 즉 contenteditable은 출발선일 뿐, 결승선이 아니에요. 지금부터 그 이유를 하나씩 볼게요.

16.2 왜 브라우저마다 태그가 다르게 나와요?

제일 먼저 데인 함정이에요. 편집창에서 엔터로 줄을 바꿨을 뿐인데, 안에 생기는 태그가 브라우저마다 달라요. 어떤 브라우저는 새 p 태그로 문단을 나누고, 어떤 브라우저는 div로, 또 어떤 상황에선 그냥 br 태그만 넣어요.


줄바꿈 한 번에 생기는 결과
어떤 곳: <p>둘째 줄</p>
어떤 곳: <div>둘째 줄</div>
어떤 곳: 첫째 줄<br>둘째 줄


같은 동작인데 결과 마크업이 제각각이니, 저장된 글을 다시 불러 보여줄 때 줄 간격이 들쭉날쭉해져요. 앞서 나온 execCommand가 폐기된 이유도 여기 있어요. 브라우저에 서식을 맡기면 결과를 내가 통제할 수 없거든요. 그래서 제대로 된 에디터는 이 자유분방한 동작을 가로채서 자기가 정한 태그로 통일해요.

16.3 굵게 버튼을 눌렀는데 왜 엉뚱한 데가 굵어져요?

굵게 버튼을 직접 만들다 보면 이상한 일이 생겨요. 버튼을 눌렀더니 방금 선택한 글자는 그대로엉뚱한 곳이 굵어지거나, 아무 데도 안 굵어져요. 원인은 포커스예요. 버튼을 클릭하는 순간 편집창에서 포커스가 버튼으로 넘어가면서, 방금 드래그해 둔 선택 영역이 사라져요.


boldBtn.addEventListener("mousedown", (e) => {
e.preventDefault();
});


그래서 툴바 버튼에는 mousedown에서 e.preventDefault()를 걸어요. 이러면 버튼을 눌러도 포커스가 편집창에 그대로 남고, 선택 영역이 살아 있어서 원하는 글자에 서식이 먹혀요. 리치 에디터에서 선택 영역을 지키는 건 거의 모든 조작의 기본기예요. 커서와 선택을 읽고 쓰는 Selection(선택 영역), Range(범위)와 친해져야 하는 이유죠.

16.4 붙여넣기 한 번에 왜 서식이 엉망이 돼요?

이건 사용자 제보로 알게 된 함정이에요. 어떤 분이 워드나 다른 사이트에서 글을 복사해 붙여넣었는데, 글이 알록달록 이상한 폰트로 들어왔다는 거예요. 재현해 보니 정말 남의 서식이 통째로 딸려 왔어요.


<span style="font-family:굴림; color:#ff00aa; font-size:19px">복사된 글</span>


contenteditable은 붙여넣기를 순진하게 다 받아들여요. 복사한 쪽의 style, class, 온갖 태그가 그대로 들어와 편집창을 오염시키죠. 심하면 script 같은 위험한 태그가 섞여 보안 문제까지 생겨요. 그래서 실무에선 paste 이벤트를 가로채 필요한 것만 남기고 나머지는 싹 걸러내요. 순수 텍스트만 받거나, 허락한 태그(굵게, 링크 정도)만 통과시키는 식이에요. 이 걸러내기를 안 하면 저장된 글이 지저분해지고, 나중에 보여줄 때도 깨져 보여요.


거르는 방식은 크게 둘이에요. 아주 깐깐하게 가려면 순수 텍스트만 받아 서식을 몽땅 버려요. 조금 융통성을 두려면 허락한 태그 목록만 통과시키고 나머지는 지우죠. 어느 쪽이든 핵심은 하나예요. 붙여넣기를 브라우저가 하는 대로 두면 안 되고, 내가 가로채서 정리해야 한다는 거예요.

16.5 한글을 칠 때 글자가 왜 깨지거나 중복돼요?

한국어 개발자가 특히 잘 만나는 함정이에요. 편집창에 한글을 빠르게 치면 마지막 글자가 두 번 들어가거나, 조합 중인 글자가 이상하게 잘려요. 범인은 IME(입력기, 조합해서 글자를 완성하는 방식)예요. 한글은 ㅎ, ㅏ, ㄴ을 조합해 한을 완성하는데, 이 조합이 끝나기 전에 에디터가 끼어들면 글자가 꼬여요.


editor.addEventListener("compositionstart", () => { composing = true; });
editor.addEventListener("compositionend", () => { composing = false; });


그래서 조합이 시작되고 끝나는 compositionstart, compositionend 신호를 듣고, 조합 중일 때는 에디터가 손을 대지 않아야 해요. 입력값을 실시간으로 붙잡아 바꾸는 코드를 짜면 이 조합과 충돌하기 딱 좋아요. 한글, 일본어, 중국어를 쓰는 서비스라면 반드시 챙겨야 하는 부분이에요.


이 함정이 무서운 이유는, 영어로 테스트하면 멀쩡하기 때문이에요. 영어는 글자 하나가 곧 완성이라 조합이 없거든요. 개발자가 영어로만 눌러보고 다 됐다며 넘겼다가, 한글 사용자한테서 글자 깨져요 제보가 쏟아지는 일이 흔해요. 그러니 한글 입력은 꼭 직접 빠르게 쳐보며 확인하세요.

16.6 빈 편집창이랑 되돌리기는 왜 이상하게 굴어요?

자잘하지만 신경 쓰이는 함정도 있어요. 편집창의 글자를 다 지우면 커서가 사라지거나 높이가 0으로 폭삭 주저앉아요. 그래서 많은 에디터가 빈 상태일 때 보이지 않는 br 태그를 하나 넣어 높이와 커서를 유지해요. 안내 문구(placeholder)도 진짜 글자가 아니라 CSS로 흉내 내야 커서를 안 방해해요.


또 하나는 되돌리기(undo)예요. contenteditable은 브라우저 기본 되돌리기를 주는데, 이게 내가 코드로 바꾼 부분어긋나기 일쑤예요. 버튼으로 넣은 서식은 되돌리기에 안 잡히거나, 엉뚱한 단계로 튀죠. 그래서 잘 만든 에디터는 자기만의 되돌리기 기록을 따로 관리해요. 이쯤 되면 느껴지죠. contenteditable은 거저 주는 게 많아 보여도, 실무에 쓰려면 손볼 게 산더미예요.

16.7 오늘 정리

오늘은 contenteditable의 함정을 훑었어요. 시작은 쉽지만, 줄바꿈 태그가 브라우저마다 제각각이라 결과를 통제하기 어려웠고, 굵게 버튼은 포커스가 넘어가며 선택이 풀려 mousedown 막기가 필요했죠. 붙여넣기는 남의 서식과 위험한 태그를 끌고 와 걸러내야 했고, 한글은 IME 조합 중 끼어들면 글자가 꼬여 compositionend까지 기다려야 했어요. 빈 편집창은 br로 높이를 지키고, 되돌리기는 직접 관리해야 어긋나지 않았고요. 요약하면 contenteditable은 강력한 출발점이지만 통제되지 않은 자유라, 실무에선 이 자유를 가로채 길들이는 일이 핵심이에요. 그래서 대부분은 이 고생을 대신해 주는 라이브러리를 고르는데, 그 선택 이야기를 다음에 이어가요.