짧은 URL로 바꾸자 CSS가 깨진 이유 | 상대경로와 base 태그 해결

/page.php?id=site&menu=1250 주소를 /site/1250처럼 짧은 URL로 바꿨습니다. 리라이트 규칙은 정상으로 보였고, HTML도 잘 출력됐습니다. 그런데 화면을 열자마자 CSS가 전부 빠진 것처럼 깨졌습니다.

처음에는 .htaccess 규칙을 잘못 쓴 줄 알았습니다. 하지만 네트워크 탭을 보니 원인은 리라이트가 아니라 상대경로였습니다. 브라우저가 CSS, 이미지, 자바스크립트 파일을 엉뚱한 위치에서 찾고 있었습니다.

이 글은 base 태그 사용법만 설명하는 글이 아닙니다. 쿼리스트링 URL을 짧은 URL로 바꾼 뒤 상대경로 기준이 달라지면서 CSS가 깨졌고, 전역 수정 대신 조건부 base 태그로 해결한 과정을 정리한 글입니다.

짧은 URL에서만 base 태그를 출력하는 PHP 코드

짧은 URL에서만 깨진 화면

기존 주소는 정상으로 보였습니다.

/page.php?id=site&menu=1250  → 정상 표시

그런데 새로 만든 짧은 URL에서는 HTML만 나오고 스타일이 전부 빠졌습니다.

/site/1250  → CSS, 이미지, JS 로드 실패

개발자 도구 네트워크 탭을 열어보니 이런 요청이 보였습니다.

GET /site/css/style.css       404
GET /site/img/logo.png        404
GET /site/js/common.js        404

원래 파일 위치는 아래였습니다.

/css/style.css
/img/logo.png
/js/common.js

그런데 브라우저는 /site/ 아래에서 파일을 찾고 있었습니다. PHP는 같은 파일을 실행하고 있었지만, 브라우저가 해석하는 상대경로 기준이 달라진 것입니다.

상대경로 기준이 바뀐 원인

헤더 템플릿에는 이런 코드가 있었습니다.

<link rel="stylesheet" href="css/style.css">
<script src="js/common.js"></script>
<img src="img/logo.png" alt="사이트 로고">

css/style.css처럼 /로 시작하지 않는 경로는 상대경로입니다. 브라우저는 현재 URL을 기준으로 이 경로를 해석합니다.

기존 쿼리스트링 주소에서는 기준이 사실상 루트에 가까웠습니다.

/page.php?id=site&menu=1250
→ css/style.css
→ /css/style.css

하지만 짧은 URL에서는 브라우저가 /site/ 아래에 있는 페이지처럼 인식했습니다.

/site/1250
→ css/style.css
→ /site/css/style.css

서버 내부에서는 같은 PHP 파일을 실행해도, 브라우저 입장에서는 URL 깊이가 달라진 것입니다. URL 리라이트 작업에서 자주 밟는 함정이 바로 이 부분이었습니다.

모든 경로를 절대경로로 바꾸는 방법

가장 정석적인 해결은 상대경로를 전부 절대경로로 바꾸는 것입니다.

<!-- 변경 전 -->
<link rel="stylesheet" href="css/style.css">
<script src="js/common.js"></script>
<img src="img/logo.png" alt="사이트 로고">

<!-- 변경 후 -->
<link rel="stylesheet" href="/css/style.css">
<script src="/js/common.js"></script>
<img src="/img/logo.png" alt="사이트 로고">

새로 만드는 사이트라면 이 방식이 가장 깔끔합니다. 템플릿 파일 몇 개만 고치면 이후에도 안전합니다.

하지만 오래된 사이트에서는 문제가 있었습니다. 공통 헤더뿐 아니라 게시글 본문, 에디터로 입력된 이미지, 예전 스킨 파일 안에도 상대경로가 섞여 있었습니다.

DB에 저장된 본문 HTML까지 전부 찾아 바꾸는 것은 위험했습니다. 그래서 전체 치환 대신 새 URL에서만 기준 경로를 잡아주는 방법을 선택했습니다.

base 태그로 경로 기준 고정

base 태그는 페이지 안의 상대경로가 어떤 주소를 기준으로 해석될지 정해줍니다.

<base href="https://example.com/">

이 태그가 있으면 아래 상대경로들은 루트 기준으로 해석됩니다.

<link rel="stylesheet" href="css/style.css">
<img src="img/logo.png" alt="사이트 로고">

결과는 이렇게 됩니다.

css/style.css → https://example.com/css/style.css
img/logo.png  → https://example.com/img/logo.png

한 줄로 해결되는 것처럼 보이지만, base 태그는 페이지 전체의 상대경로 의미를 바꾸기 때문에 전역으로 넣기에는 조심스러웠습니다.

짧은 URL에서만 조건부 출력

기존 긴 URL은 이미 정상으로 동작하고 있었습니다. 그래서 모든 페이지에 base를 넣지 않고, 짧은 URL 패턴에서만 출력했습니다.

<?php
$uri = $_SERVER['REQUEST_URI'] ?? '';
$isPrettyUrl = preg_match('#^/[^/]+/(board/)?([0-9]+|search)#', $uri);
$baseUrl = 'https://example.com/';

if ($isPrettyUrl) {
    echo '<base href="' . htmlspecialchars($baseUrl, ENT_QUOTES, 'UTF-8') . '">' . "\n";
}
?>

이 조건은 아래 형태의 URL에서만 base 태그를 출력합니다.

/site/1250
/site/board/856
/site/search?q=keyword

기존 주소에는 영향을 주지 않습니다.

/page.php?id=site&menu=1250
/list.php?id=site&menu=856

문제가 생기면 조건부 출력 코드만 제거하면 되므로 되돌리기도 쉬웠습니다.

base 태그 위치 확인

base 태그는 반드시 <head> 안에 있어야 하고, CSS나 JS 같은 상대경로 리소스보다 먼저 나와야 합니다.

<head>
  <base href="https://example.com/">
  <link rel="stylesheet" href="css/style.css">
  <script src="js/common.js"></script>
</head>

아래처럼 CSS 링크 뒤에 나오면 이미 지나간 리소스에는 적용되지 않습니다.

<head>
  <link rel="stylesheet" href="css/style.css">
  <base href="https://example.com/">
</head>

저는 처음에 base 태그를 헤더 템플릿 중간에 넣었다가 일부 CSS가 계속 깨지는 상황을 봤습니다. 위치를 <head> 상단으로 옮긴 뒤에야 네트워크 404가 사라졌습니다.

base 태그는 한 페이지에 하나만

base 태그는 한 페이지에 여러 개 넣는다고 순서대로 적용되지 않습니다. 보통 첫 번째 base만 의미가 있고, 뒤에 나온 base는 무시됩니다.

오래된 템플릿에서는 공통 헤더와 검색 템플릿에 각각 base를 넣어 두는 실수가 생길 수 있습니다.

확인은 브라우저 콘솔에서 했습니다.

document.querySelectorAll('base').length

기대값은 1이었습니다.

짧은 URL 페이지 → 1
기존 긴 URL 페이지 → 0 또는 의도한 값

여러 개가 나오면 공통 템플릿 한 곳으로 모으는 편이 안전했습니다.

해시 링크가 달라지는 문제

base 태그를 넣은 뒤 가장 의외였던 문제는 해시 링크였습니다.

<a href="#top">위로 이동</a>

base가 없을 때는 현재 페이지의 #top으로 이동합니다. 그런데 base가 있으면 브라우저가 이것을 base URL 기준으로 해석할 수 있습니다.

현재 페이지: https://example.com/site/1250
base: https://example.com/
href="#top" → https://example.com/#top

결과적으로 현재 글 안에서 위로 이동하는 것이 아니라 메인 주소로 튀는 것처럼 보일 수 있습니다.

해시 링크가 중요한 버튼이나 탭 메뉴라면 현재 경로를 포함해 출력하는 방식으로 바꿨습니다.

<?php
$currentPath = strtok($_SERVER['REQUEST_URI'], '?');
echo '<a href="' . htmlspecialchars($currentPath, ENT_QUOTES, 'UTF-8') . '#top">위로 이동</a>';
?>

base 태그는 CSS만 고치는 도구가 아니라 모든 상대 링크 기준을 바꾸는 태그라는 점을 여기서 다시 확인했습니다.

폼 action과 관리자 링크 확인

폼도 base의 영향을 받을 수 있습니다.

<form method="post" action="save.php">

base가 https://example.com/으로 잡혀 있으면 이 action은 아래처럼 해석됩니다.

https://example.com/save.php

의도한 경로가 /site/save.php였다면 문제가 됩니다. 반대로 루트의 save.php가 맞다면 괜찮습니다.

그래서 base를 넣은 뒤에는 아래 항목을 직접 눌러봤습니다.

  • 검색 폼 action이 의도한 경로로 가는지
  • 글쓰기 저장 폼이 정상 전송되는지
  • 관리자 수정 링크가 깨지지 않는지
  • 페이지 내부 해시 링크가 현재 페이지 안에서 움직이는지
  • 이미지 업로드 경로가 바뀌지 않는지

CSS가 살아났다고 끝내면 안 됐습니다. 상대경로를 쓰는 모든 링크가 영향을 받을 수 있기 때문입니다.

네트워크 탭으로 404 재확인

수정 후에는 개발자 도구 네트워크 탭에서 404 요청이 사라졌는지 확인했습니다.

GET /css/style.css       200
GET /img/logo.png        200
GET /js/common.js        200

명령줄에서도 간단히 확인했습니다.

curl -I https://example.com/css/style.css
curl -I https://example.com/img/logo.png
curl -I https://example.com/js/common.js

그리고 짧은 URL 페이지 소스에 base가 제대로 출력되는지도 확인했습니다.

curl -s https://example.com/site/1250 | grep -i "<base"

이렇게 확인하면 CSS 파일 자체가 없는 문제인지, 상대경로 기준이 틀어진 문제인지 구분하기 쉽습니다.

적용 후 달라진 점

조건부 base 태그를 넣은 뒤 짧은 URL 페이지에서도 리소스가 정상으로 로드됐습니다.

  • /site/1250에서 CSS, JS, 이미지 정상 로드
  • 네트워크 탭의 /site/css/... 404 요청 제거
  • 기존 긴 URL 페이지에는 영향 없음
  • DB에 저장된 오래된 본문 이미지 경로도 유지
  • 화면 깨짐과 레이아웃 흔들림 감소

특히 기존 본문 HTML을 대량 수정하지 않고 해결한 점이 컸습니다. 레거시 사이트에서는 정석적인 전체 치환보다 되돌릴 수 있는 작은 변경이 더 안전할 때가 있었습니다.

base 태그 점검 체크리스트

리라이트보다 상대경로 확인까지

짧은 URL 작업은 리라이트 규칙만 맞으면 끝나는 줄 알았습니다. 하지만 브라우저가 상대경로를 해석하는 기준이 바뀌면서 CSS와 이미지가 전부 깨질 수 있었습니다.

제가 얻은 기준은 단순합니다. 새 URL에서 HTML이 나온다고 끝이 아닙니다. 네트워크 탭에서 CSS, JS, 이미지가 어느 경로로 요청되는지 반드시 확인해야 합니다.

가능하면 /css/style.css처럼 절대경로로 정리하는 것이 가장 깔끔합니다. 하지만 오래된 본문 HTML과 여러 스킨 파일에 상대경로가 많이 섞여 있다면, 짧은 URL에서만 조건부 base 태그를 출력하는 방식도 현실적인 해결책이 될 수 있습니다.

다만 base 태그는 강한 도구입니다. 모든 상대경로의 기준을 바꾸기 때문에 해시 링크, 폼 action, 관리자 링크까지 함께 확인해야 진짜 수정이 끝납니다.

댓글 남기기