배포하니 CSS가 통째로 사라졌다 — 트레일링 슬래시와 상대경로가 만나는 지점

발행 읽는 시간 약 4 분

  • Next.js
  • 배포
  • 트러블슈팅
  • 정적파일
  • Vercel

글 소개

로컬에서는 멀쩡한데 배포하면 스타일이 하나도 안 먹습니다. Next.js가 주소 끝 슬래시를 떼는 순간 상대경로가 한 단계 위로 풀리기 때문입니다. curl로는 절대 잡히지 않는 종류의 버그라, 원인보다 검증 방법이 더 중요합니다.

정적 페이지 몇 장을 public/ 아래 올리고 배포했습니다. 로컬에서는 완벽했습니다. 배포된 주소를 열었더니 스타일이 하나도 먹지 않은 날것의 HTML이 떴습니다.

파란 밑줄 링크, 기본 세리프 폰트, 거대하게 늘어난 아이콘. CSS·JS·이미지가 전부 안 붙은 화면입니다.

그런데 서버는 정상이었습니다. 파일도 전부 배포돼 있었습니다.


확인한 것부터#

가장 먼저 한 일은 파일이 실제로 올라갔는지 확인하는 것이었습니다.

Bash
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' \
  https://example.com/sample/app/css/site.css
# 200  text/css; charset=utf-8

200이 떨어집니다. JS도, 이미지도, HTML도 전부 200에 MIME 타입까지 정확했습니다. 배포된 HTML을 받아 <link> 태그도 확인했습니다. 경로가 멀쩡히 들어 있었습니다.

HTML
<link rel="stylesheet" href="./css/site.css">

파일 있고, 경로 있고, 응답 200. 그런데 브라우저에서는 안 붙습니다.


범인은 주소 끝 슬래시였다#

Next.js는 기본값이 trailingSlash: false입니다. 주소 끝의 슬래시를 떼어내는 방향으로 리다이렉트합니다.

Bash
curl -sI https://example.com/sample/app/
# HTTP/2 308

308이 떨어집니다. /sample/app/ 으로 요청해도 브라우저는 최종적으로 /sample/app 에 있게 됩니다.

여기서 문제가 생깁니다. 상대경로는 "현재 문서가 있는 디렉터리" 기준으로 풀립니다.

브라우저 주소./css/site.css 가 풀리는 곳
/sample/app//sample/app/css/site.css ✅
/sample/app/sample/css/site.css ❌

슬래시가 떨어지는 순간 app은 디렉터리가 아니라 파일 이름으로 취급됩니다. 기준 디렉터리가 /sample/ 로 한 단계 올라가고, 모든 상대경로가 통째로 어긋납니다.

Bash
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/sample/css/site.css
# 404

브라우저가 실제로 요청한 주소는 이쪽이었고, 당연히 404였습니다.


왜 curl로는 안 잡혔나#

이 버그의 진짜 교훈은 원인이 아니라 내 검증 방법이 이 버그를 구조적으로 못 잡는다는 것이었습니다.

Bash
curl .../sample/app/css/site.css   # 200 — 내가 만든 절대 주소

curl에 넘긴 것은 내가 손으로 조립한 완성된 주소입니다. 상대경로 해석 단계를 아예 거치지 않습니다. 그래서 파일이 존재하기만 하면 무조건 200이 나옵니다.

브라우저는 다릅니다. 리다이렉트로 바뀐 최종 주소를 기준으로 ./css/를 직접 계산해서 요청합니다. 그 계산 결과가 틀렸다는 사실은 브라우저만 알 수 있습니다.

절대 URL을 찍는 검증은 "파일이 있는가"만 답한다. "브라우저가 그 파일을 찾아가는가"는 답하지 못한다.

배포 후 자동 점검 스크립트가 전부 200을 뱉는 동안, 화면은 계속 깨져 있었습니다.


고친 방법#

1. 에셋은 절대경로로#

가장 확실한 해법입니다. 상대경로 해석 자체를 없앱니다.

HTML
<!-- 리다이렉트에 따라 풀리는 위치가 달라짐 -->
<link rel="stylesheet" href="./css/site.css">

<!-- 항상 같은 곳 -->
<link rel="stylesheet" href="/sample/app/css/site.css">

CSS 안에서 쓰는 url(../images/bg.jpg) 같은 경로는 그대로 둬도 됩니다. CSS 파일의 위치는 고정이고, 그 파일을 기준으로 풀리기 때문에 페이지 주소에 영향받지 않습니다.

2. 내부 링크에서 끝 슬래시를 뺀다#

리다이렉트를 없애면 문제의 원인 자체가 사라집니다.

HTML
<a href="/sample/app/detail/">상세</a>   <!-- 308 한 번 거침 -->
<a href="/sample/app/detail">상세</a>    <!-- 바로 도착 -->

페이지를 옮겨 다닐 때마다 리다이렉트가 한 번씩 끼는 것도 없어집니다. canonical과 구조화 데이터의 URL도 같이 맞춰야 검색엔진이 최종 주소를 정확히 인식합니다.

3. 로컬 미리보기 방식이 배포와 다르다는 것을 인지한다#

파일을 브라우저로 직접 여는 방식(file://)에서는 절대경로가 디스크 루트를 가리킵니다. /sample/app/css/site.css는 file:///sample/app/css/site.css가 되어 존재하지 않습니다.

로컬 확인은 정적 서버를 띄워서 해야 합니다.

Bash
python3 -m http.server 8899 --directory public
# → http://127.0.0.1:8899/sample/app/

여기서 주의할 점이 하나 더 있습니다. 파이썬 기본 서버는 Next.js와 반대로 동작합니다. 슬래시 없는 주소를 슬래시 붙은 쪽으로 301 리다이렉트합니다. 로컬에서 상대경로가 멀쩡히 동작하는 이유가 여기 있습니다. 로컬 통과는 배포 안전을 보장하지 않습니다.


다음부터 쓸 점검 절차#

배포 후 이 순서로 확인합니다. 앞의 두 개는 자동화할 수 있고, 세 번째는 사람이 봐야 합니다.

1. 리다이렉트가 붙는지 본다

Bash
curl -sI https://example.com/path/ | head -1
# 308이 나오면 상대경로를 쓴 문서는 전부 의심 대상

2. 문서 안의 경로를 뽑아 최종 주소 기준으로 다시 푼다

절대 URL을 그대로 찍지 말고, HTML에서 긁은 상대경로를 리다이렉트 이후 주소 기준으로 join한 뒤 요청합니다.

Python
import urllib.parse
base = 'https://example.com/sample/app'   # 슬래시 떨어진 최종 주소
print(urllib.parse.urljoin(base + '/', './css/site.css'))  # 잘못된 기준
print(urllib.parse.urljoin(base, './css/site.css'))        # 브라우저가 쓰는 기준

두 결과가 다르면 그 페이지는 깨집니다.

3. 브라우저로 실제 화면을 본다

헤드리스 브라우저로 스크린샷 한 장이면 충분합니다.

Bash
chrome --headless --screenshot=out.png --window-size=1440,900 \
  https://example.com/sample/app

스타일이 없는 화면은 사람 눈에 0.5초면 보입니다. 자동 점검이 초록불인데 화면이 깨져 있는 상황을, 이 한 장이 막습니다.


정리#

  • Next.js 기본값은 주소 끝 슬래시를 떼는 방향이고, 그 순간 상대경로의 기준 디렉터리가 한 단계 올라갑니다.
  • 프레임워크 라우팅 아래에 정적 파일을 둘 때는 에셋 경로를 절대경로로 고정하는 편이 안전합니다.
  • 내부 링크의 끝 슬래시를 없애면 리다이렉트 자체가 사라져 원인이 제거됩니다.
  • HTTP 200은 "파일이 있다"는 뜻일 뿐입니다. 브라우저가 그 파일에 도달하는지는 브라우저로만 확인됩니다.

배포 파이프라인에 스크린샷 한 장을 추가하는 비용은 몇 초입니다. 그 몇 초가, 링크를 보낸 뒤에 "화면이 이상한데요"를 듣는 상황을 막아줍니다.

Follow · Ask · Work

이 글이 도움이 됐다면

짧은 작업 기록과 데모는 인스타그램에 올립니다. 만들고 싶은 것이 있다면 프로젝트 의뢰나 협업 제안도 편하게 보내 주세요.

글쓴이

파란여우

직접 만든 AI 앱·제품의 개발·배포를 공개하는 실전형 테크 크리에이터

파란여우 소개 Instagram @bluefox._.hi

시리즈 · DevOps 노트

시리즈 전체 보기

Next.js 글 더 보기

이어서 보기

만들고 싶은 것이 있다면 프로젝트 의뢰로 알려 주세요.