이 글은 Today I Learn 시리즈의 208번째 기록입니다. (총 213개)

어디부터 보나

PDF가 너무 컸다

방 안쪽을 흰색으로 채우는 기능을 넣고 PDF로 뽑아 보니 파일이 너무 컸다. 그래서 용량을 줄여 달라고 했다. 압축을 켜는 것으로 91MB가 0.2MB가 됐다.

곧이어 배치도 여러 개를 한 PDF로 묶는 기능에 여덟 개를 넣었더니, 이번엔 PDF가 아예 안 만들어졌다. 큰 도면 두 장·세 장으로 여러 번 재현해 봤지만 전부 정상이었다.


콘솔 한 줄이 원인을 다 말하고 있었다

넣은 파일은 .ol 여덟 개, 합계 17.2MB였고, 브라우저 콘솔(개발자 도구 안의 오류 기록 창)에는 RangeError: Invalid string length가 떠 있었다. 이 한 줄을 보자마자 원인이 나왔다.

그래서 앞으로는 문제가 생기면 콘솔부터 확인하자고 했다. 증상은 겪은 사람이 보는 결과고, 콘솔은 프로그램이 남긴 원인이다. 증상만 들고 재현부터 시작하면 원인에 닿기까지 몇 배가 걸린다.


재현이 안 된다고 정상인 것은 아니었다

두 장·세 장은 되고 여덟 장에서 터지는 문제였다. 한도를 넘기는 지점이 그 사이에 있었다.

재현이 통과했다는 건 “문제가 없다”가 아니라 “그 조건이 문제를 안 건드렸다”는 뜻이다. 개수·크기가 방아쇠인 문제는 겪은 사람의 숫자를 그대로 맞춰 봐야 한다.


두 증상이 한 뿌리였다

앞의 “PDF가 너무 크다”와 뒤의 “PDF가 안 만들어진다”는 다른 문제로 보였다. 그런데 뿌리가 같았다 — 압축이 꺼져 있어 그림이 통째로 들어가고 있었다. 파일이 컸던 이유와 여덟 장에서 터진 이유가 같다.

시간차를 두고 온 증상 두 개는 따로 고치기 쉽다. “같은 원인일 수 있나”를 먼저 한 번 물어보는 편이 낫다.


올려야 확인할 수 있었다

고쳤으니 다시 해 보라는데, 고친 코드는 아직 올라가 있지 않았다. 나는 배포된 주소에서 쓰니 push(고친 코드를 저장소에 올려 배포되게 하는 일)를 해 줘야 확인할 수 있다고 했다.

증상을 알려 준 쪽이 확인하는 곳이 배포본이라면, 고친 뒤 올리는 데까지가 한 묶음이다. 올리고 나서 다시 돌려 보니 여덟 장이 제대로 묶여 나왔다.


알게 된 원리

PDF 안의 그림은 스트림이고, 압축은 선택이다

PDF는 그림을 스트림(덩어리로 담긴 데이터)으로 품고, 그 스트림을 어떤 방식으로 압축했는지 함께 적어 둔다. 압축을 지정하지 않으면 픽셀 값이 그대로 들어간다.

7500 × 5300 도면 한 장이면 약 4천만 픽셀이고, 픽셀당 3바이트니 원본만 100MB가 넘는다. 실제로 도면 한 장짜리 PDF가 91MB로 나왔다. 압축을 켜니 같은 그림이 0.2MB가 됐다.

jsPDF(브라우저 안에서 PDF를 만드는 자바스크립트 라이브러리)에서 압축은 켜야 켜지는 선택 항목이다. 기본값이 안전한 쪽일 거라고 믿으면 이런 일이 난다.


자바스크립트 문자열에는 최대 길이가 있다

jsPDF는 PDF 문서 전체를 문자열 하나에 이어 붙인 뒤 파일로 내보낸다. 그런데 크롬이 쓰는 자바스크립트 엔진에는 문자열 최대 길이가 있다(대략 5억 자).

압축이 꺼져 있으면 도면 한 장이 그 한도의 3분의 1쯤을 먹는다. 그래서 두세 장까지는 되고 여덟 장에서 터진다. RangeError: Invalid string length가 정확히 그 말이었다.

용량 문제는 “파일이 크다”로 끝나지 않는다. 어느 지점에선가 한도를 넘기면 기능 자체가 죽는다.


무손실과 손실은 그림 종류에 따라 승자가 바뀐다

PNG는 무손실이다 — 원본 픽셀을 그대로 되살린다. 같은 색이 넓게 이어지면 아주 잘 줄어드는데, 선 도면이 딱 그런 그림이다.

JPEG는 손실이다 — 눈에 잘 안 띄는 정보를 버려서 줄인다. 스캔한 도면처럼 픽셀마다 잡티가 있으면, PNG는 그 잡티까지 전부 보존하느라 오히려 커진다.

같은 크기로 재보니 이렇게 갈렸다.

  • 선 도면: PNG 0.2MB
  • 잡티 있는 도면: PNG 26MB · JPEG 8.6MB

그래서 무손실을 기본으로 두되, PNG가 일정 크기를 넘으면 그때만 JPEG로 바꾸게 했다. “무조건 무손실”도 “무조건 압축”도 답이 아니었고, 그림 성격이 기준이 된다.

손실 압축을 쓸지는 눈으로 확인해야 안다. 같은 자리를 1:1 크기로 잘라 나란히 놓고 보니 선도 글자도 구분되지 않았다. 축소한 화면에서 “괜찮네” 하는 건 확인이 아니다.


결과를 말하지 않으면 성공도 실패처럼 보인다

여덟 장이 안 만들어졌을 때 화면은 아무 말이 없었다. 앞선 TIL 10에서 정리한 원칙 — 결과를 안 보여 주면 성공도 실패처럼 보인다 — 이 여기서도 그대로였다. 이번엔 「PDF 만드는 중」, 결과 용량, 실패 사유를 상태 문구에 적게 했다. 다시 돌렸을 때 「8개 변환 완료 · 2.4MB」가 떠서 성공을 바로 알았다.


Series: Today I Learn

1 C++ 자료형(Data Type) 2 MD5 vs pHash 3 C++에서 함수의 선언과 정의 4 Tkinter padx, pady 5 메모리와 포인터 변수 6 Call by Value, Call by Reference, Call by Pointer 비교 7 const 8 Gemfile — Jekyll 프로젝트의 의존성 파일 9 kramdown-parser-gfm — Jekyll의 GFM 파서 10 파서(Parser) 11 AHU vs OHU 12 I might try it vs I'll try it 뉘앙스 차이 13 SESSION_EXPIRE_AT_BROWSER_CLOSE=True 14 configuration key 15 Git stash vs discard 16 subprocess.Popen으로 Windows 탐색기에 명령어를 전달 17 Post 잔디 분석하기 18 Google Sheets Sync 최적화 19 DSL (Domain Specific Language)과 GPL (General Purpose Language) 20 마크다운 표 그리는 방법 21 쿼리 파라미터(Query Parameter). 기존 QR코드 재활용 22 Django 보안 취약점 점검 및 수정 23 OOP Object-Oriented Programming 객체 지향 프로그래밍 24 Fernet 대칭 암호화 25 Jekyll 코드블록 안의 Liquid 태그 26 습관만들기 Pawbit TIL 1 27 insertOnConflictUpdate vs DoUpdate(target) 28 세션 필터 29 아코디언(Accordiaon) UI를 펼친상태로 만들기 30 input의 step 31 습관만들기 Pawbit TIL 2 32 습관만들기 Pawbit TIL 3 33 Word Cloud 34 Google Sheets를 데이터 버스로(with AppSheet) 35 Django 모델 텍스트 필드 자동 수집 패턴 36 localStorage로 섹션 토글 상태 유지 37 순차 ID 생성(`select_for_update()` + `max()` 조합) 38 역참조 검색과 distinct() 39 xlsx 다운로드와 로딩 오버레이 충돌 40 Android 파일 공유 MIME 타입 41 AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 42 크레센도 Phaser + TypeScript TIL 1 43 크레센도 Phaser + TypeScript TIL 2 44 크레센도 Phaser + TypeScript TIL 3 45 크레센도 Phaser + TypeScript TIL 4 46 크레센도 Phaser + TypeScript TIL 5 47 여둘까 Office Layout TIL 1 48 습관만들기 Pawbit TIL 4 49 여둘까 Office Layout TIL 2 50 여둘까 Office Layout TIL 3 51 크레센도 Phaser + TypeScript TIL 6 52 UTF-8 BOM과 PowerShell 파일 쓰기 53 소리꽃 KeyBloom TIL 1 54 습관만들기 Pawbit TIL 5 55 여둘까 Office Layout TIL 4 56 소리꽃 KeyBloom TIL 2 57 소리꽃 KeyBloom TIL 3 58 메트로놈 Simple Metronome TIL 1 59 소리꽃 KeyBloom TIL 4 60 메트로놈 Simple Metronome TIL 2 61 소리꽃 KeyBloom TIL 5 62 습관만들기 Pawbit TIL 6 63 크레센도 Phaser + TypeScript TIL 7 64 메트로놈 Simple Metronome TIL 3 65 소설 타임라인 관리 도구 Lumior TIL 1 66 습관만들기 Pawbit TIL 7 67 정적 블로그 SEO 정비와 Pagefind 검색 도입 68 크레센도 Phaser + TypeScript TIL 8 69 메트로놈 Simple Metronome TIL 4 70 습관만들기 Pawbit TIL 8 71 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 72 메트로놈 Simple Metronome TIL 5 73 모래게임 Sandrop TIL 1 74 습관만들기 Pawbit TIL 9 75 메트로놈 Simple Metronome TIL 6 76 모래게임 Sandrop TIL 2 77 모래게임 Sandrop TIL 3 78 습관만들기 Pawbit TIL 10 79 습관만들기 Pawbit TIL 11 80 메트로놈 Simple Metronome TIL 7 81 모래게임 Sandrop TIL 5 82 모래게임 Sandrop TIL 4 83 습관만들기 Pawbit TIL 12 84 모래게임 Sandrop TIL 6 85 소리꽃 KeyBloom TIL 6 86 모래게임 Sandrop TIL 7 87 모래게임 Sandrop TIL 8 88 소리꽃 KeyBloom TIL 7 89 모래게임 Sandrop TIL 9 90 소리꽃 KeyBloom TIL 8 91 모래게임 Sandrop TIL 10 92 소리꽃 KeyBloom TIL 9 93 콘티온 ContiOn TIL 1 94 소리꽃 KeyBloom TIL 10 95 콘티온 ContiOn TIL 2 96 습관만들기 Pawbit TIL 13 97 콘티온 ContiOn TIL 3 98 모래게임 Sandrop TIL 11 99 소리꽃 KeyBloom TIL 11 100 여둘까 Office Layout TIL 5 101 모래게임 Sandrop TIL 12 102 모래게임 Sandrop TIL 13 103 콘티온 ContiOn TIL 4 104 모래게임 Sandrop TIL 14 105 소리꽃 KeyBloom TIL 12 106 습관만들기 Pawbit TIL 14 107 모래게임 Sandrop TIL 15 108 모래게임 Sandrop TIL 16 109 소리꽃 KeyBloom TIL 13 110 온실 GreenHouse TIL 1 111 콘티온 ContiOn TIL 5 112 모래게임 Sandrop TIL 17 113 소리꽃 KeyBloom TIL 14 114 온실 GreenHouse TIL 2 115 콘티온 ContiOn TIL 6 116 모래게임 Sandrop TIL 18 117 콘티온 ContiOn TIL 7 118 모래게임 Sandrop TIL 19 119 소리꽃 KeyBloom TIL 15 120 모래게임 Sandrop TIL 20 121 모래게임 Sandrop TIL 21 122 소리꽃 KeyBloom TIL 16 123 소리꽃 KeyBloom TIL 17 124 소리꽃 KeyBloom TIL 18 125 여둘까 Office Layout TIL 6 126 소리꽃 KeyBloom TIL 19 127 소리꽃 KeyBloom TIL 20 128 콘티온 ContiOn TIL 8 129 콘티온 ContiOn TIL 9 130 로그스톤 상표 셀프 출원 TIL 1 131 소리꽃 KeyBloom TIL 21 132 로그스톤 샵 LogStone Shop TIL 1 133 소리꽃 KeyBloom TIL 22 134 로그스톤 샵 LogStone Shop TIL 2 135 소리꽃 KeyBloom TIL 23 136 모래게임 Sandrop TIL 22 137 모래게임 Sandrop TIL 23 138 소리꽃 KeyBloom TIL 24 139 로그스톤 샵 LogStone Shop TIL 3 140 모래게임 Sandrop TIL 24 141 소리꽃 KeyBloom TIL 25 142 모래게임 Sandrop TIL 25 143 MiniMacro TIL 1 144 모래게임 Sandrop TIL 26 145 콘티온 ContiOn TIL 10 146 Astro가 무엇인지, 왜 옮기는지 147 모래게임 Sandrop TIL 27 148 소리꽃 KeyBloom TIL 26 149 MiniMacro TIL 2 150 로그스톤 샵 LogStone Shop TIL 4 151 메트로놈 Simple Metronome TIL 8 152 모래게임 Sandrop TIL 28 153 여둘까 Office Layout TIL 7 154 메트로놈 Simple Metronome TIL 9 155 블로그 Astro 이관 TIL 156 여둘까 Office Layout TIL 8 157 로그스톤 샵 LogStone Shop TIL 5 158 모래게임 Sandrop TIL 29 159 오픈데이 Openday TIL 1 160 로그스톤 샵 LogStone Shop TIL 6 161 모래게임 Sandrop TIL 30 162 오픈데이 Openday TIL 2 163 모래게임 Sandrop TIL 31 164 Unreal Engine MCP TIL 1 165 모래게임 Sandrop TIL 32 166 소리꽃 KeyBloom TIL 27 167 콘티온 ContiOn TIL 11 168 오픈데이 Openday TIL 3 169 로그스톤 샵 LogStone Shop TIL 7 170 모래게임 Sandrop TIL 33 171 소리꽃 KeyBloom TIL 28 172 오픈데이 Openday TIL 4 173 여둘까 Office Layout TIL 9 174 오픈데이 Openday TIL 5 175 ScorePlayer TIL 1 176 모래게임 Sandrop TIL 34 177 BuildingHub TIL 1 178 ScorePlayer TIL 2 179 여둘까 Office Layout TIL 10 180 BuildingHub TIL 2 181 ScorePlayer TIL 3 182 Unreal Engine MCP TIL 2 183 궁 미로 PalaceMaze TIL 1 184 메트로놈 Simple Metronome TIL 10 185 메트로놈 Simple Metronome TIL 11 186 오픈데이 Openday TIL 6 187 BuildingHub TIL 3 188 Obsidian vault에서 코드만 빼기 — directory junction 189 ScorePlayer TIL 4 190 여둘까 Office Layout TIL 11 191 오픈데이 Openday TIL 7 192 URL은 바뀔 수 있는 것에 묶지 않는다 193 도구는 기능이 아니라 내 일에 맞는지로 고른다 194 돌이키기 힘든 변경은 작게 먼저 확인한다 195 궁 미로 PalaceMaze TIL 2 196 궁 미로 PalaceMaze TIL 3 197 소리꽃 KeyBloom TIL 29 198 오픈데이 Openday TIL 8 199 BuildingHub TIL 4 200 ScorePlayer TIL 5 201 로그스톤 샵 LogStone Shop TIL 8 202 여둘까 Office Layout TIL 12 203 오픈데이 Openday TIL 9 204 BuildingHub TIL 5 205 MiniMacro TIL 3 206 소리꽃 KeyBloom TIL 30 207 오픈데이 Openday TIL 10 208 여둘까 Office Layout TIL 13 읽는 중 209 소리꽃 KeyBloom TIL 31 210 궁 미로 PalaceMaze TIL 4 211 로그스톤 샵 LogStone Shop TIL 9 212 로그스톤 샵 LogStone Shop TIL 10 213 소리꽃 KeyBloom TIL 32