로그인 후 원래 보던 페이지로 돌려보내기, 페이지 이동 시 검색 조건 유지하기, canonical 주소 만들기. PHP 로 웹을 만들다 보면 "지금 이 페이지 주소가 뭐지?"를 코드에서 알아내야 할 일이 꽤 자주 생깁니다. PHP 에는 location.href 같은 값이 하나로 딱 있는 게 아니라서, $_SERVER 배열의 몇 가지 값을 조합해서 씁니다.

주소를 조각별로 나눠 보면

program1472.com/php/php.php?a=123&b=456 으로 접속했다고 가정하면 각 값은 이렇게 나옵니다.

광고
표현식 의미 결과
$_SERVER["HTTP_HOST"] 도메인 program1472.com
$_SERVER["REQUEST_URI"] 도메인 뒤 전체(경로 + 쿼리) /php/php.php?a=123&b=456
$_SERVER["QUERY_STRING"] ? 뒤에 붙은 GET 값 a=123&b=456
$_SERVER["PHP_SELF"] 도메인 뒤에서 쿼리를 뺀 경로 /php/php.php
basename($_SERVER["PHP_SELF"]) 파일 이름만 php.php

헷갈리는 건 REQUEST_URI 와 PHP_SELF 의 차이입니다. 앞의 것은 브라우저가 요청한 그대로라 쿼리 문자열까지 들어 있고, 뒤의 것은 실행 중인 스크립트 경로라서 쿼리가 빠집니다. 그래서 "검색 조건까지 포함해서 그대로 되돌아가기"는 REQUEST_URI, "같은 페이지로 폼을 보내되 파라미터는 새로" 같은 경우는 PHP_SELF 를 씁니다.

HTTP_HOST 는 기본 포트가 아니면 포트 번호까지 붙어서 나옵니다. 로컬에서 localhost:8080 으로 띄워 놓고 테스트하면 값이 localhost:8080 이 되니, 도메인만 비교하는 코드가 있다면 이 부분에서 어긋납니다. 그리고 명령줄(CLI)로 PHP를 실행하면 요청 자체가 없으니 HTTP_HOST, REQUEST_URI 같은 값은 아예 존재하지 않습니다. 크론으로 도는 스크립트에서 이런 값을 쓰면 경고가 나는 이유입니다.

URL 재작성을 쓰는 사이트라면

.htaccess 로 모든 요청을 index.php 하나로 보내는 구조(프런트 컨트롤러)에서는 두 값의 차이가 더 크게 벌어집니다. 사용자가 /board/notice/15 로 접속해도 실제로 실행되는 파일은 index.php 라서, PHP_SELF 는 /index.php 를 가리키고 REQUEST_URI 는 /board/notice/15 를 그대로 갖고 있습니다. 라우팅이나 "원래 보던 페이지로 돌아가기"에는 이런 구조에서 REQUEST_URI 만 의미가 있습니다. 실행 중인 파일 경로만 필요하다면 PHP_SELF 보다 SCRIPT_NAME 을 쓰는 편이 낫습니다. 뒤에 덧붙인 경로가 섞이지 않기 때문입니다.

직접 찍어 보는 테스트 페이지

서버마다 설정에 따라 값이 조금씩 다를 수 있어서, 저는 새 서버에 올리면 이런 페이지를 하나 만들어 실제 값을 먼저 확인합니다.

<!doctype html>
<html lang="ko">
  <head>
    <meta charset="utf-8">
    <title>PHP</title>
    <style>
      body {
        font-family: Consolas, monospace;
      }
    </style>
  </head>
  <body>
    <p><strong>../../../php/php.php?a=123&b=456</strong></p>
    <p>
      <strong>$_SERVER[ "HTTP_HOST" ] :</strong>
      <?php echo $_SERVER[ "HTTP_HOST" ]; ?>
    </p>
    <p>
      <strong>$_SERVER[ "REQUEST_URI" ] :</strong>
      <?php echo $_SERVER[ "REQUEST_URI" ]; ?>
    </p>
    <p>
      <strong>$_SERVER[ "QUERY_STRING" ] :</strong>
      <?php echo $_SERVER[ "QUERY_STRING" ]; ?>
    </p>
    <p>
      <strong>$_SERVER[ "PHP_SELF" ] :</strong>
      <?php echo $_SERVER[ "PHP_SELF" ]; ?>
    </p>
    <p>
      <strong>basename( $_SERVER[ "PHP_SELF" ] ) :</strong>
      <?php echo basename( $_SERVER[ "PHP_SELF" ] ); ?>
    </p>
  </body>
</html>

?a=123&b=456 을 붙여 접속한 결과 화면입니다.

$_SERVER 값별 출력 결과를 보여주는 테스트 페이지 화면

화면에 그대로 찍으면 안 되는 이유

위 예제는 값 확인용이라 그냥 echo 했지만, 실제 페이지에서는 이렇게 쓰면 안 됩니다. REQUEST_URI, QUERY_STRING, PHP_SELF 는 전부 사용자가 주소창에 넣은 문자열이 섞여 들어오는 값입니다. 특히 PHP_SELF 는 /php/php.php/"><script>... 처럼 파일명 뒤에 경로를 덧붙여 접속하면 그 부분까지 담기는 경우가 있어서, 폼 action 에 그대로 넣었다가 XSS 가 뚫리는 일이 흔합니다. 출력할 때는 항상 이스케이프를 거치세요.

<form action="<?php echo htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES, 'UTF-8'); ?>" method="post">

HTTP_HOST 도 요청 헤더에서 오는 값이라 클라이언트가 바꿔 보낼 수 있습니다. 비밀번호 재설정 메일의 링크처럼 중요한 절대 주소를 만들 때는 설정 파일에 도메인을 고정해 두고 쓰는 편이 안전합니다.

전체 주소가 필요하면 조합하면 됩니다. 프로토콜은 $_SERVER['HTTPS'] 가 비어 있지 않고 off 가 아니면 https 로 보면 되는데, 리버스 프록시 뒤에 있는 서버에서는 이 값이 비어 있어 항상 http 로 잡히기도 합니다. 그런 구성이라면 프록시가 넘겨주는 헤더를 확인하거나 역시 설정값으로 고정해 두는 게 덜 헷갈립니다.

$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$currentUrl = $scheme . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];

파라미터 하나만 바꾼 주소 만들기

페이지 번호 링크처럼 "지금 주소에서 page 만 바꾼 주소"가 필요할 때는 QUERY_STRING 을 문자열로 고치는 것보다 $_GET 을 복사해 값만 바꾸고 http_build_query 로 다시 만드는 게 깔끔합니다. 기존 검색 조건은 그대로 따라오고, 한글이나 특수문자도 알아서 인코딩됩니다.

$params = $_GET;
$params['page'] = 2;
$nextUrl = strtok($_SERVER['REQUEST_URI'], '?') . '?' . http_build_query($params);

strtok 은 ? 앞부분, 즉 경로만 떼어 내는 용도입니다. 이렇게 만든 주소도 화면에 넣을 때는 위에서 말한 대로 htmlspecialchars 를 거쳐야 합니다. 특히 & 가 들어간 주소를 HTML 속성에 그대로 넣으면 엄밀히는 잘못된 마크업이 되니, 이스케이프를 습관처럼 붙여 두세요.