블로그 관리자 페이지의 글쓰기 에디터를 세 번 연속으로 다시 만들었다. 세 번 다 “고쳤다"고 생각했는데, 실제로 브라우저를 열면 카테고리 목록도 안 뜨고 툴바 버튼도 하나도 반응하지 않았다. 원인은 라이브러리가 아니라, 훨씬 더 근본적인 곳에 있었다.

처음 시도는 Tiptap이었다. esm.sh CDN에서 @tiptap/core, @tiptap/starter-kit, @tiptap/extension-placeholder를 각각 따로 불러왔는데, 패키지마다 별도로 번들링된 esm.sh 특성상 같은 클래스가 서로 다른 인스턴스로 중복 로드될 위험이 있는 조합이었다. 전혀 동작하지 않아서 Quill.js로 바꿨다 — 단일 UMD 스크립트라 이 위험 자체가 없는 선택이었다. 그런데도 여전히 안 됐다.

Quill로 바꾼 뒤에도 “툴바가 전체 텍스트에 적용된다”, “이미지 추가 버튼이 안 눌린다” 같은 증상이 이어졌다. 이쯤에서 라이브러리를 의심하는 대신 실제로 서버가 브라우저에 뭘 보내고 있는지 바이트 단위로 확인하기로 했다. Node 스크립트로 서버가 렌더링하는 HTML을 직접 추출해서 읽어보니, 정규식 하나가 /\s/g가 아니라 /s/g로 나가고 있었고, 문자열이어야 할 곳에 줄바꿈이 리터럴로 박혀 있었다.

원인은 이거였다 — 클라이언트에서 실행될 자바스크립트 코드를, 서버(Node)의 템플릿 리터럴 문자열 안에 그대로 텍스트로 박아 넣고 있었다. return `<script>...\s...</script>`; 같은 코드를 쓰면, \s나 \n 같은 이스케이프 시퀀스는 브라우저가 아니라 이 바깥쪽 Node 템플릿 리터럴이 먼저 해석해버린다. \s는 원래 무효한 이스케이프라 그냥 s로 조용히 깨졌고, \n은 유효한 이스케이프라서 브라우저로 넘어가는 코드 한가운데에 진짜 줄바꿈 문자가 그대로 삽입됐다 — 작은따옴표로 감싼 문자열 리터럴 중간에 원본 개행이 끼어드는 셈이라, 브라우저 쪽에서는 명백한 문법 오류였다. <script> 블록 안에서 문법 오류가 하나라도 나면 그 블록 전체가 죽는다. 카테고리 로딩도, 툴바 이벤트 리스너도, 심지어 에러를 화면에 띄워주려던 핸들러까지 같은 스크립트 안에 있었기 때문에 전부 함께 조용히 사라진 것이다.

같은 코드를 세 번 다른 라이브러리로 갈아엎어도 매번 같은 방식으로 실패했던 이유가 여기 있었다 — 문제는 에디터 자체가 아니라, 그 에디터 코드를 서버에 박아 넣는 방식이었다.

고친 방법은 간단했다. 클라이언트 코드를 별도의 진짜 파일(editor-client.js)로 분리하고, 서버는 fs.readFileSync로 이 파일을 읽어서 완성된 문자열 값으로 템플릿 리터럴에 끼워 넣도록 바꿨다. 런타임에 치환되는 문자열 값은 다시 이스케이프 해석의 대상이 되지 않는다 — 따옴표나 백틱 사이의 리터럴 소스 문자만 재해석 대상이라서다. 그리고 최종적으로는 이런 종류의 실패를 다시는 조용히 지나치지 않도록, 클라이언트 코드 최상단에 try/catch와 window.addEventListener('error', ...)를 걸어 두었다. 앞으로 뭔가 깨지면 최소한 화면에 빨간 배너로 보이게.