- 발행일
워드클라우드가 통째로 안 그려졌다 — tojson을 백틱으로 감싸면 안 되는 이유
워드클라우드가 통째로 안 그려졌다
데이터 통계 화면에는 워드클라우드 탭이 셋 있습니다. 검색어, 활용, 등록 데이터 어휘. 어느 날 세 탭이 전부 비어 있었어요. 하나만 비었으면 그 탭의 데이터를 의심했을 텐데, 셋이 같이 비면 데이터가 아니라 그리는 쪽입니다.
화면에는 아무 표시가 없었습니다. 원인을 찾고 보니 시작은 JSON.parse가 던진 SyntaxError 하나였어요.
백틱 안의 tojson
그리는 코드는 이렇게 시작하고 있었습니다.
(function () {
const statsSearchUrl = "{{ stats_search_url }}";
const keywordEntries = JSON.parse(`{{ keyword_entries | tojson }}`);
const usageEntries = JSON.parse(`{{ usage_entries | tojson }}`);
const valueEntries = JSON.parse(`{{ value_entries | tojson }}`);
Jinja의 tojson은 파이썬 값을 JSON 문자열로 내려줍니다. 그 문자열을 백틱으로 감싸 JS 문자열로 만든 다음 JSON.parse로 다시 객체로 돌리고 있었어요. 한 번 문자열이 됐다가 다시 객체가 되는, 왕복이 하나 더 있는 구조입니다.
이 왕복이 문제였습니다. JSON 안에 줄바꿈이 든 값이 있으면 tojson은 "참고사항\n둘째 줄"처럼 \n 두 글자로 내려줍니다. JSON으로는 맞아요. 그런데 이게 백틱 안에 놓이는 순간 JS 파서가 먼저 읽습니다. 템플릿 리터럴에서 \n은 이스케이프 시퀀스라 실제 줄바꿈 문자로 바뀌고, 그 결과 문자열이 JSON.parse에 들어갑니다. JSON 문자열 값 안에 진짜 줄바꿈이 있으면 문법 오류예요.
파이썬 값 참고사항⏎둘째 줄 (줄바꿈 문자 하나)
tojson 출력 "참고사항\n둘째 줄" (백슬래시 + n 두 글자, JSON으로 유효)
백틱이 읽은 뒤 "참고사항
둘째 줄" (진짜 줄바꿈 — JSON으로 무효)
JSON.parse SyntaxError
valueEntries 한 줄에서 던진 예외가 IIFE 전체를 세웁니다. 그 아래에 있던 세 탭의 렌더 호출이 전부 안 돌았고, 그래서 검색어 탭처럼 줄바꿈이 없는 데이터까지 같이 빈 것이었어요. 한 탭의 데이터가 세 탭을 죽였습니다.
고친 것은 왕복을 없앤 것
+ // ⚠️ tojson 을 백틱 안에 넣고 JSON.parse 하지 말 것 — 값에 든 `\n` 을
+ // 템플릿 리터럴이 먼저 실제 줄바꿈으로 바꿔 JSON.parse 가 통째로 죽는다
+ // (참고사항 값 여러 건이 줄바꿈을 담고 있다). tojson 은 그대로 JS 리터럴이다.
const statsSearchUrl = "{{ stats_search_url }}";
- const keywordEntries = JSON.parse(`{{ keyword_entries | tojson }}`);
- const usageEntries = JSON.parse(`{{ usage_entries | tojson }}`);
- const valueEntries = JSON.parse(`{{ value_entries | tojson }}`);
+ const keywordEntries = {{ keyword_entries | tojson }};
+ const usageEntries = {{ usage_entries | tojson }};
+ const valueEntries = {{ value_entries | tojson }};
JSON은 JS 리터럴의 부분집합입니다. tojson이 낸 문자열을 그대로 코드 자리에 놓으면 JS 파서가 객체로 읽어요. JSON.parse도, 백틱도 필요 없습니다. tojson은 <·>·&도 유니코드 이스케이프로 바꿔 주기 때문에 </script>가 값에 들어 있어도 스크립트 태그가 끊기지 않습니다.
같은 패턴이 하나 더 있었습니다. 설문 내용 관리 화면이 날짜 필드 이름 목록을 같은 방식으로 받고 있었어요.
- const dateFields = JSON.parse(`{{ ["start_date", "end_date"] | tojson }}`);
+ const dateFields = {{ ["start_date", "end_date"] | tojson }};
이쪽은 값이 고정 문자열 둘이라 터질 일이 없었는데, 패턴이 같으니 같이 바꿨습니다. 저장소에서 JSON.parse(`{{를 다시 찾으면 0건입니다.
왜 그동안 안 터졌나
이 패턴은 7월 20일에 워드클라우드를 검색어 빈도 기반으로 개편할 때 들어왔습니다. 그때 이 줄에 실린 건 검색어 목록이었고, 검색어에는 줄바꿈이 없어요. 7월 31일에 등록 데이터 어휘 탭이 추가되면서 valueEntries가 생겼고, 이 탭이 참고사항 같은 서술형 값을 모읍니다.
그러니까 줄바꿈이 든 참고사항이 처음 저장된 날부터 이 화면은 비어 있었을 겁니다. 그게 언제인지는 모릅니다. 데이터가 들어온 시점은 git에 없고, 화면이 빈 것을 알아챈 날과 실제로 비기 시작한 날 사이가 얼마인지도 몰라요. 오류가 콘솔에만 찍히고 화면은 조용히 비는 구조라 화면에서는 알림이 없었습니다. 구름이 그려지는지 보는 e2e는 8월 1일부터 있었지만, 오래 빨갛던 다른 실패들 사이에 섞여 있었던 것으로 보입니다(빨갛게 남아 있던 e2e 25건 중 코드 회귀는 1건이었다2026-09-03빨갛게 남아 있던 e2e 25건 중 코드 회귀는 1건이었다 — 낡은 계약·조용한 skip·잔재를 가르기Playwright 전체 스위트가 235 통과 · 25 실패 · 11 skip인 채로 한동안 있었습니다. 25건을 하나씩 열어 갈라 보니 코드 회귀 1 · 데이터 없음 4 · 나머지는 화면과 데이터를 못 따라간 낡은 스펙이었어요. 빨간불이 오래 쌓이면 진짜 회귀가 그 안에 묻힙니다. 같은 날 선택자가 사라져 영영 건너뛰던 스펙, 저장한 뒤에 건너뛰어 데이터를 남기던 스펙도 함께 고쳐, 다시 돌린 274건이 실패 0으로 끝났습니다.).
검증
- 고친 뒤 세 탭이 다시 그려지는 것 확인. 등록 데이터 어휘 탭에 줄바꿈이 든 값이 실제로 여러 건 있었습니다.
- 저장소 템플릿에서
JSON.parse(`{{패턴 검색 → 0건. 워드클라우드와 설문 화면 두 곳이 전부였습니다. - 값이 없을 때는
tojson이[]를 내려주고, 리터럴로 놓으면 그대로 빈 배열입니다. 예외가 날 자리가 파싱 단계에서 사라졌습니다.
남은 것 · 한계
- 한 탭의 예외가 세 탭을 죽이는 구조는 그대로입니다. 세
const가 한 IIFE 안에 있어서, 다음에 다른 이유로 하나가 던지면 또 셋이 같이 빕니다. 탭마다try로 감싸거나 렌더를 나누는 편이 맞는데, 이번엔 원인만 고쳤어요. - 화면이 비어도 아무 말을 안 합니다. 데이터가 0건이라 빈 것과 스크립트가 죽어서 빈 것이 같은 모양이에요. 빈 상태 문구는 데이터 0건에만 뜨고, 예외로 멈추면 그 문구조차 안 나옵니다.
- 줄바꿈 값 전용 회귀 테스트는 안 붙였습니다. 첫 탭 구름이 그려지는지 보는 e2e는 8월 1일부터 있었지만, 줄바꿈이 든 값을 넣고 화면을 여는 경우는 따로 잠그지 않고 주석으로만 막아 뒀어요. 주석은 같은 파일을 고치는 사람만 읽습니다.
- 언제부터 비어 있었는지 모른다는 것 자체가 한계입니다. 프런트 예외를 서버로 보내는 장치가 없어서, 사용자가 말해 주기 전까지 이런 종류는 계속 조용합니다.
관련 글: Django 프로젝트에 D3.js 시각화 붙이기2026-04-18Django 프로젝트에 D3.js 시각화 붙이기 — 워드클라우드, 히트맵, 데이터 맵jQuery 기반 Django 프로젝트에 D3.js 시각화를 도입한 경험을 정리합니다. 공통 그래프 유틸 설계, 실데이터 부재 시 샘플 폴백, 줌/중앙정렬 개선 포인트를 다룹니다. · 아무도 안 묻는 질문에 답하던 화면 — 워드클라우드를 검색어 빈도로2026-07-21아무도 안 묻는 질문에 답하던 화면 — 워드클라우드를 검색어 빈도로ASN.1 노드 단어를 세어 서버에서 PNG로 그려주던 워드클라우드를, 통합검색 검색어를 세어 클라이언트에서 그리는 화면으로 바꿨습니다. 화면의 성격을 "무엇이 등록됐나"에서 "무엇을 찾았나"로 옮긴 결정과, 같은 집계를 두 군데 두는 바람에 같은 검색어가 화면마다 다른 비율로 보이게 된 이야기입니다. · Jinja 템플릿 정리2025-04-21Jinja 템플릿 정리 — 문법 레퍼런스와 Django에서 실제로 쓰며 배운 것Jinja 템플릿의 문법·필터·상속·매크로를 표로 정리한 레퍼런스입니다. Django 프로젝트에서 Jinja2를 실제로 쓰며 배운 것들을 뒤에 붙였습니다 — url·static이 공짜가 아니라는 것, safe 필터가 사고의 지름길이라는 것, 그리고 원문 필터 표의 예시가 문법부터 틀려 있었다는 고백까지.