에디터를 만들면 언젠가 꼭 이런 제보가 와요. "글자가 이상하게 커요", "배경이 노래요", "지웠는데 서식이 안 지워져요"라고요. 열에 아홉은 사용자가 워드나 웹페이지에서 복사해 붙여넣은 게 원인이에요. 붙여넣기는 겉보기엔 그냥 텍스트가 들어오는 것 같지만, 실제로는 보이지 않는 서식 쓰레기가 잔뜩 딸려 와요. 오늘은 이 붙여넣기를 가로채서 깨끗하게 정리하는 법을, 상황별로 하나씩 풀어볼게요.

20.1 워드에서 복붙하면 왜 이 모양이죠?

먼저 왜 이런 일이 생기는지부터 봐요. 사용자가 워드나 다른 사이트에서 글을 복사하면, 클립보드에는 순수 글자만 담기는 게 아니에요. 서식이 붙은 HTML이 통째로 같이 담겨요. 그 HTML을 그대로 에디터에 부으면 이런 게 들어와요.


<p style="font-size:19px; color:#c00">
<span class="MsoNormal">안녕하세요</span>
</p>


보이세요? 인라인 스타일(태그에 직접 박힌 style)에 폰트 크기와 색이 박혀 있고, MsoNormal 같은 워드 전용 클래스까지 딸려 왔어요. 이게 우리 사이트 CSS와 충돌하면서 글자가 커지고 배경이 노래지는 거예요. 그러니 붙여넣기를 그대로 두면 안 되고, 우리가 중간에서 손을 봐야 해요.


클립보드가 이렇게 여러 형태를 한꺼번에 담는 데는 이유가 있어요. 붙여넣는 곳이 워드일 수도, 메모장일 수도 있으니 서식 있는 버전과 순수 글자 버전을 둘 다 챙겨두는 거예요. 받는 쪽이 알아서 골라 쓰라는 뜻이죠. 우리 에디터는 이 선택권을 적극적으로 활용해서, 상황에 맞는 형태만 골라 쓰면 돼요.

20.2 붙여넣기를 어떻게 가로채요?

붙여넣는 순간을 잡는 이벤트가 paste예요. 여기서 기본 동작을 막고 우리가 직접 넣으면, 브라우저가 서식째로 부어넣는 걸 중간에 가로챌 수 있어요.


editor.addEventListener("paste", (e) => {
e.preventDefault();
const data = e.clipboardData;
const html = data.getData("text/html");
const text = data.getData("text/plain");
insertClean(html, text);
});


e.preventDefault()가 브라우저의 자동 붙여넣기를 막아요. 그리고 e.clipboardData에서 원하는 형태를 꺼내죠. getData("text/html")는 서식 포함 HTML을, getData("text/plain")은 서식을 뺀 순수 글자를 줘요. 이 둘 중 무엇을 어떻게 넣을지가 붙여넣기 처리의 전부예요.

20.3 서식 다 버리고 글자만 넣으려면요?

가장 간단하고 안전한 방식은 순수 텍스트만 받는 거예요. 서식은 통째로 버리고요. 메모나 댓글처럼 서식이 필요 없는 곳이라면 이걸로 충분해요.


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


getData("text/plain")으로 글자만 뽑고, insertText로 지금 커서 자리에 밀어 넣어요. 이러면 워드에서 아무리 요란하게 복사해 와도 글자만 쏙 들어가요. 폰트 크기, 색, 클래스 같은 쓰레기는 전부 사라지고요. 서식을 살릴 필요가 없다면 이 방식이 제일 튼튼해요. 애초에 위험한 태그가 들어올 여지가 없으니까요.


한 가지 덧붙이면, 순수 텍스트에도 줄바꿈은 남아 있어요. 여러 문단을 복사해 붙이면 그 줄바꿈이 그대로 오죠. 문단으로 예쁘게 나누고 싶으면 넣기 전에 줄바꿈을 기준으로 잘라 문단마다 감싸주면 돼요. 별것 아닌 것 같아도, 이걸 안 하면 긴 글이 한 덩어리로 뭉쳐 붙어서 사용자가 당황해요.

20.4 서식은 살리되 쓰레기만 버리려면요?

문제는 굵게, 링크, 목록 같은 서식은 살리고 싶을 때예요. 이땐 HTML을 받되 허용한 것만 남기고 나머지는 벗겨내야 해요. 이걸 화이트리스트(허용 목록) 방식이라고 해요. 미리 정한 태그와 속성만 통과시키는 거죠.


const ALLOWED = ["P", "B", "I", "A", "UL", "LI", "BR"];
function clean(node) {
for (const el of [...node.children]) {
if (!ALLOWED.includes(el.tagName)) {
el.replaceWith(...el.childNodes);
} else {
el.removeAttribute("style");
el.removeAttribute("class");
clean(el);
}
}
}


허용 목록에 없는 태그replaceWith로 껍데기만 벗기고 안의 내용은 살려요. 허용된 태그는 남기되 style과 class를 떼어내서 우리 CSS만 적용되게 하죠. 이렇게 하면 굵게와 링크는 살아남고, 워드가 심어둔 인라인 스타일과 전용 클래스는 싹 사라져요. 다만 이런 정리를 직접 손으로 짜는 건 구멍이 많아요. 이 화이트리스트 개념이 다음 편에서 다룰 sanitize(위험 요소 정화)로 이어지는데, 실제로는 검증된 라이브러리에 맡기는 게 안전해요.

20.5 이미지가 딸려 오면요?

붙여넣기엔 글자만 오는 게 아니에요. 사용자가 사진을 복사해 붙이거나, 캡처한 화면을 바로 붙이기도 하죠. 이건 clipboardData.items에서 잡아요.


for (const item of e.clipboardData.items) {
if (item.type.startsWith("image/")) {
e.preventDefault();
uploadImage(item.getAsFile());
}
}


클립보드 조각 중 type이 image로 시작하는 게 있으면, 그건 글자가 아니라 그림 파일이에요. getAsFile()로 파일을 꺼내 앞 편에서 만든 업로드 흐름에 그대로 태우면 돼요. 여기서 조심할 건, 이미지를 붙였을 때 워드가 data URL로 박힌 img를 HTML로도 같이 넘기는 경우예요. 그걸 걸러내지 않으면 같은 사진이 두 번 들어가거나 본문에 거대한 data URL이 박혀요. 이미지는 파일 통로로만 처리하고 HTML 쪽 img는 정리 단계에서 버리는 게 깔끔해요.

20.6 오늘 정리

붙여넣기 사고의 뿌리는 보이지 않는 서식 HTML이 딸려 오는 데 있었어요. 그래서 paste 이벤트에서 preventDefault로 기본 붙여넣기를 막고, clipboardData에서 원하는 형태를 직접 꺼내 넣는 게 출발점이었죠. 서식이 필요 없으면 text/plain만 받아 insertText로 넣는 게 제일 튼튼했고, 서식을 살리려면 화이트리스트로 허용 태그만 남기고 style과 class를 벗겨냈어요. 사진을 붙이면 clipboardData.items에서 파일을 꺼내 업로드로 넘기되, HTML에 박힌 중복 img는 버렸고요. 붙여넣기를 길들이면 사용자 제보의 절반이 사라져요.