- 발행일
워드클라우드가 통째로 안 그려졌다 — 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에 없고, 화면이 빈 것을 알아챈 날과 실제로 비기 시작한 날 사이가 얼마인지도 몰라요. 오류가 콘솔에만 찍히고 화면은 조용히 비는 구조라 알림이 없었습니다.
검증
- 고친 뒤 세 탭이 다시 그려지는 것 확인. 등록 데이터 어휘 탭에 줄바꿈이 든 값이 실제로 여러 건 있었습니다.
- 저장소 템플릿에서
JSON.parse(`{{패턴 검색 → 0건. 워드클라우드와 설문 화면 두 곳이 전부였습니다. - 값이 없을 때는
tojson이[]를 내려주고, 리터럴로 놓으면 그대로 빈 배열입니다. 예외가 날 자리가 파싱 단계에서 사라졌습니다.
남은 것 · 한계
- 한 탭의 예외가 세 탭을 죽이는 구조는 그대로입니다. 세
const가 한 IIFE 안에 있어서, 다음에 다른 이유로 하나가 던지면 또 셋이 같이 빕니다. 탭마다try로 감싸거나 렌더를 나누는 편이 맞는데, 이번엔 원인만 고쳤어요. - 화면이 비어도 아무 말을 안 합니다. 데이터가 0건이라 빈 것과 스크립트가 죽어서 빈 것이 같은 모양이에요. 빈 상태 문구는 데이터 0건에만 뜨고, 예외로 멈추면 그 문구조차 안 나옵니다.
- 회귀 테스트를 안 붙였습니다. 줄바꿈이 든 값을 넣고 화면을 여는 e2e 한 건이면 잡히는 종류인데, 주석으로만 막아 뒀어요. 주석은 같은 파일을 고치는 사람만 읽습니다.
- 언제부터 비어 있었는지 모른다는 것 자체가 한계입니다. 프런트 예외를 서버로 보내는 장치가 없어서, 사용자가 말해 주기 전까지 이런 종류는 계속 조용합니다.
관련 글: Django 프로젝트에 D3.js 시각화 붙이기 · 아무도 안 묻는 질문에 답하던 화면 — 워드클라우드를 검색어 빈도로 · Jinja 템플릿 정리