PHP / Session / 세션 값 출력하는 방법

PHP 세션에 저장한 값은 session_start()로 기존 세션을 연 뒤 $_SESSION 배열에서 읽습니다. 문자열을 HTML에 표시할 때는 htmlspecialchars()로 이스케이프하고, 아직 저장하지 않은 키에는 기본값을 적용합니다. 세션 ID나 전체 세션 내용을 공개 화면에 그대로 출력하지 말고 필요한 항목만 선택하세요.

session_start()와 $_SESSION의 관계

세션은 여러 HTTP 요청에 걸쳐 서버 쪽 상태를 이어 주는 기능입니다. 일반적인 쿠키 기반 구성에서는 브라우저가 세션 ID를 보내고 PHP가 그 ID에 대응하는 저장 데이터를 읽습니다. session_start()는 새 세션을 만들거나 기존 세션을 재개하며, 저장된 값을 $_SESSION에 채웁니다. 호출할 때마다 모든 값을 초기화하는 함수는 아닙니다.

쿠키와 응답 헤더를 보내야 하므로 HTML·echo·공백 출력 전에 호출해야 합니다. 파일은 UTF-8 BOM 없이 저장하고 <?php 앞에 빈 줄을 두지 마세요. 읽는 페이지에서도 같은 세션 설정으로 세션을 시작해야 합니다. 세션에 값을 저장하지 않은 새 브라우저에서는 해당 키가 없을 수 있습니다.

한 파일로 저장·출력·지우기를 실습하기

PHP 8.x와 세션 저장소에 쓸 수 있는 권한이 필요합니다. 별도 DB는 사용하지 않습니다. 새 실습 폴더에 아래 전체 코드를 session-demo.php로 저장합니다. 예제는 실습 이름 하나를 저장하는 기능이며 로그인·권한 부여 기능은 아닙니다.

<?php
declare(strict_types=1);
ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.use_trans_sid', '0');
session_name('CFSESSDEMO');
$https = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' && $_SERVER['HTTPS'] !== '';
session_set_cookie_params([
    'lifetime' => 0, 'path' => '/', 'secure' => $https,
    'httponly' => true, 'samesite' => 'Lax',
]);
if (!session_start()) {
    http_response_code(500);
    exit('세션을 시작하지 못했습니다.');
}
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store');
if (!isset($_SESSION['demo_csrf']) || !is_string($_SESSION['demo_csrf'])) {
    $_SESSION['demo_csrf'] = bin2hex(random_bytes(32));
}
$error = '';
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
    $token = $_POST['csrf'] ?? null;
    if (!is_string($token) || !hash_equals($_SESSION['demo_csrf'], $token)) {
        http_response_code(403);
        exit('입력 양식을 다시 열어 주세요.');
    }
    $action = $_POST['action'] ?? '';
    if ($action === 'clear') {
        unset($_SESSION['demo_name']);
    } elseif ($action === 'save') {
        $raw = $_POST['name'] ?? null;
        if (!is_string($raw) || trim($raw) === '' || strlen(trim($raw)) > 120) {
            http_response_code(422);
            $error = '이름은 UTF-8 기준 1~120바이트의 문자열로 입력하세요.';
        } else {
            $_SESSION['demo_name'] = trim($raw);
        }
    } else {
        http_response_code(400);
        exit('지원하지 않는 작업입니다.');
    }
    if ($error === '') {
        session_write_close();
        header('Location: session-demo.php', true, 303);
        exit;
    }
}
$name = $_SESSION['demo_name'] ?? null;
$name = is_string($name) ? $name : null;
$csrf = $_SESSION['demo_csrf'];
session_write_close();
function e(string $value): string {
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
?>
<!doctype html>
<html lang="ko">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>세션 값 확인</title></head>
<body>
<p>저장된 이름: <?= $name === null ? '없음' : e($name) ?></p>
<?php if ($error !== ''): ?><p><?= e($error) ?></p><?php endif; ?>
<form method="post" action="session-demo.php">
  <input type="hidden" name="csrf" value="<?= e($csrf) ?>">
  <label>실습용 이름 <input name="name" value="<?= e($name ?? '') ?>"></label>
  <button name="action" value="save">저장</button>
  <button name="action" value="clear">실습 값 지우기</button>
</form>
<p>실습 항목의 배열 표현</p>
<pre><?= e(var_export(['demo_name' => $name], true)) ?></pre>
</body>
</html>
php -S 127.0.0.1:8000

해당 폴더에서 명령을 실행하고 브라우저로 http://127.0.0.1:8000/session-demo.php를 엽니다. 파일을 직접 더블 클릭하는 file:// 방식은 PHP를 실행하지 않습니다. 포트가 사용 중이면 다른 번호로 바꿔 주소에도 반영하고, 실습이 끝나면 터미널에서 Ctrl+C로 종료합니다.

  1. 처음에는 “저장된 이름: 없음”과 배열의 NULL 값이 표시됩니다.
  2. 실습용 이름에 민수를 입력하고 저장합니다. 응답이 303으로 같은 페이지에 이동한 뒤 “저장된 이름: 민수”와 배열의 demo_name 값이 표시됩니다.
  3. 같은 브라우저에서 새로고침합니다. 입력을 다시 저장하지 않아도 민수가 유지되어야 합니다. 일반적으로 같은 브라우저의 다른 탭도 같은 세션 쿠키를 공유합니다.
  4. 다른 쿠키 저장소를 사용하는 시크릿 창에서 같은 주소를 열면 별도 세션이므로 저장된 이름은 없음으로 시작합니다.
  5. 실습 값 지우기를 누르면 demo_name만 없어지고 화면은 처음 상태로 돌아옵니다.

저장·지우기는 POST로 받고, CSRF 토큰으로 이 세션에서 연 양식인지 확인합니다. 성공 후 리디렉션하므로 결과 페이지를 새로고침할 때 저장 요청을 반복하지 않습니다. 세션 이름 CFSESSDEMO는 다른 실습과 구분하기 위한 것이며 기존 애플리케이션에서 임의로 이름을 바꾸면 기존 쿠키와 연결되지 않습니다.

개별 값과 배열을 출력하는 부분 이해하기

$_SESSION['demo_name'] ?? null은 키가 없거나 값이 null이면 null을 사용합니다. 예제에서는 값이 실제 문자열인지도 검사하고, 없으면 “없음”으로 표시합니다. 빈 문자열·숫자 0·문자열 "0"을 모두 없음으로 취급하는 empty()와는 목적이 다릅니다. null 자체와 키 부재를 구분해야 하는 데이터라면 array_key_exists() 등의 기준도 선택해야 합니다.

e()는 HTML 본문과 따옴표로 감싼 입력 속성에 사용할 값을 htmlspecialchars()로 바꿉니다. 이름에 <b>민수</b>를 넣어도 태그가 적용되지 않고 문자열로 보여야 합니다. 세션에 저장한 값이라는 이유만으로 HTML에 직접 출력해도 안전한 것은 아닙니다.

하단의 배열 표시는 $_SESSION 전체가 아니라 demo_name만 복사한 작은 배열입니다. var_export(..., true)가 만든 문자열을 다시 e()로 처리해 태그·따옴표가 HTML로 해석되지 않게 합니다. var_dump($_SESSION)나 session_id()를 공개 페이지에 출력하면 다른 중요한 값이나 세션 식별자가 노출될 수 있으므로 진단에 필요한 항목만 선택하세요.

페이지를 이동하거나 새로고침하면 값이 사라질 때

  • 두 요청의 주소가 같은 호스트인지 확인합니다. localhost와 127.0.0.1은 쿠키를 같은 호스트로 취급하지 않습니다. HTTP·HTTPS 전환과 쿠키 path·domain·이름 차이도 확인합니다.
  • 개발자 도구에서 CFSESSDEMO 쿠키가 생성되고 다음 요청에도 전송되는지 확인합니다. 값을 답변·화면 캡처에 공개할 필요는 없습니다. 쿠키 차단 설정이나 별도 브라우저 프로필이면 다른 세션이 됩니다.
  • session_start()가 성공하는지와 서버 오류 로그를 확인합니다. 파일 저장 방식이라면 session.save_path의 존재·권한·디스크 상태를 점검합니다. 저장소를 확인하지 않고 매번 기본값을 덮어쓰면 원인을 숨길 수 있습니다.
  • PHP 파일 앞의 BOM·공백, include한 파일의 출력 때문에 headers already sent가 발생하지 않는지 확인합니다. 출력 버퍼를 켜서 경고만 숨기기보다 최초 출력 위치를 바로잡습니다.
  • 여러 서버를 사용하는 환경에서는 요청마다 같은 세션 저장소에 접근하는지 확인합니다. 실습의 한 서버 파일 저장과 분산 운영 환경은 구성이 다를 수 있습니다.

예제의 session_set_cookie_params()는 세션 시작 전에 쿠키 속성을 정합니다. 로컬 HTTP에서는 Secure가 꺼지고, PHP가 HTTPS 요청으로 인식하면 켜집니다. 실제 서비스는 HTTPS를 사용하고 TLS 종료 프록시 환경에서는 신뢰할 수 있는 서버 설정으로 HTTPS 인식과 Secure 쿠키를 보장해야 합니다. 클라이언트가 보낸 임의 헤더를 그대로 믿어 판정하지 마세요. 관련 설정은 PHP 세션 보안 설정을 참고하세요.

값 삭제와 세션 쓰기 종료를 구분하기

unset($_SESSION['demo_name'])은 이름 항목만 제거합니다. 세션 전체를 없애는 로그아웃과 다르며, 이 실습에서는 CSRF 토큰 같은 다른 항목을 보존합니다. $_SESSION 변수 자체를 unset하지 마세요. 전체 로그아웃은 배열 정리, 세션 쿠키와 서버 저장 데이터 처리 등을 함께 고려해야 하며 session_destroy()만 호출하면 모든 쿠키와 현재 변수가 자동으로 사라지는 것은 아닙니다.

예제는 변경 후 session_write_close()로 데이터를 쓰고 세션 처리를 닫습니다. 화면에 사용할 이름과 CSRF 토큰도 닫기 전에 별도 변수로 읽어 둡니다. 쓰기를 닫은 뒤 $_SESSION 배열만 수정해도 그 변경이 자동 저장되지는 않습니다. 값 저장이 확인되었는지는 다음 요청에서 다시 읽어 비교해야 합니다.

파일 기반 세션은 같은 세션의 동시 요청을 잠그는 경우가 있으므로 세션 처리를 오래 잡고 있는 작업도 점검해야 합니다. 순수 조회만 할 때는 session_start()의 read_and_close 옵션을 검토할 수 있지만, 예제처럼 토큰이나 이름을 새로 저장하는 요청에는 그대로 적용하면 안 됩니다.