BlockNote 7단계 업그레이드가 지운 코드 색, Shiki 로 렌더 시점에 칠했습니다
안 쓰는 의존성을 지우려고 앉았습니다. 30분이면 끝날 청소로 봤고, 실제로는 90분이 걸렸습니다. 그 90분이 끝날 때 바뀐 것은 패키지 목록이 아니라 본문 데이터의 설계였습니다.
중간에 업그레이드가 끼었고, 그 업그레이드가 발행글 17편의 코드 색을 지웠습니다. 릴리스 노트에는 그 이야기가 한 줄도 없었습니다.
안 쓰는 것을 지우러 갔다가 쓰는 것을 올리게 됐습니다
지우려던 쪽은 TipTap 계열 패키지 7개였습니다. 그 패키지를 쓰는 컴포넌트는 3월에 만든 뒤 어디서도 불러오지 않는 잔재였고, 실제로 화면에 떠 있는 에디터는 다른 계열인 BlockNote 였습니다.
지우는 김에 쓰고 있는 쪽 버전을 봤습니다. 설치된 것이 0.47.2 인데 최신이 0.54.0 이었습니다. 마이너 7단계가 밀려 있었습니다. 그래서 이 작업은 그 자리에서 성격이 바뀌었습니다.
올리기 전에 릴리스 노트를 처음부터 읽었습니다. 노트가 예고한 '파괴적 변경'(저자가 호환이 깨진다고 명시한 변경)은 4개였고 하나씩 대조한 결과는 전부 무관이었습니다. 공동 편집 연동·배포 형태·마크다운 파서 교체·테마 셀렉터 모두 이 사이트가 안 쓰는 기능입니다. 빌드도 통과했습니다.
릴리스 노트에 없던 회귀
발행된 글을 열어 보니 코드블록이 무채색이었습니다. 0.54 가 코드블록 안의 인라인 스타일을 버립니다. 노트의 어느 항목에도 그 문장은 없었습니다. 영향 범위는 발행글 17편이었고, 그중 한 편에는 색이 입혀진 텍스트 조각이 66개 들어 있었습니다.
저자 입장에서는 파괴가 아니라 정리였을 겁니다. 특정 노드 안의 인라인 스타일을 더 이상 들고 가지 않는 것은 API 계약이 깨지는 일이 아닙니다. 릴리스 노트는 저자가 의도한 변경의 목록이고, 내 데이터의 목록이 아닙니다.

회귀는 계기였고 진짜 문제는 저장 구조였습니다
원인을 파니 더 아래에 다른 것이 있었습니다. 코드의 색이 본문 JSON 안에 토큰마다 'textColor' 속성으로 박혀 있었습니다. 색이 입혀진 코드를 다른 도구에서 복사해 에디터에 붙여넣은 결과물입니다. 사람이 고른 강조가 아니라 붙여넣기의 부산물이었습니다.
그 구조 때문에 따라온 문제가 셋이었고, 셋 다 업그레이드와 무관하게 이미 일어나 있었습니다.
붙여넣은 출처마다 팔레트가 달라 전역 CSS 의 하드코딩 매핑이 이미 어긋나 있었습니다. 저장값은 rgb(112,184,255) 인데 CSS 규칙은 rgb(121,192,255) 를 잡고 있었습니다
에디터에서 직접 타이핑한 코드에는 색이 아예 안 붙습니다. 붙여넣기를 거치지 않으면 저장될 값이 없습니다
색의 종류가 늘 때마다 사람이 CSS 규칙을 손으로 추가해야 합니다
업그레이드는 이 셋을 드러낸 계기였을 뿐입니다. 색처럼 입력에서 매번 유도할 수 있는 표현을 본문에 저장해 두면 업그레이드가 아니라도 언젠가 어긋납니다.
저장해 둔 파생값이 규칙과 어긋나는 자리는 여기만이 아닙니다. 색인할 때 계산해 저장한 값이 규칙을 고친 뒤에도 옛 데이터에는 따라오지 않던 기록이 있습니다.
라이브와 로컬을 나란히 놓고 눈으로 확정했습니다
방향을 정하기 전에 비교 페이지를 만들었습니다. 왼쪽에 지금 서비스되는 발행글의 HTML, 오른쪽에 올린 버전으로 로컬에서 다시 렌더한 결과를 같은 글 단위로 세운 화면입니다. 만드는 데 오래 걸리지 않았고, 달라졌다는 사실과 어떻게 달라졌는지를 한 번에 줬습니다.
검증 기준을 '빌드가 되나' 로 잡으면 이 회귀는 안 걸립니다. 빌드는 처음부터 통과했습니다. 기준을 '기존 산출물이 그대로 나오나' 로 바꿔야 걸립니다.
저장값을 보정할지, 렌더 시점에 계산할지
선택 | 하는 일 | 남는 것 |
|---|---|---|
저장값을 보정한다 | 인라인 스타일을 살리는 경로를 찾거나, 어긋난 팔레트를 일괄 치환해 CSS 매핑에 맞춥니다 | 손은 덜 댑니다. 대신 직접 타이핑한 코드에 색이 없는 것과 색이 늘면 규칙을 추가해야 하는 것은 그대로입니다 |
렌더 시점에 계산한다 | 본문의 색 정보를 쓰지 않고, HTML 을 만들 때 코드블록의 언어를 보고 칠합니다 | 서버 전용 단계가 하나 생깁니다. 대신 위 세 문제가 한 번에 사라집니다 |
두 번째를 골랐습니다. 회귀를 되돌리는 쪽이 아니라 회귀가 드러낸 설계를 바꾸는 쪽입니다. 판단에 쓴 질문은 하나였습니다. 이 값이 입력에서 유도할 수 있는 값인가. 코드의 색은 언어와 문법만 있으면 매번 같은 결과로 계산됩니다.
이미 저장돼 있던 색은 살리지 않고 버렸습니다. 손실이 0 이었던 것은 운이 아니라 확인의 결과입니다. 사람이 특정 토큰만 일부러 칠해 둔 문서였다면 계산으로는 복원되지 않았을 겁니다.

서버 전용 구문강조 단계를 하나 신설했습니다
Shiki 로 코드블록을 칠하는 단계를 만들어 본문 HTML 을 만드는 마지막 자리에 연결했습니다. 테마는 'catppuccin-mocha' 로, 기존 코드블록의 배경 #1e1e2e 과 전경 #cdd6f4 과 같은 계열이라 화면 인상이 유지됩니다. 하드코딩돼 있던 rgb 규칙 8개는 경위 주석만 남기고 제거했습니다.
대상 범위를 세어 보니 발행글의 코드블록은 117개였고, 언어가 지정돼 강조 대상이 되는 것은 python 12 · cypher 12 · json 5 · yaml 4 · sql 3 · java 2 였습니다. 나머지 79개는 언어 없는 평문이었습니다.
가는 길에 함정 둘을 만났습니다.
서버에서 블록을 HTML 로 바꾸는 유틸은 0.54 에서 'y-prosemirror'(공동 편집용 패키지)를 따로 설치해야 빌드됩니다. 공동 편집을 안 써도 그렇습니다
같은 라이브러리의 코드블록 패키지가 주는 'syntaxHighlighter' 는 라이브 에디터 화면의 장식이라 서버 변환 함수에 들어오지 않습니다. 모르고 에디터 스키마에 물리면 출력 HTML 에 언어 선택 '<select>' 태그가 섞여 나갑니다
'새 것이 있다'와 '옛 것이 없다'를 따로 셌습니다
검증은 배포한 뒤 서비스 중인 HTML 을 직접 받아서 했습니다. 센 값은 셋입니다. 새로 붙은 색 조각 85개, 옛 rgb 조각 0개, 에디터용 '<select>' 누출 0개. 빌드 종료 코드는 0 이었습니다.
따로 센 이유가 있습니다. 새 색이 85개 붙은 것만 확인하면 옛 조각이 남아 섞여 있어도 통과합니다. 되돌리기 어려운 상태 변경에서는 '새 것이 생겼나'와 '옛 것이 없어졌나'는 서로를 대신하지 못합니다.
반영 여부는 바뀐 본문 문구가 보이는지로 보지 않고, 응답의 'etag'(배포본이 바뀌면 값이 달라지는 응답 헤더) 가 바뀌는지로 봤습니다. 비동기로 올라가는 배포에서 '아직 안 됐다'와 '애초에 안 된다'를 가르는 기준이 그것입니다. 푸시한 지 60초 뒤에 값이 달라졌습니다.
안 바꿨는데 결과가 달라지는 자리는 원인이 매번 다른 곳에 있습니다.

감수한 것
편집 화면과 미리보기에서는 코드가 여전히 무채색입니다. 색을 칠하는 단계가 서버 경로에만 붙어 있어서입니다. 편집 중 모습과 발행 결과가 같아야 하는 제품이라면 이 해법은 절반짜리입니다. 그리고 이 사이트는 HTML 을 한 번 만들어 정적으로 내보내므로 계산이 한 번만 일어납니다. 요청마다 본문을 조립하는 구조였다면 캐시 층을 먼저 뒀어야 했을 겁니다.
같은 자리에 서면 점검할 것
검증 기준이 빌드 통과인지 기존 산출물 비교인지 — 이번 회귀는 빌드를 통과했습니다
기존 산출물을 눈으로 대 볼 자리가 있는지 — 페이지 한 장이면 됩니다
저장된 값 중 입력에서 유도할 수 있는 것이 있는지 — 색·요약·정렬 키가 자주 걸립니다
그 저장값이 사람이 의도한 것인지 다른 경로의 부산물인지
새 것이 생겼는지와 옛 것이 없어졌는지를 따로 셀 수 있는지
마무리
파괴적 변경 목록을 다 읽고 전부 무관으로 판정한 뒤에도 안전하지 않았습니다. 저자는 API 계약이 깨지는 것을 적고, 내 데이터에 무슨 일이 생기는지는 적지 않습니다. 여러 단계를 건너뛸 때는 노트가 아니라 산출물을 봐야 합니다.
7단계 점프가 늘 90분에 수습되지는 않습니다. 이번에 수습된 것은 영향 범위가 발행글의 코드블록이라는 셀 수 있는 단위였기 때문입니다. 셀 수 없는 범위라면 한 단계씩 올리는 쪽이 맞습니다.