Skip to main content
Summary

웹은 입력을 받고 기억하며 동작한다

지금까지는 PHP 코드 안에 미리 준비한 값을 넣고 화면에 출력했다. 실제 웹 서비스는 여기서 한 단계 더 나아간다. 사용자가 검색어를 입력하고, 로그인 폼에 아이디와 비밀번호를 넣고, 장바구니에 상품을 담는다. 즉 웹 애플리케이션은 사용자 입력을 받고 처리하는 프로그램이다.

하지만 HTTP의 기본 성질은 단순하다. 브라우저가 요청을 보내면 서버가 응답한다. 응답이 끝나면 서버는 그 요청을 기본적으로 기억하지 않는다. 이런 성질을 무상태(stateless)라고 한다. 그래서 PHP 웹 애플리케이션은 두 가지를 함께 배워야 한다.

  • 사용자가 보낸 값을 안전하게 받는 방법

  • 다음 요청에서도 필요한 정보를 기억하는 방법

값을 받을 때는 $_GET, $_POST를 사용한다. 정보를 기억할 때는 쿠키와 세션을 사용한다.

폼, 요청, 쿠키, 세션 이해하기AI 생성 이미지

GET 요청: 주소에 드러나는 요청

GET은 주로 데이터를 조회할 때 사용한다. 검색, 페이지 번호, 게시글 번호처럼 서버의 데이터를 바꾸지 않고 읽기만 하는 요청에 잘 어울린다.

예를 들어 검색 폼은 보통 GET으로 만든다.

<?php
function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

$keyword = trim($_GET['keyword'] ?? '');
?>

<form method="get" action="search.php">
    <label>
        검색어
        <input type="text" name="keyword" value="<?= e($keyword) ?>">
    </label>
    <button type="submit">검색</button>
</form>

<?php if ($keyword !== ''): ?>
    <p>검색어: <?= e($keyword) ?></p>
<?php endif; ?>

이 폼에서 php를 검색하면 주소가 대략 다음처럼 바뀐다.

/search.php?keyword=php

? 뒤에 붙은 keyword=php가 서버로 전달되는 값이다. PHP에서는 $_GET['keyword']로 읽을 수 있다.

GET 요청의 장점은 주소만 복사해도 같은 결과를 다시 볼 수 있다는 점이다. 검색 결과 페이지나 게시글 상세 페이지에 적합하다. 반대로 비밀번호, 개인정보, 결제 정보처럼 주소에 드러나면 안 되는 값에는 사용하지 않는다. GET 값은 브라우저 주소창뿐 아니라 방문 기록, 서버 로그 등에 남을 수 있기 때문이다.

POST 요청: 본문에 담아 보내는 요청

POST는 주로 데이터를 생성하거나 변경할 때 사용한다. 회원가입, 로그인, 글쓰기, 댓글 작성처럼 사용자의 입력으로 서버 상태가 바뀌는 기능에 적합하다.

다음은 간단한 회원가입 폼이다.

<?php
function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

$name = '';
$email = '';
$errors = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $name = trim($_POST['name'] ?? '');
    $email = trim($_POST['email'] ?? '');

    if ($name === '') {
        $errors[] = '이름을 입력하세요.';
    }

    if ($email === '') {
        $errors[] = '이메일을 입력하세요.';
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = '올바른 이메일 형식이 아닙니다.';
    }
}
?>

<?php if ($errors): ?>
    <ul>
        <?php foreach ($errors as $error): ?>
            <li><?= e($error) ?></li>
        <?php endforeach; ?>
    </ul>
<?php endif; ?>

<form method="post" action="register.php">
    <label>
        이름
        <input type="text" name="name" value="<?= e($name) ?>">
    </label>

    <label>
        이메일
        <input type="text" name="email" value="<?= e($email) ?>">
    </label>

    <button type="submit">가입하기</button>
</form>

<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && !$errors): ?>
    <p><?= e($name) ?>님, 가입 정보를 확인했습니다.</p>
<?php endif; ?>

여기서 중요한 부분은 세 가지다.

첫째, 요청 방식은 $_SERVER['REQUEST_METHOD']로 확인한다. 페이지를 처음 열었을 때와 폼을 제출했을 때를 구분하기 위해서다.

둘째, 입력값은 $_POST['name'] ?? ''처럼 기본값을 함께 사용한다. 없는 키에 바로 접근하면 경고가 발생할 수 있다.

셋째, 화면에 출력할 때는 htmlspecialchars()를 사용한다. 사용자가 입력한 값은 믿지 않는 것이 기본이다.

한 가지 더 기억할 점이 있다. POST는 값이 URL에 보이지 않을 뿐, 그 자체로 데이터를 암호화해 주지는 않는다. 로그인이나 결제처럼 민감한 정보를 다루는 페이지는 반드시 HTTPS로 제공해야 한다.

$_GET, $_POST, $_REQUEST의 차이

PHP에는 요청 값을 담는 특별한 배열이 있다. 이런 배열을 슈퍼글로벌 변수라고 부른다. 함수 안팎 어디서든 접근할 수 있기 때문이다.

  • $_GET: URL 쿼리 문자열로 전달된 값

  • $_POST: POST 본문으로 전달된 값

  • $_COOKIE: 브라우저 쿠키로 전달된 값

  • $_REQUEST: GET, POST, COOKIE 값이 섞인 배열

초보자는 $_REQUEST가 편해 보일 수 있다. 하지만 실무에서는 가급적 피하는 것이 좋다. 값이 GET에서 온 것인지, POST에서 온 것인지, 쿠키에서 온 것인지 코드만 보고 알기 어렵기 때문이다. 설정에 따라 어떤 값이 우선되는지도 달라질 수 있어, 같은 이름의 값이 여러 곳에서 들어오면 의도와 다르게 동작할 여지도 있다.

예를 들어 글 삭제 기능은 최소한 POST로 처리해야 한다. 그런데 $_REQUEST['id']를 사용하면 URL에 ?id=10을 붙여도 값이 들어올 수 있다. 요청의 의도가 흐려지는 것이다. 실제 삭제 기능에서는 POST 사용뿐 아니라 로그인 여부, 권한 확인, CSRF 방어 같은 보안 처리도 함께 필요하다.

좋은 습관은 명확하다.

$id = $_GET['id'] ?? null;      // 조회용 ID
$title = $_POST['title'] ?? ''; // 폼 입력값

어디서 온 값인지 코드에 드러나야 한다.

입력값 검사는 서버에서 반드시 한다

HTML의 required, type="email" 같은 속성은 사용자를 돕는 장치다. 하지만 브라우저 개발자 도구나 직접 만든 요청으로 얼마든지 우회할 수 있다. 따라서 중요한 검사는 PHP에서 다시 해야 한다.

입력값 검사는 보통 다음 순서로 진행한다.

  1. 값이 존재하는지 확인한다.

  2. 앞뒤 공백을 제거한다.

  3. 필수값이 비어 있는지 확인한다.

  4. 길이, 형식, 허용 범위를 검사한다.

  5. 화면에 출력할 때 이스케이프한다.

예를 들어 닉네임을 검사해 보자.

$nickname = trim($_POST['nickname'] ?? '');
$errors = [];

if ($nickname === '') {
    $errors[] = '닉네임을 입력하세요.';
} elseif (mb_strlen($nickname) < 2) {
    $errors[] = '닉네임은 2글자 이상이어야 합니다.';
} elseif (mb_strlen($nickname) > 20) {
    $errors[] = '닉네임은 20글자를 넘을 수 없습니다.';
}

한글처럼 여러 바이트로 표현되는 문자는 strlen()보다 mb_strlen()을 사용하는 것이 안전하다. strlen()은 글자 수가 아니라 바이트 수를 세기 때문이다.

이메일은 PHP가 제공하는 필터를 사용할 수 있다.

$email = trim($_POST['email'] ?? '');

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    $errors[] = '이메일 형식이 올바르지 않습니다.';
}

검증은 입력값을 바꾸기 위한 과정이 아니라, 애플리케이션이 처리할 수 있는 값인지 확인하는 과정이다. 그리고 이스케이프는 검증과 별개의 작업이다. 값이 검증을 통과했더라도 화면에 출력할 때는 출력 위치에 맞게 안전하게 변환해야 한다. HTML 화면에 출력할 때는 이 장에서 본 것처럼 htmlspecialchars()를 사용한다.

쿠키: 브라우저에 작은 정보를 저장하기

쿠키는 서버가 브라우저에 저장해 두는 작은 정보다. 브라우저는 같은 사이트에 다시 요청을 보낼 때 쿠키를 함께 보낸다.

예를 들어 사용자가 선택한 화면 테마를 기억해 보자.

<?php
$allowedThemes = ['light', 'dark'];
$isHttps = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $theme = $_POST['theme'] ?? 'light';

    if (!in_array($theme, $allowedThemes, true)) {
        $theme = 'light';
    }

    setcookie('theme', $theme, [
        'expires' => time() + 60 * 60 * 24 * 30,
        'path' => '/',
        'secure' => $isHttps,
        'httponly' => true,
        'samesite' => 'Lax',
    ]);

    header('Location: theme.php');
    exit;
}

$currentTheme = $_COOKIE['theme'] ?? 'light';

if (!in_array($currentTheme, $allowedThemes, true)) {
    $currentTheme = 'light';
}
?>

<p>현재 테마: <?= htmlspecialchars($currentTheme, ENT_QUOTES, 'UTF-8') ?></p>

<form method="post" action="theme.php">
    <select name="theme">
        <option value="light" <?= $currentTheme === 'light' ? 'selected' : '' ?>>밝게</option>
        <option value="dark" <?= $currentTheme === 'dark' ? 'selected' : '' ?>>어둡게</option>
    </select>
    <button type="submit">저장</button>
</form>

setcookie()는 응답 헤더를 통해 쿠키를 설정한다. 그래서 HTML을 출력하기 전에 호출해야 한다. 이미 화면 출력이 시작된 뒤에는 헤더를 보낼 수 없어 오류가 난다.

위 예제에서 httponly는 자바스크립트에서 쿠키를 읽기 어렵게 하고, samesite는 다른 사이트에서 시작된 요청에 쿠키가 함께 전송되는 범위를 줄이는 데 도움을 준다. secure는 HTTPS 요청에서만 쿠키를 보내도록 하는 옵션이다. 실제 서비스에서는 가능한 한 HTTPS를 사용하고 secure를 켜 두는 것이 좋다.

쿠키에는 민감한 정보를 저장하지 않는다. 쿠키는 브라우저에 저장되므로 사용자가 직접 볼 수 있고 수정할 수도 있다. 사용자 ID, 권한, 비밀번호 같은 중요한 정보는 쿠키에 그대로 넣지 않는다.

쿠키는 이런 정보에 적합하다.

  • 테마 설정

  • 최근 본 상품 ID

  • 언어 선택

  • 간단한 방문 설정

세션: 서버에 상태를 저장하기

세션은 사용자의 상태를 서버 쪽에 저장하는 방법이다. 브라우저에는 세션 ID만 쿠키로 저장된다. 실제 로그인 정보 같은 데이터는 서버가 관리한다.

세션을 사용하려면 파일 맨 위에서 session_start()를 호출한다.

<?php
session_start();

$_SESSION['username'] = 'kim';

다음 요청에서도 같은 브라우저라면 값을 읽을 수 있다.

<?php
session_start();

$username = $_SESSION['username'] ?? null;

로그인 흐름을 아주 단순하게 만들면 다음과 같다.

<?php
session_start();

function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}

$errors = [];

// 예제용 사용자입니다. 실제 서비스에서는 데이터베이스에서 조회합니다.
$userId = 'admin';
$passwordHash = password_hash('secret123', PASSWORD_DEFAULT);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $id = trim($_POST['id'] ?? '');
    $password = $_POST['password'] ?? '';

    if ($id === $userId && password_verify($password, $passwordHash)) {
        session_regenerate_id(true);
        $_SESSION['user'] = [
            'id' => $id,
        ];

        header('Location: mypage.php');
        exit;
    }

    $errors[] = '아이디 또는 비밀번호가 올바르지 않습니다.';
}
?>

<?php foreach ($errors as $error): ?>
    <p><?= e($error) ?></p>
<?php endforeach; ?>

<form method="post" action="login.php">
    <label>
        아이디
        <input type="text" name="id">
    </label>

    <label>
        비밀번호
        <input type="password" name="password">
    </label>

    <button type="submit">로그인</button>
</form>

로그인에 성공하면 $_SESSION['user']에 사용자 정보를 저장한다. 이후 페이지에서는 이 값이 있는지 확인해 로그인 여부를 판단한다.

<?php
session_start();

if (!isset($_SESSION['user'])) {
    header('Location: login.php');
    exit;
}
?>

<p>로그인한 사용자만 볼 수 있는 페이지입니다.</p>

로그아웃은 세션 값을 비우고 세션을 종료한다.

<?php
session_start();

$_SESSION = [];

if (ini_get('session.use_cookies')) {
    $params = session_get_cookie_params();

    setcookie(session_name(), '', [
        'expires' => time() - 3600,
        'path' => $params['path'],
        'domain' => $params['domain'],
        'secure' => $params['secure'],
        'httponly' => $params['httponly'],
        'samesite' => $params['samesite'] ?? 'Lax',
    ]);
}

session_destroy();

header('Location: login.php');
exit;

로그인 직후 session_regenerate_id(true)를 호출하는 이유는 세션 ID를 새로 발급하기 위해서다. 기존 세션 ID가 악용되는 위험을 줄이는 기본적인 보안 습관이다.

실습 과제

  1. GET 방식으로 동작하는 검색 폼을 만들고, 검색어가 비어 있으면 "검색어를 입력하세요."라고 출력해 보세요.

  2. POST 방식 문의 폼을 만들고 이름, 이메일, 문의 내용을 검사해 보세요. 이메일은 filter_var()로 확인하세요.

  3. 쿠키를 사용해 사용자가 선택한 글자 크기 small, normal, large를 7일 동안 기억해 보세요.

  4. $_SESSION['user']를 사용해 로그인한 사용자만 볼 수 있는 mypage.php를 만들어 보세요.

  5. 로그아웃 파일을 만들고 세션이 사라진 뒤 마이페이지에 접근하면 로그인 페이지로 이동하게 해 보세요.

자주 발생하는 오류 점검

  • $_POST['name']에 바로 접근해 경고가 난다면 $_POST['name'] ?? ''처럼 기본값을 사용했는지 확인한다.

  • setcookie()나 session_start() 오류가 난다면 그 전에 HTML, 공백, 줄바꿈이 출력되지 않았는지 확인한다.

  • 비밀번호를 GET으로 보내고 있다면 즉시 POST로 바꾼다. GET 값은 주소에 남는다.

  • POST로 보낸 값도 자동으로 암호화되는 것은 아니므로 민감한 정보를 다룰 때는 HTTPS를 사용한다.

  • $_REQUEST를 사용하고 있다면 값의 출처가 명확한 $_GET 또는 $_POST로 바꾼다.

  • 쿠키에 비밀번호나 권한 값을 저장했다면 구조가 잘못된 것이다. 중요한 상태는 세션이나 데이터베이스에서 관리한다.

  • header('Location: ...') 뒤에 exit를 쓰지 않았다면 이후 코드가 계속 실행될 수 있다.

  • 사용자가 입력한 값을 화면에 그대로 출력하고 있다면 htmlspecialchars()로 이스케이프한다.

폼, 요청, 쿠키, 세션은 거의 모든 PHP 웹 애플리케이션의 바탕이다. 검색은 GET, 변경은 POST, 작은 설정은 쿠키, 로그인 상태는 세션으로 다룬다는 기준을 먼저 익히면 이후 데이터베이스와 보안 장을 훨씬 쉽게 따라갈 수 있다.

책 목록

PHP 교과서: 기초 문법부터 게시판 프로젝트와 배포까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기