PHP · 심층 가이드
PHP 컬렉션 완전 정리
리스트와 해시맵을 한 타입으로 겸하는 PHP 배열의 성질을 짚고, array_map 계열과 정렬, 키 보존 규칙을 실제 데이터 가공 순서대로 익히게 합니다.
다른 언어에서 리스트와 딕셔너리로 갈라지는 두 자료구조를 PHP는 array 하나로 처리합니다. 내부적으로는 삽입 순서를 유지하는 정렬된 해시맵이고, 정수 키가 0부터 빈틈없이 이어질 때만 우리가 아는 배열처럼 보일 뿐입니다. 게다가 배열은 객체와 달리 값 의미론을 가져서 대입하거나 인자로 넘기면 논리적으로 복사됩니다. 실제 복사는 수정이 일어날 때까지 미뤄지므로 성능 걱정은 대개 불필요하고, 대신 함수 안에서 배열을 바꿔도 호출자에게 반영되지 않는다는 점이 중요합니다.
인덱스 배열 만들기와 연관 배열 만들기로 두 얼굴을 확인한 뒤에는 가공 3종인 array_map 사용, array_filter 사용, array_reduce 합계를 묶어 보십시오. 실무 데이터는 대개 레코드 목록이라 array_column 으로 열 추출이 유용하고, 정렬은 sort 와 rsort 비교로 감을 잡은 뒤 기준이 복잡해지면 usort 사용자 정의 정렬로 넘어가면 됩니다. 마지막의 SplQueue 사용은 배열로 큐를 흉내 낼 때의 재색인 비용을 피하는 대안입니다.
가장 자주 사고가 나는 건 키 보존 규칙입니다. array_filter는 원본 키를 남기므로 중간 원소가 걸러지면 인덱스에 구멍이 생기고, 그 배열은 json_encode에서 배열이 아니라 객체로 직렬화됩니다. API 응답이 {"0":…,"2":…} 로 나오는 원인이 대부분 이것이고, 해법은 array_values() 한 번입니다. array_merge 와 더하기 연산자 차이도 같은 맥락으로, + 는 왼쪽에 이미 있는 키를 덮어쓰지 않지만 array_merge는 정수 키를 새로 매깁니다.
01인덱스 배열 만들기
값만 나열해 만든 배열에는 0부터 시작하는 정수 키가 자동으로 붙고, print_r이 그 키까지 함께 보여 줍니다. 순서가 의미를 갖는 목록에 씁니다. PHP에는 별도의 리스트 타입이 없어 이것도 내부적으로는 키가 정수인 순서 있는 해시 맵이며, 중간 원소를 지우면 번호에 구멍이 남습니다.
PHP code
<?php
$langs = ['php', 'python', 'go'];
print_r($langs);값만 나열한 배열인지 확인하려면 array_is_list()를 쓰면 되고, 이 함수가 true인 배열만 json_encode에서 JSON 배열로 나갑니다.
unset($langs[1])로 중간을 지우면 키가 0과 2로 남아 JSON 직렬화 결과가 배열이 아닌 객체로 바뀝니다. 프런트엔드에서 목록을 돌리다 타입 오류가 나는 원인이 됩니다.
02연관 배열 만들기
=>로 문자열 키에 값을 짝지어 두고 대괄호로 꺼냅니다. 레코드 한 건처럼 이름 붙은 필드를 다룰 때 쓰며, 없는 키를 읽으면 경고와 함께 null이 돌아옵니다. 키 이름 오타가 실행 시점까지 그대로 통과하므로, 구조가 고정되어 있다면 프로퍼티가 정의된 클래스 쪽이 안전합니다.
PHP code
<?php
$user = ['id' => 1, 'name' => 'mida'];
echo $user['name'];정수처럼 생긴 문자열 키는 자동으로 정수로 바뀌어 '1'과 1이 같은 칸을 가리킵니다. 키 타입을 보존해야 한다면 배열 대신 객체나 SplObjectStorage를 씁니다.
$user['nmae']처럼 키를 잘못 적으면 경고와 함께 null이 돌아와 화면에는 빈칸만 남습니다. 오타가 실행 전에 잡히지 않아 원인 추적이 오래 걸립니다.
03배열에 값 추가
$stack[] = ...와 array_push는 모두 배열 끝에 값을 덧붙이며 결과는 같습니다. 값 하나만 넣을 때는 함수 호출이 없는 대괄호 형태가 관용적이고, array_push는 한 번에 여러 값을 넣을 때 의미가 있습니다. 새로 붙는 정수 키는 지금까지 쓰인 최대 정수 키의 다음 값이 됩니다.
PHP code
<?php
$stack = ['php'];
$stack[] = 'laravel';
array_push($stack, 'symfony');
print_r($stack);array_push()는 첫 인자를 참조로 받아 배열을 제자리에서 늘리고 새 길이를 돌려줍니다. 배열이 아닌 값에 []= 를 쓰면 오류가 나므로 초기화가 먼저 필요합니다.
$a[10] = 'x' 뒤에 $a[] = 'y'를 하면 새 키가 11이 되어 0부터 이어지지 않습니다. 이 배열을 리스트로 여기고 순번을 믿으면 인덱스 접근에서 어긋납니다.
04array_map 사용
각 원소에 화살표 함수를 적용해 제곱한 값으로 새 배열을 만듭니다. 원본은 손대지 않으므로 반환값을 받아 써야 합니다. 배열을 하나만 넘기는 이 형태에서는 키가 유지되지만, 배열을 둘 이상 넘기면 키를 버리고 0부터 다시 매긴다는 점이 array_map의 헷갈리는 부분입니다.
PHP code
<?php
$numbers = [1, 2, 3];
$squared = array_map(fn (int $n): int => $n * $n, $numbers);
print_r($squared);콜백에 null을 넘기고 배열을 여러 개 주면 같은 위치끼리 묶인 배열이 나옵니다. 키까지 다뤄야 하면 array_keys()의 결과를 두 번째 배열로 함께 넘기는 방법이 있습니다.
반환값을 받지 않고 호출만 하면 아무 일도 일어나지 않습니다. 원본을 바꾸려는 의도였다면 array_map이 아니라 array_walk나 foreach가 맞습니다.
05array_filter 사용
콜백이 true를 돌려준 원소만 남겨 짝수를 걸러 냅니다. 남은 원소가 원래 키를 그대로 유지하기 때문에 여기서는 1번과 3번 키만 남고 0부터 이어지지 않습니다. 이 상태로 json_encode하면 배열이 아니라 객체로 직렬화되므로, API 응답에는 array_values로 키를 다시 매겨야 합니다.
PHP code
<?php
$numbers = [1, 2, 3, 4, 5];
$even = array_filter($numbers, fn (int $n): bool => $n % 2 === 0);
print_r($even);콜백을 생략하면 falsy한 값이 전부 걸러져 0이나 '0'까지 사라집니다. 플래그로 ARRAY_FILTER_USE_KEY나 USE_BOTH를 주면 키를 기준으로도 거를 수 있습니다.
결과에 $even[0]으로 접근하면 첫 원소가 키 1에 있어 정의되지 않은 오프셋이 됩니다. 인덱스로 다룰 배열이라면 array_values()를 반드시 덧씌워야 합니다.
06array_reduce 합계
초기값 0에서 출발해 원소를 하나씩 누적자에 더하며 합계를 만듭니다. 세 번째 인자를 생략하면 초기값이 null이 되어 첫 덧셈에서 타입 힌트와 어긋나므로 명시해 두는 편이 낫습니다. 단순 합계라면 array_sum이 짧고, array_reduce는 누적 결과가 숫자가 아닌 구조일 때 값어치를 합니다.
PHP code
<?php
$numbers = [10, 20, 30];
$sum = array_reduce($numbers, fn (int $carry, int $n): int => $carry + $n, 0);
echo $sum;누산기를 배열이나 객체로 두면 그룹핑이나 집계 구조를 한 번의 순회로 만들 수 있습니다. 콜백 인자 순서는 항상 누산기가 앞, 현재 원소가 뒤입니다.
빈 배열에 초기값 없이 부르면 결과가 null이라 이어서 숫자로 쓰면 타입 오류로 이어집니다. 초기값을 명시해야 빈 입력에서도 타입이 유지됩니다.
07array_column 으로 열 추출
행들의 배열에서 name 필드만 뽑아 평평한 배열로 만듭니다. DB 조회 결과에서 ID만 모아 IN 절에 넘길 때 자주 씁니다. 세 번째 인자로 키가 될 필드를 주면 id => name 형태의 맵이 한 번에 나오고, 배열뿐 아니라 객체의 public 프로퍼티에도 동작합니다.
PHP code
<?php
$users = [
['id' => 1, 'name' => 'Kim'],
['id' => 2, 'name' => 'Lee'],
];
print_r(array_column($users, 'name'));array_column($users, null, 'id')처럼 두 번째 인자를 null로 두면 행 전체를 id로 인덱싱한 맵이 나옵니다. 두 결과셋을 ID로 맞붙일 때 유용합니다.
지정한 키가 없는 행은 오류 없이 조용히 빠져 결과 개수가 입력보다 줄어듭니다. 개수를 확인하지 않으면 IN 절에서 일부 ID가 누락된 채 쿼리가 나갑니다.
08array_combine 사용
키 배열과 값 배열을 순서대로 짝지어 하나의 연관 배열로 합칩니다. 헤더 행과 데이터 행이 분리되어 들어오는 CSV를 다룰 때 쓸모가 있습니다. 두 배열의 길이가 다르면 ValueError가 던져지므로, 길이가 보장되지 않는 입력이라면 count를 먼저 비교해야 합니다.
PHP code
<?php
$keys = ['host', 'port'];
$values = ['localhost', 3306];
print_r(array_combine($keys, $values));키 배열에 중복이 있으면 뒤에 오는 짝이 앞의 값을 덮어써 결과 길이가 줄어듭니다. CSV 헤더처럼 중복 가능성이 있는 입력에는 미리 검사가 필요합니다.
count()만 비교하고 넘어가면 키 배열에 null이나 배열이 섞였을 때 여전히 TypeError가 납니다. 외부에서 들어온 헤더는 값 타입까지 확인해야 합니다.
09array_chunk 로 묶기
1부터 7까지를 세 개씩 끊어 3, 3, 1 크기의 하위 배열 세 개로 나눕니다. 대량 INSERT나 배치 API 호출을 정해진 크기로 쪼갤 때 씁니다. 조각 안의 키는 기본적으로 0부터 다시 매겨지므로, 원래 키를 살려야 하면 세 번째 인자를 true로 넘겨야 합니다.
PHP code
<?php
$numbers = range(1, 7);
print_r(array_chunk($numbers, 3));마지막 조각은 나머지 개수만큼만 채워지므로 크기가 균일하다고 가정하면 안 됩니다. INSERT 배치라면 조각 크기를 쿼리 파라미터 개수 한도에 맞춰 정합니다.
연관 배열을 기본 설정으로 넘기면 문자열 키가 0부터의 정수로 바뀌어 사라집니다. 키가 의미를 갖는 데이터라면 세 번째 인자를 true로 줘야 합니다.
10sort 와 rsort 비교
복사본을 두 개 만들어 각각 오름차순과 내림차순으로 정렬합니다. 두 함수 모두 배열을 참조로 받아 제자리에서 바꾸고 반환값은 성공 여부일 뿐이라, 원본을 남기려면 이렇게 미리 복사해야 합니다. 키를 버리고 0부터 다시 매기니 연관 배열에는 asort나 ksort가 맞습니다.
PHP code
<?php
$scores = [70, 95, 88];
$asc = $scores;
sort($asc);
$desc = $scores;
rsort($desc);
print_r([$asc, $desc]);PHP 8부터 숫자와 숫자 문자열 비교가 더 직관적으로 바뀌었고, 비교 방식을 못 박고 싶으면 SORT_STRING이나 SORT_NUMERIC 플래그를 두 번째 인자로 줍니다.
sort 계열 함수는 새 배열을 반환하지 않고 원본 배열을 직접 수정합니다.
11usort 사용자 정의 정렬
비교 콜백에서 $b와 $a의 점수를 우주선 연산자로 견주어 높은 점수가 앞에 오도록 정렬합니다. 기준이 필드 하나 이상이거나 역순일 때 필요한 방식입니다. usort도 키를 버리고 다시 매기며, 값이 같은 원소끼리의 원래 순서는 PHP 8.0부터 유지됩니다.
PHP code
<?php
$users = [
['name' => 'Kim', 'score' => 90],
['name' => 'Lee', 'score' => 98],
];
usort($users, fn (array $a, array $b): int => $b['score'] <=> $a['score']);
print_r($users);콜백이 0을 돌려주는 경우가 많다면 두 번째 기준을 ?: 로 이어 붙여 결정적인 순서를 만들 수 있습니다. 콜백 안에서 원본 배열을 건드리면 동작이 정의되지 않습니다.
$a와 $b 자리를 헷갈려 $a['score'] <=> $b['score']로 적으면 랭킹이 거꾸로 나옵니다. 오류가 아니라 순서만 뒤집혀 눈으로 보기 전까지 지나칩니다.
12array_unique 중복 제거
중복 값을 걸러 낸 다음 array_values로 구멍 난 키를 0부터 다시 채웁니다. 원본에서는 뒤에 나온 중복이 제거되고 앞의 것이 남습니다. 비교가 기본적으로 문자열 변환 후 이뤄져 1과 '1'이 같은 값으로 묶이므로, 타입이 섞인 배열이라면 두 번째 인자로 비교 방식을 지정해야 합니다.
PHP code
<?php
$tags = ['php', 'api', 'php', 'queue'];
print_r(array_values(array_unique($tags)));두 번째 인자로 SORT_REGULAR를 주면 타입을 유지한 비교가 됩니다. 문자열 목록에서 중복만 없애면 되는 경우에는 array_flip을 두 번 거는 편이 더 빠릅니다.
정수 배열에 기본 옵션을 쓰면 10과 '10'이 하나로 합쳐집니다. ID 목록에서 이런 일이 생기면 존재하는 레코드를 조회 대상에서 통째로 빠뜨리게 됩니다.
13array_flip 키와 값 뒤집기
키와 값의 자리를 맞바꿔 admin => owner 형태를 만듭니다. 값으로 키를 되찾는 조회를 여러 번 해야 한다면 array_search를 반복하는 대신 한 번 뒤집어 두는 편이 낫습니다. 값이 겹치면 나중 것이 앞의 것을 덮어쓰고, 값이 정수나 문자열이 아닌 원소는 경고와 함께 빠집니다.
PHP code
<?php
$roles = ['owner' => 'admin', 'member' => 'user'];
print_r(array_flip($roles));뒤집은 결과 역시 키가 정수와 문자열만 가능하다는 배열 규칙을 그대로 따릅니다. 존재 여부만 자주 본다면 이렇게 뒤집어 두고 isset()으로 확인하는 편이 훨씬 빠릅니다.
값이 중복된 배열을 뒤집으면 앞 항목이 조용히 사라져 원소 수가 줄어듭니다. 권한 맵처럼 값이 겹치기 쉬운 데이터에서는 되돌릴 수 없는 손실이 됩니다.
14in_array 에 strict 옵션 주기
세 번째 인자 true가 === 비교를 강제해, 정수 1은 문자열 '1'과 다른 값으로 취급되어 false가 나옵니다. 이 인자를 빼면 느슨한 비교가 되어 의도치 않은 일치가 생깁니다. 큰 배열에서 포함 여부만 자주 본다면 값을 키로 옮겨 isset으로 확인하는 편이 훨씬 빠릅니다.
PHP code
<?php
$values = [0, '1', 2];
var_dump(in_array(1, $values, true));세 번째 인자는 array_search()에도 똑같이 있습니다. 값이 객체면 엄격 비교는 같은 인스턴스인지를 보고 느슨한 비교는 프로퍼티 값만 견줍니다.
strict 플래그를 생략하면 문자열 숫자와 정수가 같은 값처럼 비교될 수 있습니다.
15isset 와 array_key_exists 차이
값이 null인 키에 대해 isset은 false를, array_key_exists는 true를 돌려줍니다. 키가 아예 없는 것과 값이 비어 있는 것을 구분해야 하는 설정 병합이나 부분 수정 API에서 이 차이가 결과를 가릅니다. null을 유효한 값으로 쓰지 않는다면 더 빠른 isset으로 충분합니다.
PHP code
<?php
$data = ['token' => null];
var_dump(isset($data['token']));
var_dump(array_key_exists('token', $data));array_key_exists는 함수 호출이라 언어 구조인 isset보다 느리고, 객체 프로퍼티에는 쓸 수 없어 property_exists를 따로 씁니다.
PATCH 요청 본문을 isset으로 검사하면 null을 명시적으로 보낸 필드와 아예 보내지 않은 필드를 구분하지 못합니다. 값을 지우려는 요청이 조용히 무시됩니다.
16다차원 배열 읽기
중첩된 배열을 대괄호를 이어 붙여 안쪽 값까지 내려가 읽습니다. 설정을 배열로 반환하는 파일 구조에서 흔히 보는 접근 방식입니다. 중간 단계 키가 빠져 있으면 그 지점부터 경고가 쏟아지므로, 있을지 없을지 모르는 경로에는 $config['db']['host'] ?? '127.0.0.1' 형태를 씁니다.
PHP code
<?php
$config = [
'db' => ['host' => 'localhost', 'port' => 3306],
];
echo $config['db']['host'];설정 파일을 return [...]로 끝내면 include의 반환값으로 배열을 그대로 받을 수 있습니다. 깊은 경로가 잦다면 점 표기법 헬퍼를 하나 두는 편이 읽기 좋습니다.
?? 기본값은 중간 키가 없어도 조용히 넘어가기 때문에, 설정 파일 오타로 기본값이 계속 쓰이는 상황을 알아채지 못합니다. 필수 키는 따로 검사해야 합니다.
17array_merge 와 더하기 연산자 차이
array_merge는 뒤 배열의 값으로 같은 키를 덮어쓰고, +는 앞 배열에 이미 있는 키를 지킵니다. 그래서 version이 각각 8.3과 8.2로 갈립니다. 정수 키에서는 차이가 더 커져서 array_merge는 뒤 배열을 이어 붙이며 번호를 다시 매기지만, +는 번호가 겹치는 원소를 통째로 버립니다.
PHP code
<?php
$a = ['name' => 'php', 'version' => '8.2'];
$b = ['version' => '8.3', 'type' => 'backend'];
print_r(array_merge($a, $b));
print_r($a + $b);+ 연산자는 왼쪽 값을 우선하므로 $입력 + $기본값 순서로 적으면 기본값 채우기가 한 줄로 끝납니다. 깊은 병합이 필요하면 array_replace_recursive를 봅니다.
정수 키 목록 두 개를 +로 합치면 겹치는 번호의 원소가 통째로 사라져 결과가 짧아집니다. 목록을 이어 붙일 의도였다면 array_merge나 스프레드를 써야 합니다.
18array_walk 로 제자리 가공
콜백 인자를 참조로 받아 각 행에 slug 키를 원본 자리에서 직접 덧붙입니다. 새 배열을 만들지 않고 기존 구조에 필드를 보태고 싶을 때 씁니다. 반환값은 성공 여부일 뿐이라 결과를 대입하면 안 되고, 인자 앞의 &를 빠뜨리면 오류 없이 아무것도 바뀌지 않아 원인을 찾기 어렵습니다.
PHP code
<?php
$rows = [
['name' => 'php'],
['name' => 'go'],
];
array_walk($rows, function (array &$row): void {
$row['slug'] = strtolower($row['name']);
});
print_r($rows);세 번째 인자는 콜백의 추가 인자로 전달되어 매번 use로 잡지 않아도 됩니다. 중첩 배열까지 훑어야 하면 array_walk_recursive가 따로 있습니다.
콜백 인자에 &를 빠뜨리면 사본만 고쳐져 원본은 그대로인데 오류도 나지 않습니다. 화면에 값이 비어 있는데 코드는 정상으로 보여 원인을 찾는 데 시간이 걸립니다.
19교집합과 차집합
array_intersect는 양쪽에 모두 있는 값을, array_diff는 첫 배열에만 있는 값을 남깁니다. 권한 목록이나 태그 비교처럼 집합 연산이 필요할 때 꺼내 씁니다. 두 함수 모두 값을 문자열로 바꿔 비교하고 원래 키를 유지하므로, 번호가 이어진 배열이 필요하면 array_values를 덧씌웁니다.
PHP code
<?php
$a = ['php', 'go', 'python'];
$b = ['php', 'rust'];
print_r(array_intersect($a, $b));
print_r(array_diff($a, $b));키를 기준으로 걸러야 하면 array_intersect_key와 array_diff_key를, 비교 규칙을 직접 정하려면 array_udiff나 array_uintersect를 씁니다. 요청 본문에서 허용 필드만 남길 때 유용합니다.
두 함수 모두 값을 문자열로 바꿔 비교하므로 0과 '0'이 같게 취급되고, 다차원 배열에는 그대로 쓸 수 없습니다. 객체가 섞이면 문자열 변환에서 오류가 납니다.
20SplQueue 사용
enqueue로 뒤에 넣고 dequeue로 앞에서 꺼내는 선입선출 큐입니다. 기본 상태에서는 foreach로 순회해도 원소가 남아 있지만, setIteratorMode로 삭제 모드를 켜면 순회하면서 꺼내는 동작으로 바뀝니다. 원소마다 노드 객체가 붙어 메모리는 일반 배열보다 더 씁니다.
PHP code
<?php
$queue = new SplQueue();
$queue->enqueue('job-1');
$queue->enqueue('job-2');
echo $queue->dequeue();SplQueue는 SplDoublyLinkedList를 상속해 push나 shift 같은 리스트 메서드도 그대로 쓸 수 있습니다. 우선순위가 필요하면 SplPriorityQueue가 따로 있습니다.
빈 큐에 dequeue()를 부르면 RuntimeException이 던져지므로 isEmpty()로 먼저 확인해야 합니다. 배열처럼 $queue[0]으로 꺼내려 해도 없는 인덱스에서 예외가 납니다.
정리하며
- array_filter 결과를 JSON으로 내보내기 전에 array_values로 인덱스를 다시 채웁니다.
- in_array와 array_search는 세 번째 인자
true로 엄격 비교를 켭니다. - 키 존재 확인은 값이 null일 수 있으면 isset 대신 array_key_exists를 씁니다.
- 정수 키를 유지하려면 array_merge가 아니라
+연산자를 선택합니다.
더 깊이 들어가고 싶다면 PHP 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.