글 좌우 짤림 사고 — LLM 출력의 `width:800px` 한 줄이 모바일을 망친 사연

3 min read · 609 words

활용 팁 / 블로그 운영 / 디버깅 스토리
약 1,600자

LLM 으로 글 본문을 받아서 그대로 Blogger 에 박았더니 데스크탑은 멀쩡한데 모바일에서 글이 화면 밖으로 삐져나갔습니다. 같은 문제가 사이트 글 41 편에 누적되어 있었습니다. 추적과 일괄 fix 패턴을 풀어 둡니다.

사고 발견

모바일 (375px iPhone) 에서 글을 열어보니 본문이 화면보다 넓어서 가로 스크롤이 발생. 글 안 사진은 화면 밖으로 잘림. 표는 옆으로 미끄러져 사라짐.

데스크탑 (1280px) 에서는 멀쩡해서 한참 동안 못 발견. 사장님이 휴대폰으로 본인 글을 처음 본 날 발견했습니다.

추적 — 본문 안 위험 inline style

HTML 본문을 grep:


grep -oE 'style="[^"]*"' post_body.html | sort | uniq -c | sort -rn | head

가장 흔한 패턴:

  • width:800px (LLM 이 표 너비 명시)
  • margin-left:-30px (LLM 이 좌측 정렬 시도)
  • width:100vw (LLM 이 hero 이미지 풀너비)
  • min-width:600px (LLM 이 카드 최소 너비)
  • position:absolute (LLM 이 배지 floating 시도)

이 다섯 패턴이 모바일 좌우 짤림의 90%. LLM 출력에 자주 나타납니다.

해결 — publish-time sanitize

발행 직전 단계에서 위험 inline-style 자동 strip.


import re

def strip_dangerous_styles(html: str) -> tuple[str, int]:
 n = 0
 def clean(m):
 nonlocal n
 val = m.group(1)
 orig = val
 # 고정 너비 400px+
 val = re.sub(r'width\s*:\s*(?:[4-9]\d{2}|[1-9]\d{3,})px\s*;?\s*', '', val)
 # 100vw
 val = re.sub(r'width\s*:\s*100vw\s*;?\s*', '', val)
 # min-width 400px+
 val = re.sub(r'min-width\s*:\s*(?:[4-9]\d{2}|[1-9]\d{3,})px\s*;?\s*', '', val)
 # 음수 horizontal margin
 val = re.sub(r'margin(?:-left|-right)\s*:\s*-\d+px\s*;?\s*', '', val)
 # position:absolute
 val = re.sub(r'position\s*:\s*absolute\s*;?\s*', '', val)
 val = val.strip().rstrip(';').strip()
 if val != orig:
 n += 1
 return f'style="{val}"' if val else ''
 return m.group(0)
 html = re.sub(r'style="([^"]*)"', clean, html)
 # <img> 태그의 고정 width/height 속성도 제거
 html = re.sub(r'(<img\b[^>]*)\s+width="\d+"', r'\1', html)
 html = re.sub(r'(<img\b[^>]*)\s+height="\d+"', r'\1', html)
 return html, n

# 사용
new_html, fixes = strip_dangerous_styles(html)
print(f"strip count: {fixes}")

이 한 함수만 발행 직전에 호출하면 좌우 짤림 사고가 사라집니다. 동시에 safety CSS 한 줄도 본문 top 에 강제 inject.

safety CSS

본문 컨테이너에 max-width 강제. LLM 이 어떤 inline style 박더라도 컨테이너가 화면 밖으로 안 나가게 안전망.


<style data-tsp-safety-css="v2">
.post-body, .entry-content, .article-content {
 max-width: 100% !important;
 overflow-wrap: anywhere;
 word-break: keep-all;
}
.post-body img, .post-body iframe, .post-body video,
.post-body svg, .post-body canvas {
 max-width: 100% !important;
 height: auto !important;
}
.post-body table {
 width: 100% !important;
 max-width: 100% !important;
}
.post-body pre, .post-body code {
 max-width: 100% !important;
 white-space: pre-wrap;
 word-break: break-word;
 overflow-x: auto;
}
</style>

!important 가 들어가서 LLM 의 inline style 보다 우선. 컨테이너 단단함.

일괄 fix

기존 발행된 41 편에는 적용 안 됨. 그래서 walker 한 번 돌렸습니다.


import asyncio
from googleapiclient.discovery import build

async def fix_all_live():
 svc = build_blogger_service()
 posts = svc.posts().list(blogId=BLOG_ID, maxResults=500).execute()
 fixed = 0
 for p in posts.get("items", []):
 old = p["content"]
 new, n_strip = strip_dangerous_styles(old)
 # safety CSS 강제 inject (idempotent — 옛 marker strip 후 새로 박힘)
 new = re.sub(r'<style[^>]*data-tsp-safety-css[^>]*>.*?</style>', '', new, flags=re.S)
 new = SAFETY_CSS + new
 if new != old:
 svc.posts().patch(blogId=BLOG_ID, postId=p["id"],
 body={"content": new}).execute()
 fixed += 1
 print(f"fixed {fixed} posts")

asyncio.run(fix_all_live())

41 / 41 fixed. 모바일에서 다시 봤을 때 좌우 짤림 0건.

따라하실 분께

LLM 으로 본문 받는 모든 블로그가 같은 사고 가능. 위 두 패턴 (sanitize 함수 + safety CSS) 만 발행 hook 한 곳에 박아두면 평생 예방.

요약: LLM 출력의 fixed-width inline style 이 모바일 좌우 짤림의 90% 원인. 발행 직전 strip 한 줄 + safety CSS 한 블록 = 사고 0.

Category Coverage Notice

This article follows our label-specific editorial criteria. Details:

ToolSignal Pro Editorial

ToolSignal Pro는 AI·IT·소프트웨어 트렌드를 다루는 종합 IT 인사이트 매거진입니다.

이전 글 다음 글