PHpullh
심층 가이드/PHP/기초 문법

PHP · 심층 가이드

PHP 기초 문법 완전 정리

느슨한 비교와 점진적 타입 선언이 뒤섞인 PHP 문법을 strict_types, match, enum, readonly 같은 최신 도구 중심으로 정리해 예측 가능한 코드를 쓰도록 만듭니다.

주제 25개 · 예제 코드 포함 · 최종 수정 2026-09-03 · 작성 pullh 편집팀

PHP의 문법을 어렵게 만드는 건 문법 자체가 아니라 두 시대가 한 파일에 공존한다는 사실입니다. 값은 여전히 동적으로 타입이 바뀌지만, 파라미터와 반환값에는 타입을 선언할 수 있고 그 선언이 실제로 강제되는지는 파일 맨 위의 declare(strict_types=1) 한 줄에 달려 있습니다. 이 선언은 파일 단위로만 적용되며 호출하는 쪽 기준으로 동작합니다. 이 구조를 모르면 같은 함수가 어떤 파일에서는 문자열 "3"을 순순히 정수로 받고 다른 파일에서는 TypeError를 던지는 상황을 이해할 수 없습니다.

그래서 strict_types 선언을 변수와 기본 타입 바로 다음에 배치했습니다. 여기서 타입 경계를 정한 뒤 union 타입과 nullable 타입으로 표현 범위를 넓히고, 값이 없을 때의 처리를 null coalescing 사용과 nullsafe 연산자로 이어 가면 됩니다. 분기 쪽은 if elseif else 조건문에서 시작하되 반환값이 필요한 분기는 match 표현식으로 옮기고, 고정된 후보 집합이 보이면 enum 기본 사용으로 승격시키는 흐름이 자연스럽습니다.

비교 연산자에서 한 번은 반드시 데입니다. match는 항상 === 로 판정하지만 switch는 == 로 판정하므로, 같은 조건표를 옮겨 적었는데 결과가 달라질 수 있습니다. 또 ?? 는 isset() 기준이라 값이 null일 때만 폴백하고, ?: 는 빈 문자열이나 0 에도 폴백합니다. readonly 프로퍼티는 초기화 후 재할당이 막히는데, 배열을 담아 두면 원소 하나를 바꾸는 것조차 재할당으로 간주되어 거부됩니다.

01변수와 기본 타입

문자열, 정수, 불리언을 각각 변수에 담고 var_dump로 타입과 값을 함께 출력합니다. 선언에 타입을 적지 않아도 대입한 값에 따라 타입이 정해집니다. echo는 true를 1로, false를 빈 문자열로 찍어 둘을 구분하기 어려우니 확인용으로는 var_dump를 씁니다.

PHP code

<?php
$name = 'PHP';
$year = 2026;
$isStable = true;

var_dump($name, $year, $isStable);
알아두면 좋은 점

var_dump()는 문자열 길이까지 함께 보여 줘 눈에 보이지 않는 공백이나 개행이 섞였는지 바로 드러납니다. 구조만 사람이 읽기 좋게 보고 싶으면 print_r()을 씁니다.

자주 하는 실수

var_dump() 호출을 남긴 채 배포하면 응답 본문에 내부 값이 섞여 나갑니다. 웹 화면에서는 줄바꿈도 뭉개져 디버깅에도 쓸모가 없어집니다.

02strict_types 선언

첫 줄의 declare(strict_types=1)이 이 파일에서 하는 함수 호출의 인자 검사 방식을 엄격 모드로 바꿉니다. 선언이 없으면 add('1', '2')가 조용히 숫자로 변환돼 통과하지만, 켜 두면 TypeError가 납니다. 검사 기준은 함수가 정의된 파일이 아니라 호출하는 쪽 파일을 따릅니다.

PHP code

<?php
declare(strict_types=1);

function add(int $a, int $b): int {
    return $a + $b;
}

echo add(1, 2);
알아두면 좋은 점

declare(strict_types=1)은 파일의 첫 문장이어야 하며 앞에 다른 코드가 있으면 치명적 오류가 납니다. 내장 함수 호출에도 같은 검사 규칙이 적용됩니다.

자주 하는 실수

strict_types는 선언한 파일에만 적용되므로 프로젝트 전체가 자동으로 엄격해지지는 않습니다.

03const 와 define 사용

const는 컴파일 시점에 고정되는 상수를, define은 실행 중에 만드는 상수를 선언합니다. 조건에 따라 값이 갈리는 설정이면 함수나 if 안에서도 쓸 수 있는 define이, 늘 같은 값이면 네임스페이스를 따르는 const가 맞습니다. const는 블록 안에서는 선언할 수 없습니다.

PHP code

<?php
const APP_NAME = 'DevToolsVerse';
define('APP_STAGE', 'local');

echo APP_NAME . ' / ' . APP_STAGE;
알아두면 좋은 점

const는 배열도 담을 수 있고 클래스 안에서는 접근 제어자까지 붙일 수 있습니다. 반면 define()의 이름은 문자열이라 변수로 조립하는 동적 상수명이 가능합니다.

자주 하는 실수

네임스페이스 안에서 define()으로 만든 상수는 이름에 네임스페이스가 자동으로 붙지 않아 전역에 놓입니다. const와 섞어 쓰면 참조 지점에서 상수를 찾지 못합니다.

04숫자 구분자와 산술

밑줄로 자릿수를 끊어 적은 1_500_000은 파서가 밑줄을 지우고 읽어 값이 달라지지 않습니다. 세율을 곱해 더하는 계산의 결과는 0.1이 부동소수라 float가 됩니다. 금액을 다룰 때는 이 방식 대신 원 단위 정수로 계산하거나 BCMath를 써야 반올림 오차가 쌓이지 않습니다.

PHP code

<?php
$budget = 1_500_000;
$taxRate = 0.1;

echo $budget + ($budget * $taxRate);
알아두면 좋은 점

밑줄은 숫자 사이에만 올 수 있어 1__000이나 _1000처럼 적으면 파스 오류가 납니다. 16진수나 2진수 리터럴에도 같은 방식으로 쓸 수 있습니다.

자주 하는 실수

float가 섞인 결과를 (int)로 캐스팅하면 반올림이 아니라 절삭이라 금액이 1원 모자라게 나올 수 있습니다. 반올림이 필요하면 round()를 명시해야 합니다.

05문자열 보간과 연결

큰따옴표 안에서는 {$name}이 값으로 치환되고, 작은따옴표는 치환이 없어 점 연산자로 이어 붙여야 합니다. 배열 원소나 프로퍼티를 넣을 때 중괄호가 어디까지가 변수인지 경계를 정해 줍니다. 작은따옴표는 변수도 이스케이프도 해석하지 않으니 리터럴을 그대로 두고 싶을 때 고릅니다.

PHP code

<?php
$name = 'pullh';
echo "hello {$name}
";
echo 'hello ' . $name . "
";
알아두면 좋은 점

큰따옴표 안에 배열 원소를 넣을 때는 {$user['name']}처럼 중괄호로 감싸야 하고, 중괄호 없이 쓰면 따옴표를 뺀 키만 허용됩니다.

자주 하는 실수

작은따옴표 안에 {$name}을 적으면 치환되지 않고 글자 그대로 출력됩니다. 로그나 메일 본문에서 이 실수를 하면 값 대신 변수 이름이 사용자에게 나갑니다.

06mb_strlen 으로 길이 재기

한글과 영문이 섞인 문자열의 길이를 문자 단위로 셉니다. strlen은 바이트를 세어 UTF-8 한글 한 자를 3으로 계산하므로, 글자 수 제한이나 잘라내기에는 mb_ 계열이 필요합니다. mbstring 확장이 빠진 환경에서는 함수 자체가 없어 치명적 오류가 나니 배포 대상에서 확인해야 합니다.

PHP code

<?php
$text = '안녕하세요 PHP';
echo mb_strlen($text, 'UTF-8');
알아두면 좋은 점

PHP 8부터 인코딩 인자를 생략하면 mb_internal_encoding() 값이 쓰이며 기본이 UTF-8입니다. 이모지처럼 결합 문자로 이뤄진 글자는 여전히 여러 개로 셉니다.

자주 하는 실수

strlen은 바이트 길이를 반환하므로 UTF-8 한글 길이를 셀 때 그대로 쓰면 틀릴 수 있습니다.

07배열 구조 분해

대괄호 문법으로 배열의 0번과 1번 원소를 두 변수에 한 번에 꺼냅니다. 값 여러 개를 배열로 돌려주는 함수의 결과를 받을 때 인덱스를 반복해 적지 않아도 됩니다. 원소가 모자라면 오류가 아니라 경고와 함께 null이 들어가므로, 개수가 확실하지 않은 입력에는 따로 검사가 필요합니다.

PHP code

<?php
[$first, $second] = ['php', 'laravel'];
echo $first . ' / ' . $second;
알아두면 좋은 점

['id' => $id] = $row처럼 키를 지정한 구조 분해도 되고, 중첩 배열은 [[$a, $b], $c] 형태로 한 번에 풀 수 있습니다. 건너뛸 자리는 비워 두면 됩니다.

자주 하는 실수

연관 배열을 [$a, $b] 형태로 분해하면 문자열 키가 0, 1과 맞지 않아 둘 다 null이 됩니다. 경고만 나고 실행은 이어져 한참 뒤에 문제가 드러납니다.

08if elseif else 조건문

점수를 위에서부터 비교해 90 이상이면 A, 80 이상이면 B, 나머지는 C를 출력합니다. 조건은 순서대로 평가되고 하나가 맞으면 아래는 건너뛰므로, 범위가 넓은 조건을 먼저 두면 뒤쪽 가지에 영영 닿지 못합니다. 값 하나를 여러 갈래로 나누기만 한다면 match 쪽이 짧습니다.

PHP code

<?php
$score = 87;

if ($score >= 90) {
    echo 'A';
} elseif ($score >= 80) {
    echo 'B';
} else {
    echo 'C';
}
알아두면 좋은 점

elseif를 else if로 띄어 써도 중괄호 문법에서는 같지만, 콜론 문법에서는 else if가 파스 오류가 됩니다. 템플릿 파일에서 이 차이가 드러납니다.

자주 하는 실수

조건 순서를 뒤집어 80 이상을 먼저 두면 95점도 B가 되어 A 가지에 영영 닿지 않습니다. 문법 오류가 아니라 결과만 조용히 틀려 테스트 없이는 알아채기 어렵습니다.

09match 표현식

match는 대상 값을 ===로 비교해 맞는 가지의 표현식 결과를 그대로 돌려줍니다. switch와 달리 느슨한 비교도, 아래 가지로 흘러내리는 동작도 없어 break가 필요 없습니다. default가 없는데 맞는 가지도 없으면 UnhandledMatchError가 나서 빠뜨린 경우가 드러납니다.

PHP code

<?php
$code = 404;

$message = match ($code) {
    200 => 'ok',
    404 => 'not found',
    default => 'unknown',
};

echo $message;
알아두면 좋은 점

한 가지에 200, 201처럼 여러 값을 쉼표로 묶을 수 있고, 범위 비교가 필요하면 match (true)에 조건식을 나열하는 형태가 자주 쓰입니다.

자주 하는 실수

match는 느슨한 비교가 아니라 엄격한 비교를 사용하므로 case 매칭 규칙이 switch와 다릅니다.

10for 와 while 반복문

카운터를 한 줄에서 초기화·비교·증가시키는 for와, 조건만 두고 몸통에서 증가시키는 while이 같은 0~2 출력을 냅니다. 반복 횟수가 정해져 있으면 세 요소가 모여 있는 for가 실수를 줄입니다. while에서 증가문을 빠뜨리면 무한 루프가 되어 실행 시간 제한에 걸립니다.

PHP code

<?php
for ($i = 0; $i < 3; $i++) {
    echo "for: {$i}
";
}

$n = 0;
while ($n < 3) {
    echo "while: {$n}
";
    $n++;
}
알아두면 좋은 점

for의 세 부분은 각각 비워 둘 수 있어 for (;;)는 무한 루프가 됩니다. 조건에 count()를 직접 쓰면 매 반복마다 다시 계산되므로 큰 배열이면 밖으로 빼는 편이 낫습니다.

자주 하는 실수

while 조건을 <= 3으로 적으면 0부터 네 번 돌아 한 번 더 실행됩니다. 배열 인덱스에서 같은 실수를 하면 없는 키를 읽어 경고와 null이 따라옵니다.

11foreach 키와 값 순회

연관 배열을 돌며 키와 값을 함께 꺼내 출력합니다. PHP 배열은 순서를 기억하는 해시 맵이라 삽입한 차례대로 나오고, 이 점이 순서를 보장하지 않는 다른 언어의 해시와 다릅니다. 루프 안에서 원본을 고치려면 as $k => &$v처럼 참조를 쓰되, 끝난 뒤 unset하지 않으면 다음 루프가 마지막 원소를 덮어씁니다.

PHP code

<?php
$langs = ['backend' => 'php', 'ml' => 'python'];

foreach ($langs as $key => $value) {
    echo "{$key}: {$value}
";
}
알아두면 좋은 점

foreach는 배열의 사본을 순회하므로 루프 안에서 원본에 원소를 추가해도 이번 순회에는 반영되지 않습니다. 키는 정수와 문자열 중 하나로만 올 수 있습니다.

자주 하는 실수

as $k => &$v로 돌린 뒤 unset($v)를 빼고 다시 foreach를 돌리면 남은 참조 탓에 마지막 원소가 순회 값으로 계속 덮여 배열 끝이 망가집니다.

12null coalescing 사용

$_GET['name']이 아예 없거나 null일 때만 'guest'가 쓰입니다. ??는 왼쪽을 isset처럼 평가하므로 정의되지 않은 인덱스에도 경고가 나지 않습니다. 빈 문자열이나 0은 그대로 통과시키니 그것까지 걸러야 한다면 ?:나 명시적 검사를 덧붙여야 합니다.

PHP code

<?php
$name = $_GET['name'] ?? 'guest';
echo $name;
알아두면 좋은 점

??는 여러 개를 이어 붙여 $a ?? $b ?? '기본'처럼 순서대로 첫 유효값을 고를 수 있습니다. 왼쪽이 유효하면 오른쪽은 아예 평가되지 않습니다.

자주 하는 실수

$_GET 값을 ??로만 받으면 값은 채워지지만 검증은 하나도 되지 않습니다. htmlspecialchars() 없이 화면에 찍으면 그 자리가 곧 XSS 통로가 됩니다.

13nullsafe 연산자

?->는 왼쪽이 null이면 프로퍼티 접근을 건너뛰고 전체 결과를 null로 만들며, 여기서는 그 값을 ??가 받아 기본 문자열까지 정합니다. 객체가 없는 경우와 bio만 null인 경우가 똑같은 결과로 뭉개지므로, 둘을 구분해야 하는 로직에는 이 조합만으로 부족합니다.

PHP code

<?php
class Profile {
    public function __construct(public ?string $bio) {}
}

$profile = new Profile('php learner');
echo $profile?->bio ?? 'empty';
알아두면 좋은 점

?->는 사슬 중간에서 null을 만나면 뒤따르는 메서드 호출과 프로퍼티 접근을 통째로 건너뛰고 인자 평가도 하지 않습니다. 배열 첨자에는 쓸 수 없습니다.

자주 하는 실수

?->를 습관적으로 붙이면 null이면 안 되는 자리에서도 오류가 나지 않아 문제가 한참 뒤로 밀립니다. 값이 반드시 있어야 하는 곳은 ->로 두고 즉시 터지게 하는 편이 낫습니다.

14enum 기본 사용

문자열 값을 갖는 backed enum을 정의하고 케이스의 ->value를 꺼냅니다. 상태 값을 문자열 상수로 흩어 두는 대신 타입 힌트로 강제할 수 있습니다. 케이스는 인스턴스가 하나뿐이라 ===로 비교되며, DB에서 읽은 문자열은 from()이나 실패를 null로 돌려주는 tryFrom()으로 되돌립니다.

PHP code

<?php
enum Status: string {
    case Draft = 'draft';
    case Published = 'published';
}

echo Status::Draft->value;
알아두면 좋은 점

backed enum은 cases()로 케이스 목록을 얻을 수 있어 선택지 목록을 코드에서 바로 만들 수 있습니다. 인터페이스를 구현하거나 상수를 둘 수도 있습니다.

자주 하는 실수

DB에서 읽은 값에 from()을 쓰면 목록에 없는 문자열에서 ValueError가 던져집니다. 예전 데이터가 섞일 수 있는 컬럼이라면 tryFrom()으로 받아야 합니다.

15readonly 프로퍼티

생성자 프로퍼티 승격과 readonly를 함께 써서 한 번 정해진 id를 이후에 바꾸지 못하게 막습니다. 값 객체나 DTO에서 상태가 몰래 바뀌는 사고를 줄여 줍니다. 초기화는 선언된 클래스 범위 안에서만 가능하고, 값이 이미 들어간 프로퍼티에 다시 대입하면 Error가 던져집니다.

PHP code

<?php
class User {
    public function __construct(public readonly int $id) {}
}

$user = new User(7);
echo $user->id;
알아두면 좋은 점

readonly 프로퍼티에는 타입 선언이 반드시 있어야 하고 static과 함께 쓸 수 없습니다. 값이 객체면 참조만 고정될 뿐 그 객체 내부 상태는 계속 바뀔 수 있습니다.

자주 하는 실수

readonly 프로퍼티는 생성자 이후 다시 대입할 수 없습니다.

16Attribute 선언과 읽기

#[Attribute]로 표시한 클래스를 컨트롤러에 메타데이터로 붙이고, 리플렉션으로 읽어 인스턴스를 만든 뒤 path를 꺼냅니다. 라우팅 규칙이나 검증 규칙을 선언 위치 바로 옆에 두고 싶을 때 쓰는 방식입니다. newInstance()를 부르기 전까지는 인자만 보관될 뿐 대상 클래스가 실행되지 않습니다.

PHP code

<?php
#[Attribute]
class Route {
    public function __construct(public string $path) {}
}

#[Route('/users')]
class UserController {}

$ref = new ReflectionClass(UserController::class);
echo $ref->getAttributes(Route::class)[0]->newInstance()->path;
알아두면 좋은 점

#[Attribute(Attribute::TARGET_CLASS)]처럼 대상을 제한하면 잘못된 위치에 붙었을 때 리플렉션 단계에서 걸립니다. 반복 사용을 허용하려면 IS_REPEATABLE을 함께 줍니다.

자주 하는 실수

getAttributes()는 없으면 빈 배열을 돌려주므로 [0]을 바로 읽으면 정의되지 않은 오프셋 오류가 납니다. 선택 사항인 어트리뷰트는 개수를 먼저 확인해야 합니다.

17이름 있는 인자 호출

인자를 이름으로 지정해 role은 기본값에 맡기고 active만 바꿔 호출합니다. 기본값이 여럿인 함수에서 순서를 맞추려고 의미 없는 값을 채워 넣지 않아도 됩니다. 대신 호출부가 파라미터 이름에 묶이므로, 공개 라이브러리라면 이름을 바꾸는 순간 호환성이 깨집니다.

PHP code

<?php
function createUser(string $name, string $role = 'member', bool $active = true): array {
    return compact('name', 'role', 'active');
}

print_r(createUser(name: 'mida', active: false));
알아두면 좋은 점

이름 있는 인자는 위치 인자 뒤에 섞어 쓸 수 있지만 그 반대는 안 됩니다. 스프레드로 펼치는 배열에 문자열 키가 있으면 그대로 이름 있는 인자로 해석됩니다.

자주 하는 실수

존재하지 않는 파라미터 이름을 적으면 Error가 던져지는데, 오타는 실행이 그 줄에 닿아야 드러납니다. 내부 함수 중에는 이름이 문서와 다른 경우도 있습니다.

18union 타입과 nullable 타입

int|string|null을 받아 null이면 그대로 돌려주고, 아니면 문자열로 캐스팅해 앞뒤 공백을 잘라 냅니다. 반환 타입 ?string이 null이 나올 수 있다는 사실을 호출부에 알려 줍니다. 정수 10을 넘긴 결과가 문자열 "10"으로 찍히는 것이 이 변환을 그대로 보여 줍니다.

PHP code

<?php
function normalize(int|string|null $value): ?string {
    return $value === null ? null : trim((string) $value);
}

var_dump(normalize(10));
알아두면 좋은 점

union 타입에는 false 같은 특수 타입도 넣을 수 있지만 void나 mixed와는 조합할 수 없습니다. ?string은 string|null의 축약형과 같습니다.

자주 하는 실수

여기에 배열이나 객체를 넘기면 strict_types 여부와 상관없이 TypeError가 납니다. (string) 캐스팅이 알아서 막아 줄 것이라 믿고 검사를 건너뛰면 안 됩니다.

19DateTimeImmutable 사용

modify('+7 days')가 원본을 건드리지 않고 새 객체를 돌려주므로 $date는 처음 값 그대로 남습니다. DateTime이었다면 같은 호출이 자기 자신을 바꿔, 다른 곳에 넘겨 둔 참조까지 함께 변합니다. 시각 계산이 여러 계층에 흩어진 코드일수록 불변 쪽을 기본으로 두는 편이 안전합니다.

PHP code

<?php
$date = new DateTimeImmutable('2026-03-28 09:00:00');
$next = $date->modify('+7 days');

echo $date->format('Y-m-d') . ' -> ' . $next->format('Y-m-d');
알아두면 좋은 점

add()와 sub()도 모두 새 객체를 돌려주며, 두 시각의 차이는 diff()가 DateInterval로 알려 줍니다. 생성자 두 번째 인자로 DateTimeZone을 주면 기준 시간대를 못 박을 수 있습니다.

자주 하는 실수

$date->modify('+7 days');처럼 반환값을 버리면 아무 일도 일어나지 않습니다. DateTime을 쓰다 옮겨 오면 이 한 줄이 조용히 무시되어 날짜 계산이 어긋납니다.

20null coalescing assignment

??=는 키가 없거나 값이 null일 때만 오른쪽을 대입해서, 이미 지정된 TTL은 덮어쓰지 않습니다. 사용자 설정 위에 기본값을 채우는 병합 코드에서 자주 나옵니다. 오른쪽 표현식은 대입이 실제로 필요할 때만 평가되므로 비용이 큰 계산을 기본값으로 둬도 헛돌지 않습니다.

PHP code

<?php
$config = [];
$config['cache_ttl'] ??= 300;

echo $config['cache_ttl'];
알아두면 좋은 점

??=는 배열 원소와 객체 프로퍼티에도 쓸 수 있고 왼쪽 키가 없어도 경고 없이 새로 만듭니다. 기본값을 이렇게 나열하면 어떤 키를 채우는지 코드에 그대로 드러납니다.

자주 하는 실수

값이 null일 때만 대입하므로 이미 false나 0, 빈 문자열이 들어 있으면 그대로 둡니다. '비어 있으면 기본값'을 의도했다면 원하는 결과가 나오지 않습니다.

21참조 전달 기본

=&로 묶은 $alias는 복사본이 아니라 같은 저장소를 가리켜, 한쪽을 증가시키면 원래 변수도 2가 됩니다. 큰 배열을 함수 안에서 제자리 수정해야 할 때 쓰는 장치입니다. PHP는 배열 대입 시 실제로 쓰기 전까지 복사를 미루므로, 성능만 노린 참조는 이득 없이 추적만 어렵게 만듭니다.

PHP code

<?php
$count = 1;
$alias =& $count;
$alias++;

echo $count;
알아두면 좋은 점

함수 인자로 참조를 넘기려면 정의 쪽에 &가 있어야 하고, 묶인 참조를 끊으려면 unset()으로 이름만 떼면 됩니다. 원래 값은 다른 이름에 그대로 남습니다.

자주 하는 실수

참조는 편리하지만 데이터 흐름을 흐리기 쉬우므로 꼭 필요한 경우에만 사용하는 편이 좋습니다.

22filter_var 로 입력 검증

FILTER_VALIDATE_EMAIL은 형식이 맞으면 값을, 아니면 false를 돌려주기 때문에 !== false로 비교합니다. 정규식을 직접 짜는 것보다 경계 사례를 덜 놓칩니다. 형식만 볼 뿐 실제 수신 가능 여부는 알 수 없으므로, 가입 절차에는 확인 메일 발송이 따로 있어야 합니다.

PHP code

<?php
$email = 'team@pullh.com';
var_dump(filter_var($email, FILTER_VALIDATE_EMAIL) !== false);
알아두면 좋은 점

filter_var()는 FILTER_VALIDATE_INT나 URL 같은 검증 필터와 options의 min_range·max_range 같은 범위 지정까지 지원합니다. FILTER_NULL_ON_FAILURE를 주면 실패 시 null이 돌아옵니다.

자주 하는 실수

결과를 if (filter_var(...))처럼 받으면 정수 검증에서 유효한 0이 거짓으로 판정됩니다. 반환값 비교는 !== false로 하는 습관이 필요합니다.

23anonymous class 빠르게 만들기

이름 없는 클래스를 그 자리에서 정의하고 곧바로 인스턴스로 만들어 info()를 부릅니다. 구현체가 한 곳에서만 필요한 테스트 대역이나 임시 어댑터에 잘 맞습니다. 이 표현식이 평가될 때마다 별개의 클래스가 생기고 이름으로 타입 힌트를 걸 수도 없으니, 재사용할 것이라면 일반 클래스로 빼야 합니다.

PHP code

<?php
$logger = new class {
    public function info(string $message): void {
        echo "[info] {$message}
";
    }
};

$logger->info('boot');
알아두면 좋은 점

익명 클래스도 extends와 implements가 가능하고 new class($dep) {...} 형태로 생성자 인자를 넘길 수 있습니다. 바깥 변수는 use가 아니라 생성자로만 전달됩니다.

자주 하는 실수

루프 안에서 익명 클래스를 만들면 같은 정의라도 매번 새 클래스가 등록되어 메모리가 계속 늘어납니다. 반복 생성이 필요한 구현이라면 파일로 빼는 편이 낫습니다.

24Closure 에 값 캡처하기

use ($prefix)가 클로저를 만드는 시점의 값을 복사해 안으로 들여옵니다. 이후 바깥에서 $prefix를 바꿔도 클로저가 쓰는 값은 그대로입니다. 화살표 함수 fn은 바깥 변수를 자동으로 값 복사해 잡아 주지만 본문이 표현식 하나로 제한되므로, 여러 줄이 필요하면 이 형태를 씁니다.

PHP code

<?php
$prefix = 'php';
$formatter = function (string $value) use ($prefix): string {
    return $prefix . ':' . $value;
};

echo $formatter('8.3');
알아두면 좋은 점

use는 쉼표로 여러 변수를 잡을 수 있고, 클로저 안의 $this는 정의된 객체 컨텍스트를 그대로 물려받습니다. 필요하면 bindTo()로 다른 객체에 붙일 수도 있습니다.

자주 하는 실수

use ($prefix)로 잡은 변수를 클로저 안에서 바꿔도 바깥 변수는 변하지 않습니다. 값을 밖으로 내보내려면 use (&$prefix)로 참조 캡처를 해야 합니다.

25우주선 연산자

<=>는 왼쪽이 작으면 -1, 같으면 0, 크면 1을 돌려주며 여기서는 -1이 찍힙니다. usort 콜백이 요구하는 반환 규칙과 그대로 맞아떨어져 비교 함수를 한 줄로 줄여 줍니다. 뺄셈으로 대신하면 정수 범위를 넘길 때나 실수를 비교할 때 결과가 어긋날 수 있습니다.

PHP code

<?php
$a = 10;
$b = 20;

echo $a <=> $b;
알아두면 좋은 점

<=>는 문자열과 배열, 객체에도 쓸 수 있어 배열끼리는 길이를 먼저 보고 그다음 원소를 차례로 견줍니다. 여러 기준 정렬은 ?: 로 이어 붙이면 짧아집니다.

자주 하는 실수

usort는 콜백 반환값의 부호만 보므로 콜백이 float를 돌려주면 소수부가 잘려 서로 같은 값으로 판정됩니다. 뺄셈으로 비교하면 큰 정수에서 오버플로도 생깁니다.

정리하며

  • 새 파일은 declare(strict_types=1) 로 시작해 암묵적 형변환을 끊습니다.
  • 값을 돌려주는 분기는 switch 대신 match를 써서 엄격 비교와 누락 검출을 얻습니다.
  • ?? 는 null만, ?: 는 falsy 전체를 걸러 낸다는 차이를 구분합니다.
  • 고정된 상태 후보는 문자열 상수 대신 enum으로 선언해 오타를 컴파일 시점에 잡습니다.

더 깊이 들어가고 싶다면 PHP 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.