PHP 함수·폼·내장 서버: 실행 결과로 배우는 기초
PHP 8.5 로 직접 확인. 타입·이름 있는 인자·유니언 타입 함수, 클로저, php -S 와 curl 로 GET/POST 폼, htmlspecialchars 없는 XSS, ?? 와 require.
시리즈: PHP 강좌 2/6 (초급). 1강을 마치고, 이제 사용자 입력을 받는 작은 페이지를 만들거나 고쳐야 하는 분을 위한 글입니다. 다 읽고 나면 타입이 있는 함수와 클로저를 쓰고, 명령 한 줄로 로컬 웹 서버를 띄우고, GET·POST 폼을 처리하고, XSS 구멍 없이 사용자 입력을 출력할 수 있습니다.
신입이 처음 받는 PHP 업무는 "게시판 검색창이 이상하니 좀 봐 달라" 같은 작은 수정인 경우가 많습니다. 이 일을 하려면 잘못된 입력을 거르는 함수, 페이지를 로컬에서 돌려 보는 방법, 출력하는 값을 빠짐없이 이스케이프하는 습관이 필요한데, 보통은 이 셋이 여러 글에 흩어져 있습니다. 이 글은 셋을 한 번에 다룹니다. 아래는 모두 Apple Silicon Mac(M1 Pro)에서 PHP 8.5.6 으로 실행한 결과입니다. HTTP 요청은 127.0.0.1 의 php -S 에 실제로 보낸 curl 8.7.1 호출이고, 파일 경로는 ~/demo/ 로 바꿨습니다.
타입 선언이 있는 함수
함수는 이름이 붙은 코드 묶음으로, 매개변수(parameter) 를 받아 값을 반환(return) 할 수 있습니다. 1강처럼 항상 declare(strict_types=1); 로 시작합니다.
<?php
declare(strict_types=1);
function greet(string $name, string $greeting = "Hello", bool $shout = false): string {
$msg = "$greeting, $name!";
return $shout ? strtoupper($msg) : $msg;
}
echo greet("Mina"), "\n";
echo greet("Mina", shout: true), "\n";
echo greet(greeting: "Hi", name: "Ken"), "\n";
Hello, Mina!
HELLO, MINA!
Hi, Ken!
string $name은 매개변수의 타입 선언, 괄호 뒤의: string은 반환 타입입니다. 아무것도 반환하지 않으면: void.$greeting = "Hello"는 기본값으로, 호출하는 쪽이 생략하면 쓰입니다.shout: true는 이름 있는 인자(named argument) 입니다(PHP 8.0 이상). 중간 인자를 건너뛸 수 있고,greet("Mina", "Hello", true)처럼 읽기 힘든 호출을 줄여 줍니다.
이름 있는 인자에는 고유한 에러가 있습니다. 같은 greet() 에 호출을 한 줄씩 써 봤을 때 8.5.6 이 낸 실제 메시지입니다.
greet(shout: true);
Fatal error: Uncaught ArgumentCountError: greet(): Argument #1 ($name) not passed in ~/demo/e.php:3
greet("A", nme: "B");
Fatal error: Uncaught Error: Unknown named parameter $nme in ~/demo/e.php:5
greet(name: "A", "Hi");
Fatal error: Cannot use positional argument after named argument in ~/demo/e.php on line 5
마지막 것은 컴파일 시점 에러라서 php -l file.php(lint: 실행하지 않고 문법만 검사)로 실행 전에 잡을 수 있습니다. 손댄 파일에는 매번 php -l 을 돌리는 습관을 들이세요. 또 하나, 호출하는 쪽이 이름 있는 인자를 쓰기 시작하면 매개변수 이름만 바꿔도 호출이 깨집니다. 이제는 매개변수 이름도 함수의 공개 약속이라는 뜻입니다.
널 허용 타입과 유니언 타입
<?php
declare(strict_types=1);
function findUser(int $id): ?array {
return $id === 1 ? ["id" => 1, "name" => "Mina"] : null;
}
var_dump(findUser(2));
echo findUser(2)["name"] ?? "guest", "\n";
function formatId(int|string $id): string {
return is_int($id) ? sprintf("#%05d", $id) : "#" . $id;
}
echo formatId(42), " ", formatId("A-7"), "\n";
echo formatId(1.5), "\n";
NULL
guest
#00042 #A-7
Fatal error: Uncaught TypeError: formatId(): Argument #1 ($id) must be of type string|int, float given, called in ~/demo/fn.php on line 14 and defined in ~/demo/fn.php:10
?array는 널 허용 타입(nullable type) 으로, 배열 또는null(PHP 7.1 이상)입니다. "못 찾을 수도 있다"를 솔직하게 표현하는 방법입니다. 호출하는 쪽은null을 처리해야 하고, 여기서는??를 썼습니다.int|string은 유니언 타입(union type) 으로(PHP 8.0 이상), 나열한 타입 중 하나입니다. 에러 메시지에는 PHP 가 정한 순서로string|int라고 찍히니, 메시지 문자열 그대로 코드를 검색하면 안 나옵니다.
| 표현하고 싶은 것 | 쓰는 법 |
|---|---|
| 정확히 한 타입 | int $x |
| "또는 null" | ?int $x (int|null 과 같음) |
| 여러 타입 중 하나 | int|string $x |
| 반환값 없음 | : void |
| 생략 가능한 인자 | int $x = 0 (기본값) |
클로저, use, 화살표 함수
클로저(익명 함수)는 이름 없는 함수로, 변수에 담거나 다른 함수에 넘길 수 있습니다. 자바스크립트나 파이썬을 하던 사람이 놀라는 점은, PHP 클로저는 use 로 가져오지 않으면 바깥 변수를 볼 수 없다는 것입니다.
<?php
$rate = 1.1;
$withTax = fn(int $p): float => $p * $rate; // arrow fn: captures by value
$counter = 0;
$inc = function () use (&$counter): void { $counter++; }; // by reference
$incCopy = function () use ($counter): int { return $counter + 100; }; // copied now
$inc(); $inc();
echo $withTax(1000), " ", $counter, " ", $incCopy(), "\n";
$rate = 2.0;
echo $withTax(1000), "\n";
echo implode(",", array_map(fn($x) => $x ** 2, [1, 2, 3])), "\n";
1100 2 100
1100
1,4,9
- 화살표 함수
fn(...) => 식(PHP 7.4 이상)은 바깥 변수를 자동으로 가져오지만 만든 시점의 값을 복사합니다. 나중에$rate를 바꿔도$withTax(1000)은 그대로였습니다. use ($counter)는 만들 때 값을 복사합니다.$incCopy()는 0 을 본 채로 100 을 돌려줬습니다.use (&$counter)는 변수를 공유하므로$inc()가 실제로 2 까지 올렸습니다.
흔한 실수: 합계가 늘지 않는 클로저
<?php
$total = 0;
$add = function (int $x) { $total += $x; };
$add(5);
echo $total, "\n";
$fnAdd = fn(int $x) => $total += $x;
$fnAdd(5);
echo $total, "\n";
Warning: Undefined variable $total in ~/demo/cl.php on line 3
0
0
실제로 일어나는 일: 첫 번째 클로저 안의 $total 은 새 지역 변수라서 Warning 이 납니다. 화살표 함수는 경고는 없지만 똑같이 틀렸습니다. 자기 복사본을 바꿨을 뿐입니다. 해결: function (int $x) use (&$total) { $total += $x; }. 더 좋은 방법은 값을 반환해서 바깥에서 대입하는 것입니다. 테스트하기도 쉽습니다.
명령 한 줄로 웹 페이지: php -S
PHP 에는 내장 개발 서버가 들어 있습니다. 페이지를 시험해 볼 때는 Apache 도, nginx 도, XAMPP 나 APM 도 필요 없습니다. 파일이 있는 폴더에서 실행합니다.
$ php -S 127.0.0.1:8099 -t .
[Thu Nov 12 15:18:52 2026] PHP 8.5.6 Development Server (http://127.0.0.1:8099) started
127.0.0.1 은 "이 컴퓨터에서만"이라는 뜻입니다. 회사 와이파이에서 0.0.0.0 을 쓰면 같은 네트워크의 누구나 파일을 열 수 있으니 피합니다. -t . 는 문서 루트 지정입니다. 요청마다 로그가 찍힙니다.
[Thu Nov 12 15:18:53 2026] 127.0.0.1:64641 Accepted
[Thu Nov 12 15:18:53 2026] 127.0.0.1:64641 [200]: GET /form.php?q=php+8.5
[Thu Nov 12 15:18:53 2026] 127.0.0.1:64641 Closing
매뉴얼에 적힌 대로 이 서버는 개발용이지 운영용이 아닙니다. 기본으로는 한 번에 요청 하나만 처리합니다.
GET·POST 폼 처리하기
$_GET 에는 쿼리 문자열(?q=...), $_POST 에는 전송된 폼 본문이 들어 있고, $_SERVER['REQUEST_METHOD'] 로 어느 쪽인지 알 수 있습니다. 이들을 슈퍼글로벌이라고 하며, 스크립트가 시작되기 전에 PHP 가 채워 두는 배열입니다. form.php 전체입니다.
<?php
declare(strict_types=1);
function h(string $s): string {
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
$method = $_SERVER['REQUEST_METHOD'];
$q = $_GET['q'] ?? '';
$name = trim($_POST['name'] ?? '');
$errors = [];
if ($method === 'POST' && $name === '') {
$errors[] = 'Name is required.';
}
?>
<!doctype html>
<meta charset="utf-8">
<title>Form demo</title>
<form method="get"><input name="q" value="<?= h($q) ?>"><button>Search</button></form>
<?php if ($q !== ''): ?>
<p>You searched for: <?= h($q) ?></p>
<?php endif; ?>
<form method="post"><input name="name"><button>Send</button></form>
<?php foreach ($errors as $e): ?>
<p class="error"><?= h($e) ?></p>
<?php endforeach; ?>
<?php if ($method === 'POST' && !$errors): ?>
<p>Thanks, <?= h($name) ?>!</p>
<?php endif; ?>
<?= ... ?> 는 <?php echo ... ?> 의 줄임이고, if (...): / endif; 는 템플릿에 어울리는 제어문 문법입니다. 브라우저 없이 curl 로 확인합니다.
$ curl -s -i "http://127.0.0.1:8099/form.php?q=php+8.5"
HTTP/1.1 200 OK
Host: 127.0.0.1:8099
Connection: close
X-Powered-By: PHP/8.5.6
Content-type: text/html; charset=UTF-8
<!doctype html>
...
<form method="get"><input name="q" value="php 8.5"><button>Search</button></form>
<p>You searched for: php 8.5</p>
$ curl -s --data-urlencode "name=민아 <b>" http://127.0.0.1:8099/form.php | tail -1
<p>Thanks, 민아 <b>!</p>
$ curl -s -d "name=" http://127.0.0.1:8099/form.php | grep error
<p class="error">Name is required.</p>
-d 와 --data-urlencode 는 폼 본문을 담아 POST 를 보냅니다. 한글은 그대로 통과했고 <b> 만 이스케이프됐습니다. X-Powered-By: PHP/8.5.6 헤더는 모든 방문자에게 정확한 버전을 알려 주므로 운영 서버에서는 php.ini 에 expose_php = Off 를 둡니다. 하나 더, 이름이 빈 POST 도 상태 코드는 200 이었습니다. 검증 실패를 클라이언트에 알리고 싶다면 http_response_code(422) 를 직접 보내야 합니다.
출력 이스케이프: htmlspecialchars 가 없으면 생기는 XSS
오래된 게시판·쇼핑몰 코드에서 흔히 보이는 형태입니다.
<?php
$q = $_GET['q'];
echo "<p>You searched for: $q</p>\n";
$ curl -s -G --data-urlencode 'q=<script>alert(1)</script>' http://127.0.0.1:8099/unsafe.php
<p>You searched for: <script>alert(1)</script></p>
입력이 바뀌지 않고 HTML 에 들어갔습니다. 브라우저에서는 이 스크립트가 우리 도메인에서, 사용자의 쿠키를 가진 채로 실행됩니다. 이것이 크로스 사이트 스크립팅(XSS) 입니다. h() 를 쓰는 form.php 에 같은 요청을 보내면 이렇게 됩니다.
<p>You searched for: <script>alert(1)</script></p>
잊기 쉬운 곳이 속성입니다. 따옴표를 닫고 이벤트 핸들러를 덧붙이는 값을 보내 봅니다.
$ curl -s -G --data-urlencode 'q=" autofocus onfocus="alert(1)' http://127.0.0.1:8099/form.php | grep input
<form method="get"><input name="q" value="" autofocus onfocus="alert(1)"><button>Search</button></form>
ENT_QUOTES 가 없으면(혹은 작은따옴표를 바꾸지 않던 PHP 7 기본값이면) 따옴표가 살아남습니다. PHP 8.1 부터 기본 플래그가 ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 로 바뀌었지만, 플래그와 'UTF-8' 을 명시해 두면 어느 버전에서나 안전하고 리뷰하는 사람에게도 의도가 분명해집니다.
원칙: 이스케이프는 입력할 때가 아니라 출력할 때, 직접 쓴 에러 메시지까지 포함해 모든 값에 합니다. htmlspecialchars 가 지켜 주는 것은 HTML 본문과 따옴표로 감싼 속성뿐이고, URL·자바스크립트·CSS 문맥에는 다른 방법이 필요합니다. 이 내용과 CSRF·세션은 5강에서 다룹니다: PHP 강좌 5/6: XSS·CSRF·세션. 오래된 PHP 코드에서 이런 버그를 실제로 고친 기록은 레거시 PHP SQLi/XSS 점검에 있습니다.
키가 없을 때: $_GET Warning, ?? 와 ?:
unsafe.php 를 q 없이 엽니다.
$ curl -s http://127.0.0.1:8099/unsafe.php
<br />
<b>Warning</b>: Undefined array key "q" in <b>~/demo/unsafe.php</b> on line <b>2</b><br />
<p>You searched for: </p>
문제가 두 가지입니다. 방문자에게 경고가 보이고, 실제 출력에는 이 컴퓨터의 파일 전체 경로가 들어 있었습니다(글에서는 줄였습니다). 서버 구조가 새어 나갑니다. 운영에서는 display_errors=Off, log_errors=On 으로 둡니다. 이번 실험에서도 그렇게 하자 경고는 서버 로그에만 남았습니다.
해결은 form.php 에서 쓴 ?? 입니다. 비슷하게 생긴 ?:(엘비스 연산자)와는 다르니 주의합니다.
<?php
$x = [];
echo $x['p'] ?: 'default', "\n";
echo $x['p'] ?? 'default', "\n";
$g = ['page' => '0'];
var_dump($g['page'] ?: 1, $g['page'] ?? 1);
Warning: Undefined array key "p" in ~/demo/ex.php on line 3
default
default
int(1)
string(1) "0"
| 연산자 | 키가 없음 | 값이 null |
값이 "0", "", 0 |
|---|---|---|---|
$a['k'] ?? 'd' |
'd', 경고 없음 |
'd' |
값을 그대로 |
$a['k'] ?: 'd' |
'd' + Warning |
'd' |
'd' (거짓으로 봄) |
isset($a['k']) |
false | false | true |
폼 입력에는 거의 항상 ?? 가 맞습니다. ?page=0 은 "없음"이 아니라 엄연한 값입니다.
include 와 require
include 와 require 는 둘 다 그 자리에 다른 PHP 파일을 끼워 넣습니다. 차이는 파일이 없을 때 드러납니다.
<?php
$which = $_GET['mode'] ?? 'include';
if ($which === 'include') {
include __DIR__ . '/missing.php';
} else {
require __DIR__ . '/missing.php';
}
echo "still running after $which\n";
$ curl -s "http://127.0.0.1:8099/inc.php?mode=include"
Warning: include(~/demo/missing.php): Failed to open stream: No such file or directory in ~/demo/inc.php on line 4
Warning: include(): Failed opening '~/demo/missing.php' for inclusion (include_path='.:…/share/php/pear') in ~/demo/inc.php on line 4
still running after include
$ curl -s -i "http://127.0.0.1:8099/inc.php?mode=require"
HTTP/1.1 200 OK
...
Warning: require(~/demo/missing.php): Failed to open stream: No such file or directory in ~/demo/inc.php on line 6
Fatal error: Uncaught Error: Failed opening required '~/demo/missing.php' (include_path='.:…/share/php/pear') in ~/demo/inc.php:6
Stack trace:
#0 {main}
(읽기 쉽게 HTML 태그는 뺐습니다.) include 는 경고 두 번 뒤 계속 실행했고, require 는 Error 를 던지고 멈췄습니다. 의외인 것은 상태 줄입니다. display_errors 가 켜져 있으면 치명적 에러인데도 200 OK 였습니다. 경고 문자열 때문에 응답 본문 전송이 이미 시작됐기 때문입니다. php -d display_errors=0 -S ... 로 다시 띄우자 같은 요청이 HTTP/1.0 500 Internal Server Error 와 빈 본문을 돌려줬습니다. 내 컴퓨터가 아닌 곳에서는 display_errors 를 꺼야 하는 이유가 하나 더 생긴 셈입니다.
| 상황 | 쓸 것 |
|---|---|
| 없으면 페이지가 동작하지 않는 설정·함수·클래스 | require_once |
| 없어도 괜찮은 템플릿 조각(배너, 사이드바) | include |
| 두 번 읽힐 수 있는 파일(함수 정의) | _once 버전("Cannot redeclare" 방지) |
| 사용자 입력으로 만든 경로 | 쓰지 않는다. 정해진 목록에서 고른다 |
경로는 항상 __DIR__(지금 파일이 있는 디렉터리)에서 시작해 만듭니다. 상대 경로 include 'lib.php' 는 작업 디렉터리와 include_path 에 따라 달라지고, 이 값은 CLI·내장 서버·Apache 마다 다릅니다.
요청 왕복 시간 재기
같은 컴퓨터의 내장 서버에 요청 한 번을 보내면 얼마나 걸릴까요? curl 확장을 쓰는 PHP 클라이언트로, 세지 않는 워밍업 20번을 한 뒤 URL 마다 500번을 차례로 쟀습니다(127.0.0.1:8099, PHP 8.5.6, Apple M1 Pro, OPcache 는 손대지 않음).
<?php
$N = 500;
$ch = curl_init();
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true]);
foreach (['/ping.txt', '/ping.php', '/form.php?q=hello'] as $path) {
curl_setopt($ch, CURLOPT_URL, "http://127.0.0.1:8099$path");
for ($i = 0; $i < 20; $i++) curl_exec($ch); // warm-up
$t = [];
for ($i = 0; $i < $N; $i++) {
$s = hrtime(true); curl_exec($ch); $t[] = (hrtime(true) - $s) / 1e3;
}
sort($t);
printf("%-18s median %4.0f µs p95 %4.0f µs\n", $path, $t[$N >> 1], $t[(int) ($N * .95)]);
}
| URL | 중앙값(1회차) | p95(1회차) | 중앙값(2회차) | p95(2회차) |
|---|---|---|---|---|
/ping.txt (정적 파일) |
142 µs | 267 µs | 139 µs | 157 µs |
/ping.php ("pong" 출력) |
134 µs | 180 µs | 148 µs | 172 µs |
/form.php?q=hello (243바이트) |
152 µs | 181 µs | 155 µs | 204 µs |
표의 값은 상태 코드가 200 인지도 확인하는 전체 스크립트의 결과입니다. 위의 줄인 버전을 다른 작업이 돌고 있던 때 한 번 더 실행하자 중앙값이 159·167·193 µs 였습니다. 경향은 같고 흔들림만 컸습니다.
초보자가 여기서 얻을 것:
- localhost 에서는 HTTP 요청 전체와 작은 PHP 스크립트 실행에 약 0.13~0.15 ms 가 들었습니다. 정적 파일과 PHP 스크립트의 차이는 오차 범위였습니다. p95(느린 쪽 5%)는 중앙값보다 크게 흔들려서, 한 번의 값이 아니라 중앙값으로 보고합니다.
- PHP 를 처음부터 띄우는 비용이 훨씬 큽니다.
php hello.php를 50번 연속 실행하면 한 번에 평균 약 56 ms(두 번 측정: 57.1 ms, 55.9 ms./bin/echo실행은 약 2 ms 라서 대부분이 PHP 자체의 시작 비용)였습니다. 서버는 프로세스가 살아 있으므로 요청마다 스크립트만 실행됩니다. - 실제 호스팅에서는 네트워크 지연(수십 ms)이 둘 다 압도합니다. PHP 코드를 최적화하기 전에 운영 환경에서 먼저 재세요. 6강에서 OPcache 와 프로파일링으로 측정합니다.
8.5 새 기능: 파이프 연산자
이제 작은 함수를 쓸 수 있으니 8.5 에 추가된 기능 하나를 보겠습니다. |> 는 왼쪽 값을 오른쪽 callable 의 유일한 인자로 넘깁니다.
<?php
$slug = " Hello PHP 8.5 World "
|> trim(...)
|> strtolower(...)
|> (fn($s) => preg_replace('/[^a-z0-9]+/', '-', $s));
var_dump($slug);
string(19) "hello-php-8-5-world"
trim(...) 은 함수를 클로저로 바꾸는 일급 callable 문법(8.1 이상)입니다. |> 는 새 문법이라 예전 버전은 이 파일을 해석하지 못합니다(오래된 PHP 로 실제 메시지를 확인하지는 않았습니다). 오래된 공유 호스팅에서도 돌아야 하는 코드에는 쓰지 마세요.
솔직한 한계·버전 메모
- 실행한 것: 모든 코드를 macOS(Apple M1 Pro)의 PHP 8.5.6 CLI 와 내장 서버(
cli-serverSAPI)로. HTTP 는 curl 8.7.1 로 127.0.0.1 에만 보냈습니다. 실행하지 않은 것: Apache/mod_php, PHP-FPM, 8.5 보다 오래된 PHP. Apache 에서는 치명적 에러 때의 상태 코드나 include_path 가 다를 수 있습니다. - 매뉴얼 기준이며 예전 빌드로 확인하지 않은 버전: 널 허용 타입 7.1, 화살표 함수 7.4, 이름 있는 인자·유니언 타입 8.0,
htmlspecialchars기본 플래그가ENT_QUOTES | ENT_SUBSTITUTE로 바뀐 것 8.1, 일급 callable 8.1, 파이프 연산자 8.5. 컴파일 시점 Fatal error 에Stack trace:가 붙는 것은 8.5 의 새 설정fatal_error_backtraces때문입니다(이번 빌드에서 값은 1). - 왕복 시간은 루프백·순차·클라이언트 하나 기준이라 동시 접속에 대해서는 알려 주는 것이 없습니다. 내장 서버는
PHP_CLI_SERVER_WORKERS를 설정하지 않으면 프로세스 하나이고, 그 설정은 시험하지 않았습니다. - 약 56 ms 의 CLI 시작 시간은 이 Homebrew 빌드(모듈 67개) 기준입니다.
php -n(php.ini 없이)도 같은 67개였고 속도도 같았습니다(57~58 ms). 작게 만든 빌드는 시험하지 않았습니다.
연습 문제
1. "흔한 실수"의 클로저를 고쳐서 $add(5); $add(7); echo $total; 이 12 를 출력하게 하세요.
정답
<?php
$total = 0;
$add = function (int $x) use (&$total): void { $total += $x; };
$add(5); $add(7);
echo $total, "\n"; // 12
실행하면 12 가 나왔습니다. & 가 없으면 use ($total) 은 0 을 복사할 뿐이고 바깥 $total 은 바뀌지 않습니다.
2. PHP 8.5 에서 htmlspecialchars('<a href="x">it\'s</a>') 는 무엇을 출력하나요? ENT_NOQUOTES 플래그를 주면 무엇이 달라지나요?
정답
기본: <a href="x">it's</a>. ENT_NOQUOTES: <a href="x">it's</a>. 따옴표가 둘 다 남으므로 속성 안에서는 안전하지 않습니다.
3. 게시판 목록 페이지에 $page = $_GET['page'] ?: 1; 이 있습니다. /list.php 와 /list.php?page=0 에서 각각 무슨 일이 생기고, 어떻게 써야 할까요?
정답
page 없음: Warning "Undefined array key" 가 나고 $page = 1. page=0: "0" 은 거짓으로 취급돼 조용히 $page = 1. $page = (int) ($_GET['page'] ?? 1); 로 쓴 뒤 범위를 검증합니다(예: max(1, $page)).
다음 강좌
- 이전: PHP 강좌 1/6: 자료형·== 와 ===·문자열·배열
- 다음: PHP 강좌 3/6: 클래스·인터페이스·enum
- 이어서: PDO·SQLite 와 SQL 인젝션, XSS·CSRF·세션, OPcache·JIT·프로파일링
- 파이썬의 같은 강좌: 함수·스코프·기본 인자
- 파일 구조를 정리하면서도 옛 PHP API 를 살려 두는 방법: 구버전 앱을 깨뜨리지 않고 레거시 PHP API 옮기기
자주 묻는 질문
XAMPP 나 APM 을 설치하지 않고 PHP 페이지를 로컬에서 돌릴 수 있나요?
네. .php 파일이 있는 폴더에서 php -S 127.0.0.1:8099 를 실행하고 브라우저나 curl 로 http://127.0.0.1:8099/form.php 를 엽니다. 요청마다 로그가 한 줄 찍힙니다. 개발 전용이며, 127.0.0.1 에서 단순한 요청은 이번 측정에서 중앙값 약 0.13~0.15 ms 였습니다.
파일이 없을 때 include 와 require 는 무엇이 다른가요?
include 는 Warning 두 개(Failed to open stream, Failed opening ... for inclusion)를 내고 계속 실행합니다. require 는 Warning 다음에 Fatal error(Uncaught Error: Failed opening required ...)를 내고 멈춥니다. display_errors 를 끄면 require 쪽은 내장 서버가 HTTP 500 을 돌려줬습니다.
내가 만든 폼에서 온 값인데도 htmlspecialchars 가 필요한가요?
필요합니다. 폼에는 누구나 아무 값이나 보낼 수 있습니다. $_GET['q'] 를 그대로 echo 하면 <script>alert(1)</script> 가 HTML 에 그대로 나왔고, htmlspecialchars($q, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') 를 거치면 <script>... 로 바뀌었으며 속성을 깨려는 값의 따옴표도 " 가 됐습니다.