PHP 기초: 자료형·== 와 ===·문자열·배열을 실행 결과로
PHP 8.5 로 직접 실행해 확인. PHP 8 에서 바뀐 0 == "a", strict_types 의 TypeError, 한글 strlen 과 mb_strlen, 배열 키 변환과 foreach & 함정.
시리즈: PHP 강좌 1/6 (초급). 입사해서 PHP 코드를 처음 맡게 된 신입 개발자를 위한 글입니다. 다 읽고 나면 스크립트를 실행하고, PHP 가 값을 어떻게 비교·변환하는지 미리 예측하고, 한글 문자열을 깨뜨리지 않고 다루고, PHP 초보자가 꼭 밟는 배열 함정 두 가지를 피할 수 있습니다.
국내에는 아직도 PHP 로 돌아가는 쇼핑몰·그누보드 계열 커뮤니티·사내 관리자 페이지가 많고, 신입이 처음 받는 유지보수 업무가 PHP 인 경우도 흔합니다. 그런데 몇 년 전 블로그 글을 보고 따라 하면 결과가 다르게 나와서 헷갈립니다. 원인은 대부분 PHP 8 에서 == 의 동작, 타입 선언의 처리, 그리고 "조용히 넘어가던 실수가 경고로 바뀐 범위"가 달라졌기 때문입니다. 아래 출력은 모두 Apple Silicon Mac(M1 Pro)에서 PHP 8.5.6 CLI 로 직접 실행한 결과이고, 에러 메시지의 파일 경로만 ~/demo/ 로 바꿨습니다.
5분 안에 첫 PHP 스크립트
php -v 로 버전이 나오면 준비 끝입니다(없다면 macOS 는 Homebrew, Windows 는 windows.php.net 의 zip, Ubuntu 는 apt install php-cli php-mbstring 이 보통입니다. 이 글을 위해 따로 설치한 것은 없습니다). hello.php 를 만듭니다.
<?php
echo "Hello, PHP " . PHP_VERSION . "\n";
$ php hello.php
Hello, PHP 8.5.6
눈여겨볼 점 세 가지입니다.
- PHP 파일은 여는 태그
<?php로 시작합니다. PHP 코드만 있는 파일에서는 닫는 태그?>를 쓰지 않습니다. 닫는 태그 뒤의 줄바꿈이 출력돼 버리는 사고를 막을 수 있습니다. echo는 출력, 점.은 문자열 연결입니다.+는 숫자 전용입니다.PHP_VERSION은 내장 상수(값이 바뀌지 않는 이름)입니다.
한 줄만 시험할 때는 php -r 'echo 1 + 1;' 가 편합니다. 아래에서도 몇 번 씁니다.
변수와 자료형
변수는 $ 로 시작합니다. 자료형을 선언하지 않고, 값이 자료형을 가집니다. get_debug_type()(PHP 8.0 이상)은 에러 메시지와 같은 이름으로 자료형을 알려 줍니다.
<?php
$n = 42; $f = 3.14; $s = "42"; $b = true; $x = null; $a = [1, 2];
foreach ([$n, $f, $s, $b, $x, $a] as $v) {
echo get_debug_type($v), " ";
}
echo "\n";
var_dump("10" + 5, "1.5" + 1, 7 / 2, intdiv(7, 2), 0.1 + 0.2 == 0.3);
int float string bool null array
int(15)
float(2.5)
float(3.5)
int(3)
bool(false)
var_dump() 는 자료형과 값을 함께 보여 주는, 공부하는 동안 가장 많이 쓸 도구입니다. 다른 언어에서 온 사람이 놀라는 점은 둘입니다. 숫자 형태 문자열 "10" 에 + 5 를 하면 숫자로 바뀌어 15 가 된다는 것, 그리고 / 는 나누어떨어지지 않으면 항상 float 를 돌려준다는 것입니다. 정수 나눗셈은 intdiv() 를 씁니다. 0.1 + 0.2 == 0.3 이 false 인 것은 어느 언어에서나 같은 2진 부동소수점 오차입니다.
== 와 ===, PHP 8 에서 바뀐 것
== 는 느슨한 비교로, 비교하기 전에 한쪽을 변환할 수 있습니다. === 는 엄격한 비교로, 자료형과 값이 모두 같아야 true 입니다. 단골 사례를 PHP 8.5.6 으로 실행했습니다.
<?php
$cases = [
['0 == "a"', 0 == "a"], ['"1" == "01"', "1" == "01"],
['"10" == "1e1"', "10" == "1e1"], ['100 == "1e2"', 100 == "1e2"],
['null == false', null == false], ['[] == false', [] == false],
['"0" == false', "0" == false], ['"" == null', "" == null],
['1 === "1"', 1 === "1"], ['42 == "42abc"', 42 == "42abc"],
['42 == " 42"', 42 == " 42"], ['42 == "42 "', 42 == "42 "],
];
foreach ($cases as [$label, $r]) printf("%-16s %s\n", $label, var_export($r, true));
0 == "a" false
"1" == "01" true
"10" == "1e1" true
100 == "1e2" true
null == false true
[] == false true
"0" == false true
"" == null true
1 === "1" false
42 == "42abc" false
42 == " 42" true
42 == "42 " true
첫 줄이 유명한 PHP 8 변경점입니다. PHP 7 까지는 0 == "a" 에서 "a" 가 숫자 0 으로 바뀌어 true 가 됐고, in_array("abc", [0]) 까지 true 가 되어 로그인 검사 같은 코드를 망가뜨렸습니다. PHP 8.0 부터는 문자열이 숫자 형태가 아니면 숫자 쪽을 문자열로 바꿔 비교하므로 false 입니다(이번에 실행한 것은 8.5 뿐이고, PHP 7 의 동작은 RFC "Saner string to number comparisons" 의 설명을 따른 것입니다).
나머지 줄도 보세요. 숫자처럼 보이는 문자열끼리는 지금도 숫자로 비교하므로 "1" == "01" 도 "10" == "1e1" 도 true 입니다. 상품 코드나 회원 번호, 비밀번호라면 그대로 버그입니다. null, false, "", "0", [] 는 전부 false 와 느슨하게 같습니다.
| 비교 대상 | 쓸 것 |
|---|---|
| ID·코드·토큰·사용자 입력 | === (비밀 값은 hash_equals()) |
| 배열 안에서 찾기 | in_array($x, $arr, true), array_search($x, $arr, true) |
| 값이 없는지 확인 | $x === null, 또는 isset() / ?? (2강) |
| 폼에서 문자열로 오는 숫자 | 먼저 (int)·filter_var 로 변환한 뒤 === |
기본은 ===, == 는 변환 내용을 설명할 수 있을 때만 쓴다고 기억하면 됩니다.
declare(strict_types=1) 와 실제 TypeError
함수의 인자와 반환값에 자료형을 적을 수 있습니다. 기본은 강제 변환 모드(coercive mode) 로, 변환할 수 있는 값이면 조용히 변환합니다.
<?php
function addTax(int $price): int {
return (int) round($price * 1.1);
}
var_dump(addTax("1000"));
var_dump(addTax(1000.0));
var_dump(addTax("1000 yen"));
int(1100)
int(1100)
Fatal error: Uncaught TypeError: addTax(): Argument #1 ($price) must be of type int, string given, called in ~/demo/loose.php on line 7 and defined in ~/demo/loose.php:2
Stack trace:
#0 ~/demo/loose.php(7): addTax('1000 yen')
#1 {main}
thrown in ~/demo/loose.php on line 2
"1000" 은 아무 말 없이 1000 이 됐습니다. 파일의 첫 문장으로 declare(strict_types=1); 를 쓰면 같은 호출이 거부됩니다.
<?php
declare(strict_types=1);
function addTax(int $price): int {
return (int) round($price * 1.1);
}
var_dump(addTax(1000));
var_dump(addTax("1000"));
int(1100)
Fatal error: Uncaught TypeError: addTax(): Argument #1 ($price) must be of type int, string given, called in ~/demo/strict.php on line 8 and defined in ~/demo/strict.php:4
읽는 법: 에러 종류는 TypeError, "Argument #1 ($price)" 는 어느 인자인지, "called in … line 8" 은 잘못된 값을 넘긴 곳입니다. 고칠 곳은 함수가 아니라 호출하는 쪽입니다.
초보자가 놓치기 쉬운 두 가지가 있습니다.
- strict_types 는 선언한 파일에서 하는 호출에 적용됩니다. 함수가 정의된 파일 기준이 아닙니다. strict 한 라이브러리라도 strict 가 아닌 파일에서 부르면 변환됩니다.
- strict 모드에서도
int→float는 허용됩니다(float $x에5를 넘기면float(5)). 반대로 강제 변환 모드에서int인자에1.5를 넘기면Deprecated: Implicit conversion from float 1.5 to int loses precision이 나오고 값은int(1)이 됩니다. 이 Deprecated 는 PHP 8.1 에서 추가됐습니다.
터미널에서는 같은 에러가 PHP 를 앞에 붙여 한 번 더 나올 수 있습니다. CLI 가 로그(표준 에러)와 화면 양쪽에 쓰기 때문이며, 글에서는 중복을 뺐습니다.
문자열: 따옴표·변수 치환·히어독
<?php
$name = "Mina";
$n = 3;
echo 'Hi $name\n', "\n";
echo "Hi $name, you have {$n} new messages\n";
$user = ['name' => 'Mina'];
echo "Hi {$user['name']}\n";
echo <<<TXT
Dear $name,
You have {$n} items.
TXT;
echo "\n";
echo <<<'RAW'
No $interpolation here\n
RAW;
Hi $name\n
Hi Mina, you have 3 new messages
Hi Mina
Dear Mina,
You have 3 items.
No $interpolation here\n
- 작은따옴표: 거의 아무것도 해석하지 않습니다.
$name도\n도 적은 그대로입니다. - 큰따옴표: 변수가 치환(값으로 바뀜)되고
\n같은 이스케이프가 동작합니다. 단순한 변수가 아니면{…}로 감쌉니다. - 히어독(heredoc)
<<<TXT은 여러 줄짜리 큰따옴표 문자열, 나우독(nowdoc)<<<'RAW'는 작은따옴표 버전입니다. PHP 7.3 부터 닫는 표시를 들여 쓸 수 있고, 그만큼의 들여쓰기가 모든 줄에서 제거됩니다.
한글 문자열: strlen 과 mb_strlen
PHP 문자열은 바이트열입니다. strlen() 은 바이트를 셉니다. 사람이 읽는 문자열에는 mbstring 확장의 mb_(멀티바이트) 함수가 필요합니다.
<?php
foreach (["hello", "こんにちは", "안녕하세요", "👍🏽"] as $s) {
printf("%-12s strlen=%2d mb_strlen=%d\n", $s, strlen($s), mb_strlen($s));
}
$s = "こんにちは";
var_dump(substr($s, 0, 4), mb_substr($s, 0, 2));
var_dump(strrev("안녕"));
var_dump(mb_strwidth("こんにちは"), grapheme_strlen("👍🏽"));
hello strlen= 5 mb_strlen=5
こんにちは strlen=15 mb_strlen=5
안녕하세요 strlen=15 mb_strlen=5
👍🏽 strlen= 8 mb_strlen=2
string(4) "こ�"
string(6) "こん"
string(6) "��눕�"
int(10)
int(1)
출력 자체가 증거입니다. printf("%-12s") 는 바이트 수로 칸을 맞추기 때문에 한글·일본어 줄만 어긋났습니다. 한글 음절 하나는 UTF-8 에서 3바이트입니다. substr($s, 0, 4) 는 두 번째 글자를 중간에서 잘라 깨진 바이트(�)를 만들었고, strrev 는 글자가 아니라 바이트를 뒤집어 "녕안" 대신 깨진 문자열을 냈습니다. 피부색이 붙은 이모지는 코드 포인트 2개(mb_strlen = 2)지만 눈에는 한 글자이고, 1 이라고 답하는 것은 intl 확장의 grapheme_strlen() 뿐입니다.
오래된 EUC-KR 데이터를 만났을 때
국내 레거시 사이트는 DB 나 CSV 가 EUC-KR(CP949)인 경우가 아직 있습니다. 인코딩을 지정하지 않으면 mb_ 함수도 틀립니다.
<?php
$e = mb_convert_encoding("안녕하세요", "EUC-KR", "UTF-8");
var_dump(strlen($e), mb_strlen($e, "EUC-KR"), mb_strlen($e), mb_check_encoding($e, "UTF-8"));
int(10)
int(5)
int(8)
bool(false)
EUC-KR 에서는 한글이 2바이트라서 10바이트이고, 인코딩을 알려 주면 5글자, 기본값(UTF-8)으로 세면 엉뚱한 8 이 나옵니다. 입력을 받자마자 mb_check_encoding 으로 확인하고 mb_convert_encoding 으로 UTF-8 로 바꿔 두는 것이 원칙입니다.
실무 규칙: 닉네임 글자 수 제한, "앞 20글자" 미리보기, 영문 외 대문자 변환에는 mb_strlen, mb_substr, mb_strtoupper 를 씁니다. 새 함수도 있습니다. mb_str_pad(8.3), mb_trim 과 mb_ucfirst(8.4)입니다. 8.5 에서 trim(" 全角スペース ") 은 전각 공백을 남겼고 mb_trim 은 지웠습니다.
배열은 "순서 있는 맵"
PHP 배열은 C 나 자바의 리스트와 다른 순서 있는 맵입니다. 키(int 또는 string)와 값의 쌍을 넣은 순서대로 보관합니다. 함정은 PHP 가 일부 키를 멋대로 바꾼다는 점입니다.
<?php
$a = [];
$a["1"] = "string one";
$a[1] = "int one";
$a[true] = "bool true";
$a["01"] = "string 01";
$a[1.7] = "float 1.7";
$a[null] = "null";
$a["1.5"] = "string 1.5";
var_dump($a);
Deprecated: Implicit conversion from float 1.7 to int loses precision in ~/demo/arr.php on line 7
Deprecated: Using null as an array offset is deprecated, use an empty string instead in ~/demo/arr.php on line 8
array(4) {
[1]=>
string(9) "float 1.7"
["01"]=>
string(9) "string 01"
[""]=>
string(4) "null"
["1.5"]=>
string(10) "string 1.5"
}
일곱 번 대입했는데 키는 네 개입니다. "1", 1, true, 1.7 이 모두 정수 키 1 이 되어 앞의 값을 덮어썼습니다. "01" 과 "1.5" 는 정규 정수 표기가 아니어서 문자열로 남았습니다. null 은 "" 이 되고, PHP 8.5 부터는 여기에 Deprecated 가 붙습니다.
실무에서 문제가 되는 곳은 여기입니다. DB 에서 가져온 회원 번호("100", "200")를 키로 쓰면 정수 키가 되고, array_merge() 는 정수 키를 새로 매깁니다.
<?php
$byId = ["100" => "Alice", "200" => "Bob"];
print_r(array_merge($byId, ["300" => "Chen"]));
print_r($byId + ["300" => "Chen"]);
Array
(
[0] => Alice
[1] => Bob
[2] => Chen
)
Array
(
[100] => Alice
[200] => Bob
[300] => Chen
)
키를 지켜야 하면 +(또는 array_replace)를 씁니다. 또 ["a", "b", "c"] 에서 unset($list[1]) 을 하면 키가 0 과 2 만 남고, array_is_list()(8.1 이상)는 false, 다음 $list[] = "d" 는 키 3 에 들어갔습니다. 번호를 다시 매기려면 array_values() 입니다.
흔한 실수: foreach 참조로 마지막 요소가 중복된다
모든 상품 가격을 두 배로 바꾼 뒤 출력하려는 흔한 코드입니다.
<?php
$prices = [100, 200, 300];
foreach ($prices as &$p) {
$p = $p * 2;
}
foreach ($prices as $p) {
echo $p, " | ";
}
echo "\n";
print_r($prices);
200 | 400 | 400 |
Array
(
[0] => 200
[1] => 400
[2] => 400
)
실제로 일어나는 일: &$p 때문에 $p 는 각 요소의 참조(같은 저장 공간의 다른 이름)가 됩니다. 첫 루프가 끝나도 $p 는 $prices[2] 의 참조로 남아 있습니다. 두 번째 루프는 각 요소를 "$p 에" 대입하므로 실제로는 $prices[2] 에 200, 400 을 쓰고, 마지막에는 $prices[2] 를 자기 자신에게 복사합니다. 600 은 경고 하나 없이 사라집니다.
해결: 참조 루프 바로 뒤에 unset($p);. 아예 참조를 쓰지 않는 편이 더 안전합니다.
<?php
$prices = [100, 200, 300];
$doubled = array_map(fn($x) => $x * 2, $prices); // new array
foreach ($prices as $k => $v) { $prices[$k] = $v * 2; } // in place, by key
echo implode(" ", $doubled), " / ", implode(" ", $prices), "\n";
200 400 600 / 200 400 600
정의되지 않은 변수·키의 Warning
PHP 8 에서는 "없는 것을 썼다"는 실수 대부분이 눈에 잘 안 띄는 Notice 에서 Warning 으로 올라갔습니다. 스크립트는 멈추지 않으므로 경고를 꼭 읽어야 합니다.
<?php
echo $count + 1, "\n";
$user = ["name" => "Mina"];
echo $user["email"], "|\n";
$list = [10, 20];
echo $list[5], "|\n";
$s = "abc";
echo $s[10], "|\n";
$total = null;
echo $total["sum"], "|\n";
echo $user["email"] ?? "(no email)", "\n";
Warning: Undefined variable $count in ~/demo/undef.php on line 2
1
Warning: Undefined array key "email" in ~/demo/undef.php on line 4
|
Warning: Undefined array key 5 in ~/demo/undef.php on line 6
|
Warning: Uninitialized string offset 10 in ~/demo/undef.php on line 8
|
Warning: Trying to access array offset on null in ~/demo/undef.php on line 10
|
(no email)
정의되지 않은 $count 는 null 로 취급되어 null + 1 로 1 이 출력됐습니다. 변수 이름 오타가 프로그램 중단이 아니라 "합계가 틀리는" 버그가 되는 구조입니다. 마지막 줄이 정당하게 없을 수 있는 키를 다루는 방법인 null 병합 연산자 ?? 입니다. 왼쪽이 없거나 null 이면 오른쪽을 돌려주고, 경고도 나지 않습니다. 2강에서 폼 입력에 씁니다.
실측: 문자열 만들기와 배열 검색
어떤 습관이 실제로 차이를 만드는지 보는 작은 측정입니다. 조건: 숫자 문자열 10만 개(mt_srand(42)), 경우마다 7회 실행한 중앙값, PHP 8.5.6 CLI, Apple Silicon Mac(M1 Pro). CLI 는 기본으로 OPcache 와 JIT 가 꺼져 있고, 그 상태로 쟀습니다. JIT 를 켠 실행에서도 큰 차이는 없었습니다(in_array 가 53~54 ms 에서 47.9 ms, 나머지는 0.1 ms 이내 차이).
<?php
mt_srand(42);
$words = [];
for ($i = 0; $i < 100_000; $i++) $words[] = (string) mt_rand(1, 999_999);
function bench(callable $fn, int $runs = 7): float {
$t = [];
for ($r = 0; $r < $runs; $r++) {
$s = hrtime(true); $fn(); $t[] = (hrtime(true) - $s) / 1e6;
}
sort($t);
return $t[intdiv($runs, 2)];
}
printf("concat: %.3f ms\n", bench(function () use ($words) {
$out = ''; foreach ($words as $w) { $out .= $w . ','; } return $out; }));
printf("implode: %.3f ms\n", bench(fn() => implode(',', $words)));
// 실제 파일에서는 foreach 합계와 array_sum, in_array 와 isset 도 측정
| 작업(10만 개) | 중앙값 1회차 | 중앙값 2회차 |
|---|---|---|
루프 안에서 .= |
2.872 ms | 2.856 ms |
implode() |
0.784 ms | 0.775 ms |
foreach 로 합계 |
2.354 ms | 2.363 ms |
array_sum() |
1.811 ms | 1.811 ms |
1만 개 배열에 in_array 1,000번 |
53.29 ms | 54.10 ms |
array_flip 한 배열에 isset 1,000번 |
0.021 ms | 0.022 ms |
implode() 가 .= 보다 약 3.7배, array_sum() 이 루프보다 약 1.3배 빨랐습니다. 차이는 진짜지만 체감되는 건 수십만 건 규모부터입니다. 정말 중요한 것은 마지막 쌍입니다. in_array 는 매번 리스트 전체를 훑지만, array_flip() 으로 만든 배열의 isset($set[$key]) 는 해시 검색이라 이번에는 약 2,500배 빨랐고, 리스트가 길수록 차이가 벌어집니다(앞의 키 변환 때문에 숫자 문자열은 flip 후 정수 키가 됩니다. isset 에는 문제없지만 키를 꺼내 쓸 때는 주의).
솔직한 한계·버전 메모
- 모든 코드는 Apple M1 Pro 의 macOS 에서 PHP 8.5.6 CLI(Homebrew 빌드, mbstring·intl 포함)로 실행했습니다. PHP 7 이나 예전 8.x 는 실행하지 않았습니다.
0 == "a"의 PHP 7 결과와 아래 "추가 버전"은 PHP 매뉴얼과 RFC 에 근거한 설명입니다. - 버전: 문자열·숫자 비교 변경,
get_debug_type(), 정의되지 않은 변수·키의 Warning 승격(8.0), float→int Deprecated,array_is_list()(8.1),mb_str_pad(8.3),mb_trim·mb_ucfirst(8.4), "Using null as an array offset is deprecated"(8.5).strict_types는 7.0 부터 있습니다. - 측정은 한 대의 기기에서 7회 중앙값이며 공유 호스팅에서는 값이 달라집니다. CLI 기본값대로 OPcache 없이 쟀고, OPcache 와 JIT 는 6강에서 따로 측정합니다.
grapheme_strlen에는 intl 확장이 필요합니다. 저가 호스팅에서는 꺼져 있을 수 있으니php -m으로 확인하세요.
연습 문제
1. 실행하지 말고 결과를 예상한 뒤 php -r 로 확인해 보세요: "abc" == 0, "1" == "01", "10" == "1e1", null == false, "abc" == "ABC".
정답
false, true, true, true, false(PHP 8.5.6). "abc" == 0 은 PHP 8 부터 false. 숫자 형태 문자열 두 개는 숫자로 비교됩니다. null == false 는 bool 로서의 느슨한 비교입니다. 문자열 비교는 대소문자를 구분합니다. === 로 바꾸면 다섯 개 모두 false 입니다.
2. 회원가입 폼에서 "닉네임은 10글자까지"를 strlen($name) <= 10 으로 검사하고 있습니다. 9글자인 "홍길동님환영합니다" 가 거부되는 이유와 고치는 방법은?
정답
strlen("홍길동님환영합니다") 는 27바이트(9 × 3)라서 검사에 걸립니다. mb_strlen($name) <= 10 으로 고칩니다. 직접 실행하면 strlen 은 27, mb_strlen 은 9 였습니다.
3. ["1" => "a", 1 => "b", "1.0" => "c", true => "d"] 의 요소 개수와 키는?
정답
두 개입니다. "1", 1, true 는 모두 정수 키 1 이 되고 최종 값은 "d". "1.0" 은 정규 정수 표기가 아니어서 문자열 키로 남습니다. print_r 는 [1] => d 와 [1.0] => c 를 출력했습니다.
다음 강좌
- 다음: PHP 강좌 2/6: 함수·폼·내장 서버
- 이어지는 강좌: 클래스·인터페이스·enum, PDO·SQLite 와 SQL 인젝션, XSS·CSRF·세션, OPcache·JIT·프로파일링
- 다른 언어의 1강: 파이썬: 자료형과 가변성, 자바: 자료형·문자열·동등성
- 실무에서 만나는 PHP 자료형 문제: 프리페어드 스테이트먼트로 JSON 숫자 타입이 바뀐다
자주 묻는 질문
PHP 8 에서 0 == "a" 는 true 인가요 false 인가요?
false 입니다. PHP 8.0 부터는 숫자와 '숫자 형태가 아닌 문자열'을 비교할 때 숫자 쪽을 문자열로 바꿔 문자열끼리 비교합니다. PHP 7 에서는 "a" 가 0 으로 바뀌어 true 였습니다. 다만 숫자 형태 문자열끼리는 지금도 숫자로 비교하므로 PHP 8.5 에서도 "1" == "01" 과 "10" == "1e1" 은 true 입니다. 그래서 기본은 === 입니다.
다섯 글자 한글인데 strlen() 이 15 를 돌려주는 이유는?
strlen() 은 글자 수가 아니라 바이트 수를 세기 때문입니다. UTF-8 에서 한글 음절은 한 글자에 3바이트라서 안녕하세요 는 strlen=15, mb_strlen=5 였습니다. 화면에 보이는 문자열에는 mb_strlen, mb_substr 같은 mb_ 함수를 씁니다.
foreach 를 돌린 뒤 배열의 마지막 요소가 중복되는 이유는?
foreach ($a as &$v) 가 끝나도 $v 는 마지막 요소를 가리키는 참조로 남습니다. 이어지는 foreach ($a as $v) 가 매번 그 마지막 칸에 값을 쓰기 때문에 [200, 400, 600] 이 [200, 400, 400] 이 됐습니다. 참조 루프 바로 뒤에 unset($v) 를 쓰거나, array_map 또는 키로 대입해 참조를 쓰지 않습니다.