PHpullh

PHP · 심층 가이드

PHP 함수 완전 정리

기본값과 가변 인자부터 화살표 함수, Generator, first class callable까지 PHP에서 함수를 값으로 다루는 방법을 정리하고 재사용 가능한 파이프라인을 만듭니다.

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

PHP에서 함수는 오랫동안 온전한 값이 아니었습니다. 콜백은 함수 이름 문자열이나 [$obj, 'method'] 형태의 배열로 넘겼고, 그 흔적이 지금도 표준 라이브러리 시그니처에 callable이라는 이름으로 남아 있습니다. 이후 Closure, 화살표 함수, 그리고 strlen(...) 같은 first class callable 문법이 차례로 들어오면서 함수를 변수에 담고 조립하는 일이 자연스러워졌습니다. 이 계보를 알면 왜 같은 목적에 네 가지 표기법이 공존하는지 납득이 됩니다.

앞부분의 기본값 파라미터와 가변 인자 받기는 호출 규약을 다루고, 이름 있는 인자로 함수 호출과 함께 보면 인자가 많은 함수를 어떻게 정리하는지 감이 옵니다. 중반의 화살표 함수 사용과 클로저 팩토리는 상태를 품은 함수를 만드는 두 방식이고, 여기서 만든 조각을 함수형 파이프라인 헬퍼와 부분 적용 함수 만들기가 이어 붙입니다. 메모리를 아껴야 하는 순회는 Generator 로 범위 만들기에서 시작해 yield from 위임으로 중첩 소스를 평탄하게 합치는 순서로 읽으면 좋습니다.

클로저의 캡처 방식은 두 문법이 정반대입니다. 화살표 함수 fn() 은 바깥 변수를 자동으로, 값으로 캡처하지만 본문이 표현식 하나로 제한되고, function() use ($x) 는 명시적으로 적어야 하며 use (&$x) 로 참조 캡처가 가능합니다. 반복문 안에서 클로저를 모을 때 이 차이가 결과를 갈라 놓습니다. Generator도 함정이 있는데, 한 번 소비하면 되감을 수 없어 두 번째 foreach에서 예외가 나고, return으로 돌려준 값은 getReturn()으로만 꺼낼 수 있습니다.

01반환 타입이 있는 함수

두 정수를 더해 정수를 돌려주는 함수로, 파라미터와 반환 모두 타입이 선언돼 있습니다. 타입을 적어 두면 잘못된 인자가 함수 안까지 들어오지 못하고 호출 지점에서 걸립니다. 다만 기본 모드에서는 "3" 같은 숫자 문자열이 조용히 정수로 변환되며, 이를 막고 싶으면 파일 맨 위에 declare(strict_types=1)을 둬야 합니다.

PHP code

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

echo add(3, 4);
알아두면 좋은 점

반환 타입에는 static이나 never 같은 특수 타입도 쓸 수 있고, never는 이 함수가 정상적으로 돌아오지 않는다는 사실을 정적 분석기에 알려 줍니다.

자주 하는 실수

기본 모드에서는 add('3abc', 4)가 경고와 함께 3으로 변환돼 통과합니다. 사용자 입력이 그대로 들어오는 함수라면 이 조용한 변환이 계산 결과를 망칩니다.

02기본값 파라미터

인자를 생략하면 'guest'가 대신 쓰이고, 넘기면 그 값이 문자열 보간으로 들어갑니다. 호출부 대부분이 같은 값을 넘기는 상황에서 시그니처를 늘리지 않고 확장할 수 있습니다. 기본값이 있는 파라미터는 목록 뒤쪽에 몰아 둬야 하며, 기본값 자리에는 함수 호출이 아니라 상수 표현식만 올 수 있습니다.

PHP code

<?php
function greet(string $name = 'guest'): string {
    return "hello {$name}";
}

echo greet();
알아두면 좋은 점

기본값에는 상수와 배열, enum 케이스처럼 컴파일 시점에 정해지는 표현식이 올 수 있습니다. 값이 계산돼야 한다면 기본값을 null로 두고 함수 안에서 채우는 방식이 흔합니다.

자주 하는 실수

기본값이 있는 파라미터를 앞에 두고 뒤에 필수 파라미터를 놓으면 앞 인자를 생략할 수 없어 기본값이 무의미해집니다. PHP 8부터는 이 배치에 폐기 경고가 붙습니다.

03가변 인자 받기

...을 붙인 파라미터가 넘어온 인자를 전부 배열로 모아 array_sum()에 넘깁니다. 인자 개수가 호출마다 달라지는 합계나 로깅 함수에 어울립니다. 타입을 함께 적으면 배열 전체가 아니라 각 인자마다 검사가 걸리고, 아무것도 넘기지 않으면 빈 배열이 되어 array_sum()이 예외 없이 0을 반환합니다.

PHP code

<?php
function total(int ...$numbers): int {
    return array_sum($numbers);
}

echo total(1, 2, 3, 4);
알아두면 좋은 점

가변 파라미터 앞에는 일반 파라미터를 둘 수 있고, 함수 안에서 받은 배열은 언제나 0부터 시작하는 리스트입니다. 이름 있는 인자로 넘긴 값은 문자열 키로 모입니다.

자주 하는 실수

이미 배열에 담긴 값을 그대로 넘기면 int 타입 검사에 걸려 TypeError가 납니다. total(...$numbers)처럼 호출 시점에 펼쳐야 합니다.

04참조 인자로 값 바꾸기

파라미터 앞의 & 덕분에 함수 안에서 올린 값이 호출한 쪽 변수에 그대로 반영됩니다. 큰 배열을 복사 없이 손보거나 상태를 밖으로 흘려보내야 할 때 쓰입니다. 호출 지점에는 아무 표시가 남지 않아 함수 정의를 봐야 부작용을 알 수 있고, 참조 자리에는 리터럴이나 계산식을 넘길 수 없다는 제약도 있습니다.

PHP code

<?php
function increment(int &$value): void {
    $value++;
}

$count = 0;
increment($count);
echo $count;
알아두면 좋은 점

참조 파라미터에 기본값을 주면 인자를 생략할 수 있지만 그때는 함수 안 변경이 밖으로 나가지 않습니다. 참조는 반환값 하나로 표현하기 어려운 경우에만 쓰는 편이 좋습니다.

자주 하는 실수

참조 전달은 데이터 흐름을 감추기 쉬우므로 반환값으로 바꿀 수 있는지 먼저 고민하는 편이 좋습니다.

05callable 전략 주입

포매팅 방식을 함수 안에 박아 넣지 않고 callable 파라미터로 받아 호출합니다. 여기서는 내장 함수 이름 문자열 하나가 그대로 전략 객체 노릇을 합니다. callable은 함수명 문자열, [객체, 메서드] 배열, Closure를 모두 받아 주지만 프로퍼티 타입으로는 선언할 수 없어 그 자리에는 Closure를 씁니다.

PHP code

<?php
function runFormatter(string $value, callable $formatter): string {
    return $formatter($value);
}

echo runFormatter('php', 'strtoupper');
알아두면 좋은 점

callable 자리에는 'Class::method' 형태의 정적 메서드 문자열과 __invoke를 가진 객체도 들어갑니다. 실제로 호출 가능한지 미리 보려면 is_callable()을 씁니다.

자주 하는 실수

문자열 함수명은 오타가 나도 호출 순간에야 Error가 납니다. 게다가 외부 입력을 그대로 callable로 넘기면 임의 함수 실행 통로가 되므로 허용 목록으로 막아야 합니다.

06재귀 팩토리얼

1 이하일 때 재귀를 멈추고, 그 외에는 자기 자신을 한 단계 작은 값으로 호출해 곱을 쌓아 올립니다. 트리 순회처럼 구조 자체가 재귀적일 때 읽기 쉬운 코드가 됩니다. PHP에는 꼬리 호출 최적화가 없어 호출 깊이만큼 스택이 쌓이고, 20을 넘는 입력은 정수 범위를 벗어나 결과가 부정확해집니다.

PHP code

<?php
function factorial(int $n): int {
    return $n <= 1 ? 1 : $n * factorial($n - 1);
}

echo factorial(5);
알아두면 좋은 점

큰 값을 다뤄야 하면 반환 타입을 float로 바꾸거나 BCMath 계열로 옮겨야 합니다. 재귀 깊이는 디버거 확장을 켠 환경에서 별도의 한도에 먼저 걸릴 수 있습니다.

자주 하는 실수

음수를 넘기면 $n <= 1 조건에 걸려 1이 돌아오므로 잘못된 입력이 오류 없이 통과합니다. 정의역을 지키려면 앞에서 검사해 예외를 던져야 합니다.

07배열을 합치는 reduce 함수

array_reduce()가 빈 문자열에서 시작해 첫 단어는 그대로 두고 이후에는 구분자를 앞에 붙이며 이어 붙입니다. 마지막에 구분자가 남는 흔한 실수를 조건 한 줄로 피합니다. 물론 이 특정 결과만 필요하다면 implode('-', $words)가 더 짧고 빠르므로, reduce는 단계마다 조건이나 가공이 들어갈 때 값을 합니다.

PHP code

<?php
function joinWords(array $words): string {
    return array_reduce($words, function (string $carry, string $word): string {
        return $carry === '' ? $word : $carry . '-' . $word;
    }, '');
}

echo joinWords(['php', 'api', 'server']);
알아두면 좋은 점

빈 배열을 넘기면 초기값인 빈 문자열이 그대로 돌아와 별도 분기가 필요 없습니다. 구분자를 인자로 받게 바꾸면 여러 곳에서 재사용할 수 있습니다.

자주 하는 실수

누산기 타입을 string으로 못박아 두었으므로 배열에 정수가 섞이면 엄격 모드에서 TypeError가 납니다. 입력 타입을 미리 정리해 두어야 합니다.

08클로저 팩토리

곱할 값을 받아 use로 붙잡은 클로저를 반환하고, 반환된 클로저는 나중에 호출될 때 그 값을 계속 씁니다. 설정을 한 번 읽어 고정한 계산기를 여러 곳에 뿌릴 때 쓰는 구조입니다. use는 클로저를 만드는 순간의 값을 복사하므로 이후 바깥 변수를 바꿔도 무시되고, 연동시키려면 use (&$factor)로 참조 캡처를 해야 합니다.

PHP code

<?php
function makeMultiplier(int $factor): Closure {
    return function (int $value) use ($factor): int {
        return $value * $factor;
    };
}

$double = makeMultiplier(2);
echo $double(10);
알아두면 좋은 점

반환된 클로저들은 서로 독립적인 $factor를 갖기 때문에 배수를 달리한 계산기를 여러 개 만들어 둘 수 있습니다. Closure 타입으로 선언하면 프로퍼티에도 담을 수 있습니다.

자주 하는 실수

참조 캡처로 바꾸면 나중에 원본이 바뀔 때 이미 나눠 준 클로저의 동작까지 함께 바뀝니다. 설정을 고정하려던 의도와 정반대 결과가 됩니다.

09화살표 함수 사용

fn 화살표 함수를 array_map()에 넘겨 각 원소를 두 배로 만듭니다. 한 줄짜리 변환에 function 키워드와 use 절을 붙이지 않아도 되어 콜백이 짧아집니다. 화살표 함수는 표현식 하나만 담을 수 있고, array_map()에 배열을 여러 개 넘기면 콜백이 각 배열의 같은 위치 값을 인자로 나란히 받는다는 점도 알아 둘 만합니다.

PHP code

<?php
$numbers = [1, 2, 3];
$doubled = array_map(fn (int $n): int => $n * 2, $numbers);

print_r($doubled);
알아두면 좋은 점

fn은 정의 시점에 실제로 쓰인 바깥 변수만 자동으로 잡아 오고 $this도 함께 물려받습니다. fn이 다시 fn을 돌려주는 중첩 형태도 그대로 동작합니다.

자주 하는 실수

여러 줄이 필요한데 억지로 삼항이나 쉼표를 겹치면 읽기 어려운 한 줄이 됩니다. 값 변경을 바깥에 반영해야 하는 경우에도 화살표 함수로는 불가능합니다.

10Generator 로 범위 만들기

반복문 안에서 yield로 값을 하나씩 내보내는 함수라, 호출해도 곧바로 실행되지 않고 Generator만 돌려줍니다. 범위가 아주 크거나 중간에 멈출 가능성이 있는 순회에 적합합니다. 반환값은 배열이 아니어서 count()나 array_map()에 바로 넣을 수 없고, 그러려면 iterator_to_array()로 펼쳐야 합니다.

PHP code

<?php
function rangeGen(int $start, int $end): Generator {
    for ($i = $start; $i <= $end; $i++) {
        yield $i;
    }
}

foreach (rangeGen(3, 5) as $n) {
    echo $n . PHP_EOL;
}
알아두면 좋은 점

제너레이터는 순회 도중 send()로 값을 받거나 throw()로 예외를 밀어 넣을 수도 있습니다. 인자를 iterable로 선언해 두면 배열과 제너레이터를 모두 받을 수 있습니다.

자주 하는 실수

$gen[0]처럼 인덱스로 접근하면 Generator는 배열이 아니라 오류가 납니다. 필요할 때마다 배열로 펼치면 지연 평가의 이점이 사라집니다.

11yield from 위임

yield from이 배열이나 다른 제너레이터의 값을 자기 값인 것처럼 넘겨 주어, 두 목록이 하나의 순회로 이어집니다. 페이지 단위로 가져온 결과를 호출자에게는 단일 스트림처럼 보이게 할 때 씁니다. 이때 원본의 키까지 그대로 전달되므로 배열 두 개를 이어 붙이면 키가 0, 1, 0으로 겹치고, iterator_to_array()에 두 번째 인자로 false를 주지 않으면 값이 덮입니다.

PHP code

<?php
function chunks(): Generator {
    yield from ['php', 'go'];
    yield from ['python'];
}

foreach (chunks() as $item) {
    echo $item . PHP_EOL;
}
알아두면 좋은 점

yield from은 위임한 제너레이터의 return 값을 표현식 결과로 돌려주므로 하위 작업의 요약값을 받아 이어 쓸 수 있습니다. 재귀적인 트리 순회를 짧게 만들 때 유용합니다.

자주 하는 실수

키가 겹친 상태로 배열로 펼치면 나중 값이 앞 값을 덮어 원소가 사라집니다. iterator_to_array()의 두 번째 인자를 false로 줘야 전부 남습니다.

12함수 내부 static 변수

static 선언 덕분에 $id는 함수가 끝나도 사라지지 않고 다음 호출에서 이어져, 두 번 부르면 1과 2가 나옵니다. 전역 변수를 만들지 않고 호출 횟수나 캐시를 함수 안에 가둘 때 씁니다. 초기화는 첫 호출에서 한 번만 일어나며, 값은 프로세스가 아니라 요청이 끝나면 사라지므로 요청 간 카운터로는 쓸 수 없습니다.

PHP code

<?php
function nextId(): int {
    static $id = 0;
    return ++$id;
}

echo nextId() . ' / ' . nextId();
알아두면 좋은 점

static 변수의 초기값에는 상수 표현식만 올 수 있고 초기화는 첫 호출 한 번뿐입니다. 값비싼 준비 작업을 여기 두면 같은 요청 안에서 반복되지 않습니다.

자주 하는 실수

요청마다 프로세스 상태가 초기화되므로 이 카운터를 전역 ID 발급기로 쓰면 사용자마다 1부터 다시 시작합니다. 고유 값이 필요하면 DB 시퀀스나 UUID를 써야 합니다.

13이름 있는 인자로 함수 호출

인자를 위치가 아니라 path:, host:처럼 이름으로 지정해 호출하고, $https는 기본값에 맡깁니다. 불린 인자가 여럿 섞인 시그니처에서 호출부만 보고도 의미를 읽을 수 있게 해 줍니다. 이름 있는 인자 뒤에는 위치 인자를 놓을 수 없고, 파라미터 이름 자체가 공개 계약이 되어 이름을 바꾸면 호출부가 깨집니다.

PHP code

<?php
function buildUrl(string $host, string $path, bool $https = true): string {
    $scheme = $https ? 'https' : 'http';
    return "{$scheme}://{$host}/{$path}";
}

echo buildUrl(path: 'docs', host: 'example.com');
알아두면 좋은 점

가변 인자 함수는 넘겨받은 이름 있는 인자를 문자열 키가 있는 배열로 모읍니다. 덕분에 래퍼 함수가 이름을 보존한 채 원래 함수로 그대로 전달할 수 있습니다.

자주 하는 실수

이름 있는 인자 뒤에 위치 인자를 놓으면 파스 오류가 나고, 이미 이름으로 지정한 파라미터를 위치로 또 채워도 Error가 납니다.

14nullable 반환 타입

찾으면 사용자 배열을, 끝까지 못 찾으면 null을 반환하도록 ?array로 선언했습니다. 조회 실패가 예외 상황이 아니라 정상 흐름일 때 이 시그니처가 호출자에게 분기를 강제합니다. 빈 배열을 돌려주는 설계와 달리 '없음'과 '비어 있음'을 구분할 수 있고, 반환 타입에 물음표가 있으니 곧바로 쓰기 전에 확인이 필요하다는 신호도 함께 전달됩니다.

PHP code

<?php
function findUser(array $users, int $id): ?array {
    foreach ($users as $user) {
        if ($user['id'] === $id) {
            return $user;
        }
    }
    return null;
}

var_dump(findUser([['id' => 1]], 2));
알아두면 좋은 점

?array 반환은 호출부에서 ?? []나 널 검사로 곧장 이어 쓸 수 있어 분기가 짧아집니다. 찾지 못한 것이 오류에 가깝다면 null 대신 예외가 더 명확합니다.

자주 하는 실수

결과를 검사 없이 $user['name']으로 읽으면 null에서 값을 꺼내려다 오류가 납니다. 개발 중에는 목록이 비어 있지 않아 드러나지 않다가 운영에서 터집니다.

15union 반환 타입

filter_var()로 정수 형식을 검증해 유효하면 정수를, 아니면 false를 돌려주며 반환 타입을 int|false로 적었습니다. 포트나 페이지 번호처럼 문자열로 들어온 값을 신뢰 가능한 정수로 바꿀 때 씁니다. 결과를 판별할 때는 반드시 ===를 써야 하는데, 정상적으로 파싱된 0이 느슨한 비교에서는 false와 같게 취급되기 때문입니다.

PHP code

<?php
function parsePort(string $value): int|false {
    $port = filter_var($value, FILTER_VALIDATE_INT);
    return $port === false ? false : (int) $port;
}

var_dump(parsePort('8080'));
알아두면 좋은 점

FILTER_VALIDATE_INT는 앞뒤 공백만 허용하고 소수점이나 문자가 섞이면 거부합니다. 범위를 제한하려면 options에 min_range와 max_range를 함께 넘깁니다.

자주 하는 실수

if (!parsePort($v)) 형태로 검사하면 유효한 0이 실패로 판정됩니다. 0이 의미 있는 값이라면 반드시 === false로 비교해야 합니다.

16Closure::fromCallable 사용

객체와 메서드 이름으로 이뤄진 콜러블 배열을 Closure::fromCallable()이 진짜 Closure로 바꿔, 변수처럼 넘기고 호출할 수 있게 만듭니다. 배열 형태 콜러블을 받아 주지 않는 API에 메서드를 끼워 넣을 때 필요합니다. PHP 8.1부터는 $obj->slug(...) 문법이 같은 일을 더 짧게 하며, 비공개 메서드는 해당 클래스 스코프 밖에서 이렇게 감쌀 수 없습니다.

PHP code

<?php
class Slugger {
    public function slug(string $text): string {
        return strtolower(str_replace(' ', '-', trim($text)));
    }
}

$closure = Closure::fromCallable([new Slugger(), 'slug']);
echo $closure('Hello PHP');
알아두면 좋은 점

이렇게 만든 클로저는 원래 객체에 바인딩된 상태라 그 객체가 살아 있게 붙듭니다. 정적 메서드라면 클래스명과 메서드명을 이은 문자열도 그대로 넘길 수 있습니다.

자주 하는 실수

존재하지 않는 메서드 이름을 넘기면 그 자리에서 TypeError가 나므로 메서드명을 설정 문자열로 받는다면 미리 검증해야 합니다. 비공개 메서드는 클래스 밖에서 변환할 수 없습니다.

17first class callable 문법

함수 이름 뒤에 (...)를 붙이면 호출하지 않고 그 함수를 가리키는 Closure가 만들어져 변수에 담깁니다. 콜백을 넘겨야 하는 자리에서 문자열 함수명을 쓰던 자리를 대체합니다. PHP 8.1에 들어온 문법으로, 존재 여부를 만드는 시점에 확인하므로 오타가 실행 순간이 아니라 그 줄에서 드러나고 IDE와 정적 분석기도 대상을 추적할 수 있습니다.

PHP code

<?php
function normalize(string $value): string {
    return trim(strtolower($value));
}

$callable = normalize(...);
echo $callable('  PHP  ');
알아두면 좋은 점

$obj->method(...)와 정적 메서드 형태도 같은 문법으로 만들 수 있고, 만들어진 Closure는 인자 개수와 타입을 원본 그대로 유지합니다.

자주 하는 실수

이 문법은 인자를 미리 고정하는 수단이 아니라서 일부 인자만 넣어 부분 적용을 기대하면 파스 오류가 납니다. 인자를 묶으려면 클로저를 직접 만들어야 합니다.

18function_exists 폴리필

같은 이름의 함수가 이미 있는지 확인한 뒤에만 정의해, 나중에 언어나 라이브러리가 같은 이름을 제공해도 충돌하지 않게 합니다. 구버전 PHP를 지원해야 하는 패키지에서 흔히 보이는 폴리필 구조입니다. 조건문 안에 놓인 함수 정의는 파일이 읽힐 때가 아니라 그 if가 실행될 때 등록되므로, 이 블록보다 앞선 줄에서는 호출할 수 없습니다.

PHP code

<?php
if (!function_exists('str_contains_all')) {
    function str_contains_all(string $text, array $needles): bool {
        foreach ($needles as $needle) {
            if (!str_contains($text, $needle)) {
                return false;
            }
        }
        return true;
    }
}

var_dump(str_contains_all('php api server', ['php', 'api']));
알아두면 좋은 점

조건이 참일 때만 등록되므로 여러 파일에서 같은 폴리필을 읽어도 충돌하지 않습니다. Composer의 files 오토로드에 넣으면 진입점마다 require하지 않아도 됩니다.

자주 하는 실수

언젠가 같은 이름의 내장 함수가 추가되면 조건이 거짓이 되어 시그니처가 다른 쪽이 대신 쓰입니다. 폴리필 이름에는 고유한 접두사를 붙이는 편이 안전합니다.

19스프레드로 인자 풀기

인자를 배열로 준비해 두고 호출 시점에 ...로 펼쳐 각 파라미터에 순서대로 꽂습니다. 설정 배열이나 다른 함수에서 받은 인자 묶음을 그대로 넘길 때 반복적인 인덱스 접근을 없앱니다. 리스트 배열은 위치대로 매핑되지만 문자열 키가 있는 배열을 펼치면 이름 있는 인자로 해석되고, 개수가 모자라면 ArgumentCountError가 납니다.

PHP code

<?php
function connect(string $host, int $port, string $db): string {
    return "{$host}:{$port}/{$db}";
}

$params = ['localhost', 3306, 'app'];
echo connect(...$params);
알아두면 좋은 점

스프레드는 Traversable에도 동작해 제너레이터가 내보내는 값을 그대로 인자로 펼칠 수 있습니다. 정의 쪽의 점 세 개가 모으는 역할이라면 호출 쪽은 흩는 역할입니다.

자주 하는 실수

설정 배열의 순서가 바뀌면 host와 db가 뒤바뀐 채 조용히 연결 문자열이 만들어집니다. 타입이 같은 파라미터가 이어질 때 특히 위험합니다.

20정렬용 comparator 콜백

usort()에 넘긴 비교 함수가 우주선 연산자로 두 점수를 견주는데, $b를 앞에 둬 내림차순이 됩니다. 정렬 기준이 여러 필드거나 조건부일 때 이 형태로 규칙을 직접 씁니다. usort()는 원본 배열을 참조로 받아 제자리에서 바꾸고 기존 키를 버려 0부터 다시 붙이며, PHP 8부터는 같은 값끼리 원래 순서가 유지됩니다.

PHP code

<?php
$users = [
    ['name' => 'Lee', 'score' => 90],
    ['name' => 'Kim', 'score' => 98],
];

usort($users, function (array $a, array $b): int {
    return $b['score'] <=> $a['score'];
});

print_r($users);
알아두면 좋은 점

정렬 기준을 여러 개 두려면 비교 결과가 0일 때 다음 기준을 보도록 ?: 로 이어 붙입니다. 콜백을 이름 붙인 함수로 빼면 순서 자체를 테스트로 검증할 수 있습니다.

자주 하는 실수

콜백 안에서 배열 키를 기준으로 비교하려다 원본 순서를 참조하면 정렬 도중 값이 바뀌어 결과를 예측할 수 없습니다. 비교는 넘어온 두 인자만으로 끝내야 합니다.

21함수형 파이프라인 헬퍼

첫 인자로 받은 값을 가변 인자로 받은 단계들에 차례로 통과시키며 결과를 갈아 끼웁니다. 중첩 호출로 안쪽부터 읽어야 하는 코드를 위에서 아래로 읽히게 바꿔 줍니다. 내장 함수 이름과 화살표 함수를 섞어 넘길 수 있는 대신 값 타입이 mixed라, 단계 간 타입이 어긋나도 실행해 봐야 드러난다는 대가가 따릅니다.

PHP code

<?php
function pipe(mixed $value, callable ...$steps): mixed {
    foreach ($steps as $step) {
        $value = $step($value);
    }
    return $value;
}

$result = pipe('  PHP  ', 'trim', 'strtolower', fn (string $v) => $v . '-api');
echo $result;
알아두면 좋은 점

단계가 늘어도 호출부는 한 줄로 유지되고 각 단계는 단독으로 테스트할 수 있습니다. PHPDoc으로 입출력 타입을 적어 두면 정적 분석기가 흐름을 어느 정도 따라옵니다.

자주 하는 실수

단계 중 하나가 null을 돌려주면 그대로 다음 단계로 흘러가 엉뚱한 지점에서 오류가 납니다. 어느 단계가 원인인지 스택 트레이스가 알려 주지 않습니다.

22메모이제이션 함수

static $cache에 기저값 두 개를 미리 넣어 두고, 없는 항목만 계산해 저장하므로 같은 n을 두 번 계산하지 않습니다. 재귀 호출이 겹쳐 폭발하는 문제를 배열 한 줄로 잡는 고전적인 방법입니다. 캐시는 호출자별이 아니라 함수 하나에 묶여 모든 재귀가 공유하며, 밖에서 비울 수단이 없어 테스트마다 초기 상태를 만들기 어렵습니다.

PHP code

<?php
function fib(int $n): int {
    static $cache = [0 => 0, 1 => 1];
    if (!array_key_exists($n, $cache)) {
        $cache[$n] = fib($n - 1) + fib($n - 2);
    }
    return $cache[$n];
}

echo fib(10);
알아두면 좋은 점

static 캐시는 함수 하나에 묶인 상태라 재귀 호출끼리 자연스럽게 공유됩니다. 캐시를 밖에서 다루고 싶다면 클래스 프로퍼티나 주입받은 캐시 객체로 옮기는 편이 낫습니다.

자주 하는 실수

아주 큰 n을 넘기면 재귀 깊이가 커져 메모리 한도에 걸리고, 캐시 배열도 계속 늘어납니다. 오래 사는 CLI 프로세스에서는 이 증가가 누수처럼 보입니다.

23부분 적용 함수 만들기

앞쪽 인자를 미리 고정해 두고 나머지만 받아 원래 함수를 호출하는 클로저를 만들어 냅니다. 콜백 자리에 인자 하나짜리 함수만 넣을 수 있을 때 설정값을 미리 묶어 두는 용도로 씁니다. array_merge()로 이어 붙이는 구조라 고정할 수 있는 건 앞쪽 위치 인자뿐이고, 이름 있는 인자와 섞어 쓰거나 중간 인자만 고정할 수는 없습니다.

PHP code

<?php
function partial(callable $fn, mixed ...$fixed): Closure {
    return function (mixed ...$rest) use ($fn, $fixed): mixed {
        return $fn(...array_merge($fixed, $rest));
    };
}

$startsWith = partial('str_starts_with', 'php-framework');
var_dump($startsWith('php'));
알아두면 좋은 점

고정할 인자가 뒤쪽에 있다면 이 헬퍼 대신 클로저를 직접 써서 자리를 맞추는 편이 간단합니다. 반환 타입을 Closure로 선언해 두면 바로 실행할 수 있음이 드러납니다.

자주 하는 실수

array_merge는 문자열 키를 덮어쓰므로 이름 있는 인자를 뒤로 넘기면 고정해 둔 값이 사라질 수 있습니다. 위치 인자만 다루도록 사용 범위를 좁혀 두어야 합니다.

정리하며

  • 표현식 하나로 끝나는 콜백은 fn(), 여러 문장이 필요하면 use 절 클로저를 씁니다.
  • 콜백을 넘길 때는 문자열 대신 foo(...) 문법으로 오타를 즉시 드러냅니다.
  • 큰 목록은 배열을 만들어 반환하지 말고 Generator로 흘려보내 메모리를 고정합니다.
  • 제너레이터는 일회성이므로 재사용이 필요하면 팩토리 함수로 매번 새로 만듭니다.

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