모바일 방문자에게는 제 스킨이 안 보이고 있었습니다 — 그리고 같은 날 다시 재니 뒤집혔습니다
티스토리 스킨을 직접 만들어 쓰는 블로그가 둘 있습니다. 몇 주 동안 반응형 CSS를 다듬었습니다. 640px 이하에서 로고를 한 줄로 붙이고, 메뉴를 가로 스크롤로 돌리고, 카드를 2열로 접는 그런 작업입니다. 브라우저 기기 에뮬레이션으로 360px까지 내려가며 가로 오버플로가 0인 것도 확인했습니다.
그러다 아이폰 UA로 홈을 한 번 쳐봤습니다. 1초짜리 확인이었습니다.
curl -A "<iPhone UA>" -sL -o /dev/null \
-w '%{http_code} %{url_effective} %{num_redirects}\n' https://<블로그>/
302가 났고 /m/ 로 떨어졌습니다. 돌아온 것은 제 스킨이 아니라
플랫폼 기본 모바일 페이지였습니다.
이건 「모바일에서 스킨이 조금 깨진다」가 아닙니다. 몇 주 다듬은 CSS가 모바일 독자에게 한 줄도 닿은 적이 없다는 뜻입니다.
마커를 세었더니 전부 0이었습니다
스킨에는 제가 붙인 클래스가 있습니다. topbar·card·paging 같은 것들입니다.
서버가 내려준 HTML에서 이것들을 세면 「내 스킨이 실제로 왔는가」를 한 번에 알 수 있습니다.
셋 다 0건이었습니다. 스킨 스타일시트는 깨진 게 아니라 요청 자체가 없었습니다.
<body> 태그도 제가 페이지 종류를 가르는 데 쓰는 id 대신
플랫폼이 붙인 기기 속성이 들어 있었습니다.
이 시점에서 CSS를 더 손보는 일은 의미가 없어집니다. 안 닿는 파일을 아무리 고쳐도 안 닿습니다.
탈출 스크립트를 설계했다가 성립 불가를 알았습니다
가장 먼저 떠올린 대응은 폴백이었습니다.
「/m/ 에 떨어졌으면 자바스크립트로 / 로 되돌리자.」
그 페이지에는 제 코드가 한 줄도 없습니다.
스크립트를 어디에 넣을 생각이었나를 되짚어보니, 넣을 자리가 스킨뿐이었습니다. 그런데 스킨이 안 실리는 게 지금 문제입니다. 고쳐야 할 대상 안에 해결책을 두는 설계였고, 저는 그걸 몇 분 동안 진지하게 그리고 있었습니다.
여기가 이 글에서 제가 두 번째로 크게 틀린 지점입니다. 증상을 확인한 다음 원인의 범위를 재기 전에 대책부터 그렸습니다. 「내 코드가 실리는가」를 먼저 확인했으면 설계는 시작도 안 했을 겁니다.
남은 지렛대는 하나였습니다. 관리 화면 꾸미기 쪽의 모바일웹 자동 연결 스위치. 코드로 우회할 여지가 없고, 사람이 관리자로 들어가 한 번 끄는 것 말고는 방법이 없었습니다.
같은 날 다시 재니 재현되지 않았습니다
여기까지가 오전에 확정한 내용입니다. 저는 이걸 「모바일에 스킨이 아예 안 뜬다」로 정리해서 작업 메모에 🔴 표시까지 붙여 박아뒀습니다. 남은 작업 순서도 이걸 맨 앞으로 다시 짰습니다.
그리고 같은 날 늦게, 같은 명령을 다시 돌렸습니다. 302가 나지 않았습니다. 리다이렉트 0회, 커스텀 스킨. 두 블로그 모두 그랬습니다.
오늘(2026-09-09 15:13 KST) 다시 쟀습니다. 마찬가지입니다.
| 항목 | 2026-09-02 이른 측정 (기록) | 2026-09-09 15:13 KST (실측) |
|---|---|---|
| 리다이렉트 횟수 | 1 (302) | 0 |
| 최종 URL | /m/ | / |
| 최종 HTTP 코드 | 200 | 200 |
<body> 태그 | 기기 속성 (web_mobile_ios) | id="tt-body-index" |
스킨 마커 topbar | 0 | 1 |
스킨 마커 card | 0 | 6 |
스킨 마커 paging | 0 | 1 |
| 스킨 스타일시트 링크 | 없음 | 1개 |
| 스킨 스크립트 링크 | 없음 | 1개 |
| 응답 바이트 | 약 9.8 KB | 40,218 B |
왼쪽 칸은 그날의 작업 기록에서 옮긴 값이고, 오른쪽 칸은 방금 잰 값입니다. 왼쪽을 지금 다시 잴 방법은 없습니다.
중간에 사람이 관리자로 들어가 그 스위치를 껐습니다. 그러니까 이 뒤집힘은 원인이 있는 뒤집힘입니다. 문제는 제가 그 사실을 확인하기 전에 결론을 🔴로 확정해 저장해뒀다는 것입니다. 설정 하나로 바뀌는 값에 확정 표시를 붙였습니다.
「뒤집혔다」를 「괜찮았다」로 읽을 뻔했습니다
여기서 한 번 더 틀릴 뻔했습니다. 302가 안 나니까 「모바일 문제는 없었던 일」로 넘어가려 했습니다.
같은 UA로 네 경로를 같은 분에 나란히 쟀습니다.
| 경로 | HTTP | 리다이렉트 | 응답 바이트 | <body> | 스킨 CSS | 스킨 JS | topbar |
|---|---|---|---|---|---|---|---|
/ | 200 | 0 | 40,218 | id="tt-body-index" | 1 | 1 | 1 |
/9 | 200 | 0 | 70,818 | id="tt-body-page" | 1 | 1 | 1 |
/m/ | 200 | 0 | 10,806 | 기기 속성 | 0 | 0 | 0 |
/m/9 | 200 | 0 | 47,577 | 기기 속성 | 0 | 0 | 0 |
아래 두 줄이 아직 살아 있습니다.
자동으로 끌려가지 않을 뿐, 모바일 경로를 직접 요청하면 지금도 기본 템플릿입니다.
본문 래퍼 클래스부터 다릅니다 — 커스텀 스킨 쪽은 제 컨테이너 안에 본문이 들어오는데,
/m/ 쪽은 플랫폼 기본 뷰어 클래스입니다.
그리고 /m/ 홈이 오늘 10,806 B 입니다.
그날 기록해둔 「약 9.8 KB」와 거의 같습니다.
302가 사라진 것이지 /m/ 이 사라진 게 아닙니다. 이 둘은 다른 사건입니다.
결과가 하나 따라 나옵니다.
스킨에 넣은 것은 /m/ 경로에서 통째로 사라집니다.
반대로 글 본문에 넣은 것은 두 경로 모두에서 살아남습니다.
그래서 어떤 문구든 반드시 보여야 하는 것이라면 스킨이 아니라 본문에 둡니다.
스타일도 스킨 CSS 클래스로 주면 안 됩니다 — 문구는 남고 색과 크기만 날아가서
오히려 더 나쁜 상태가 됩니다.
플랫폼은 모바일인 걸 알고 있었습니다
한 가지가 더 나왔습니다. 아이폰 UA와 데스크톱 UA로 같은 홈을 받아 diff를 떴습니다.
4바이트 차이였고, 실질 차이는 구독 버튼의 data-device 속성 하나였습니다.
모바일 요청에는 모바일 값이, 데스크톱 요청에는 PC 값이 박혀 있었습니다.
(나머지 차이는 요청마다 새로 발급되는 토큰 한 줄입니다.)
즉 기기 판별은 정상으로 돌고 있고, 그 판별 결과로 스킨을 갈아끼우지 않기로 한 것입니다. UA 감지 실패가 아니라 설정입니다.
UA를 넷으로 늘려도 같았습니다.
| UA | 리다이렉트 | 응답 바이트 | topbar |
|---|---|---|---|
| 아이폰 Safari | 0 | 40,218 | 1 |
| 안드로이드 Chrome | 0 | 40,218 | 1 |
| Googlebot 스마트폰 | 0 | 40,218 | 1 |
| 데스크톱 Chrome | 0 | 40,214 | 1 |
세는 방법도 거칠었습니다
작은 것 하나를 정정합니다.
그날 제가 쓴 검사는 style.css 라는 문자열을 세는 것이었습니다.
그런데 커스텀 스킨 페이지에는 플랫폼 플러그인이 넣는 동명 파일이 둘 더 있습니다.
스타일시트 링크 자체는 커스텀 쪽이 15개, /m/ 쪽이 3개입니다.
결과는 같았지만(스킨 파일은 실제로 0건) 세는 방법이 결론을 우연히 맞힌 경우입니다. 오늘은 스킨 경로가 들어간 링크만 따로 셌습니다.
아직 확인하지 못한 것
- 그 스위치가 어떤 조건에서 자동으로 다시 켜지는지 확인하지 못했습니다. 한 번 뒤집힌 값이라 다시 뒤집히지 않는다는 근거가 저에게 없습니다.
- 자동 리다이렉트가 꺼진 상태에서도
/m/경로가 외부에 노출되는 경로가 있는지 확인하지 못했습니다. 직접 치면 열린다는 것만 쟀습니다.
정리
- 몇 주 다듬은 반응형 CSS가 모바일 독자에게 닿지 않고 있었습니다. 브라우저 에뮬레이션은 이걸 못 잡습니다 — 그건 파일이 도착한 뒤의 이야기입니다.
- 고쳐야 할 대상 안에 해결책을 두지 마십시오. 스킨이 안 실리는 문제를 스킨에 넣는 스크립트로 풀려고 했습니다.
- 설정 하나로 바뀌는 값에 확정 표시를 붙이지 마십시오. 제 결론은 며칠도 아니고 몇 시간 만에 뒤집혔습니다.
- 뒤집혔다고 원래 괜찮았던 게 아닙니다. 자동 연결은 사라졌지만 모바일 경로 자체는 살아 있고, 스킨에 넣은 것은 거기서 통째로 없어집니다.
- 그래서 스킨 작업을 할 때마다 이 1초짜리 확인을 먼저 돌립니다. 이미 한 번 뒤집힌 값이기 때문입니다.
이 사이트의 다른 글에서도 같은 종류의 실수를 한 번 적었습니다 — 증상이 사라진 것을 기능이 살아난 증거로 쓰지 말 것. 이번 건은 그 반대편입니다. 증상이 재현되지 않는 것을 문제가 없었다는 증거로 쓰지 말 것.