배열에서 존재하지 않는 인덱스에 접근하면 화면에 빨간 경고가 떴던 경험 있을까? 대부분의 개발자들은 이 에러를 무시하거나, 인터넷에서 @(에러 억제 연산자)를 붙여서 대충 넘어간다. 하지만 이건 근본적인 해결책이 아니다. 이번에는 PHP의 가장 흔한 에러 중 하나인 "Undefined offset" 에러가 정확히 뭔지, 왜 발생하는지, 그리고 어떻게 안전하게 처리하는지 완벽하게 정리해서 소개하겠다.
"Undefined offset"은 배열에 존재하지 않는 인덱스(또는 키)에 접근하려고 할 때 발생하는 Notice 레벨 에러다. 에러 메시지는 보통 이런 식이다:
PHP Notice: Undefined offset: 5 in /var/www/html/test.php on line 10
여기서 "offset 5"는 배열의 인덱스 5번이 없다는 뜻이다. 에러 자체는 심각하지 않지만, 프로덕션 환경에서는 로그가 쌓이고 응답 속도를 떨어뜨린다.
가장 흔한 시나리오들을 봐보자. 먼저 아래는 API로부터 받은 JSON 응답을 배열로 변환한 후, 존재하지 않을 수도 있는 키에 접근하는 경우다:
✗ 잘못된 코드
$response = json_decode($jsonData, true);
echo $response['user']['email']; // 'user' 키가 없으면? → Notice 에러
echo $response['data'][0]['name']; // 'data' 배열이 비어있으면? → Notice 에러
또 다른 흔한 경우는 GET/POST 파라미터를 직접 접근할 때다:
✗ 잘못된 코드
echo $_GET['id']; // 'id' 파라미터가 없으면 에러
echo $_POST['username']; // 'username'이 비어있으면 에러
$arr = [1, 2, 3];
echo $arr[5]; // 인덱스 5가 없음 → Notice 에러
이 모든 경우의 공통점은 "미리 배열이 어떤 구조인지, 어떤 데이터를 가질지 보장하지 않는다"는 것이다.
방법 1: isset() 함수로 존재 여부 먼저 확인
✓ 올바른 코드
if (isset($response['user']['email'])) {
echo $response['user']['email'];
} else {
echo '이메일 정보 없음';
}
if (isset($_GET['id'])) {
$id = $_GET['id'];
} else {
$id = null;
}
결과: isset()는 키가 존재하고 값이 null이 아닐 때만 true를 반환한다.
Note: array_key_exists()와 다른 점은, isset()은 null 값을 false로 처리한다는 것이다.
방법 2: null coalescing operator (??) 사용
✓ 올바른 코드 (PHP 7.0+)
$email = $response['user']['email'] ?? 'guest@example.com'; // 없으면 기본값 사용
$username = $_POST['username'] ?? '';
$offset = $_GET['page'] ?? 1;
결과: ?? 왼쪽이 null이면 오른쪽 값을 반환한다.
장점: 한 줄로 깔끔하게 처리 가능, isset() 체크가 필요 없다.
방법 3: null safe operator (?->) 사용
✓ 올바른 코드 (PHP 8.0+)
$email = $response['user']?->email; // 'user'가 없으면 null 반환
$street = $user?->address?->street; // 깊은 체인도 안전
결과: null을 만나면 그 지점에서 null을 반환한다.
장점: 깊은 객체 접근이 많을 때 매우 유용하다.
방법 4: array_key_exists()로 키 존재 여부 확인
✓ 올바른 코드
if (array_key_exists('email', $response['user'])) {
echo $response['user']['email'];
}
Note: isset()과의 차이점
- isset(): 키 존재 + 값이 null이 아님 (null 값은 false)
- array_key_exists(): 키만 존재하면 true (null 값도 true)
$arr = ['name' => null];
var_dump(isset($arr['name'])); // false
var_dump(array_key_exists('name', $arr)); // true
방법 5: 헬퍼 함수 작성해서 재사용
✓ 올바른 코드
function array_get($array, $key, $default = null) {
return isset($array[$key]) ? $array[$key] : $default;
}
function array_get_nested($array, $keys, $default = null) {
foreach ($keys as $key) {
if (isset($array[$key])) {
$array = $array[$key];
} else {
return $default;
}
}
return $array;
}
// 사용
$email = array_get($user, 'email', 'unknown@example.com');
$city = array_get_nested($response, ['address', 'city'], '서울');
장점: 코드 중복 제거, 일관성 있는 처리, 재사용 가능
현실에서는 복잡한 중첩 배열을 다룬다. 예를 들어 외부 API에서 받은 JSON 응답:
✗ 잘못된 코드 (Notice 에러 발생)
$json = '{
"status": "success",
"data": {
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "email": "bob@example.com"}
]
}
}';
$response = json_decode($json, true);
// 이 중 어느 하나라도 구조가 다르면 Notice 에러 발생
echo $response['data']['users'][0]['name']; // Alice
echo $response['data']['users'][1]['name']; // Notice! 'name' 키 없음
echo $response['data']['posts'][0]['title']; // Notice! 'posts' 키 없음
✓ 올바른 방법 1: isset() 체인
if (isset($response['data']['users'][1]['name'])) {
echo $response['data']['users'][1]['name'];
} else {
echo 'Name not found';
}
장점: 명시적이고 조건부 처리 가능
단점: 길면 읽기 어렵다
✓ 올바른 방법 2: null coalescing operator 체인
$name = $response['data']['users'][1]['name'] ?? 'Unknown';
$email = $response['data']['users'][1]['email'] ?? $response['data']['users'][1]['name'] ?? 'No contact';
echo $name; // "Unknown"
echo $email; // "bob@example.com"
장점: 간결하고 기본값 지정 가능
단점: 에러인지 데이터가 없는 건지 구분 어려움
✓ 올바른 방법 3: 커스텀 헬퍼 함수
function get_array_value($array, $path, $default = null) {
$keys = explode('.', $path); // 'data.users.0.name'
foreach ($keys as $key) {
if (is_array($array) && isset($array[$key])) {
$array = $array[$key];
} else {
return $default;
}
}
return $array;
}
// 사용
$name = get_array_value($response, 'data.users.0.name', 'Unknown');
$email = get_array_value($response, 'data.users.1.email', 'no-email');
$phone = get_array_value($response, 'data.users.2.phone', 'N/A');
echo $name; // "Alice"
echo $email; // "bob@example.com"
echo $phone; // "N/A"
장점: 도트 표기법으로 경로 지정, 매우 깔끔한 코드
단점: 함수 구현 필요
| 패턴 | 문제점 | 해결책 |
|---|---|---|
@$arr['key'] (@ 연산자) | 에러는 숨기지만 로그에 쌓임, 성능 저하 | isset() / ?? 연산자 사용 |
empty($arr['key']) | '0', '0.0', false 값도 true로 처리됨 | isset() 후 별도 검증 |
| 에러 로깅 없이 무시 | 버그 원인 파악 어려움 | error_log() 또는 로그 기록 |
| 모든 접근을 isset()으로만 검증 | 코드가 복잡해짐 | 헬퍼 함수나 ?? 연산자 활용 |
PHP ini 파일에서 에러 처리를 환경별로 다르게 설정하는 것도 좋은 방법이다:
; 개발 환경
error_reporting(E_ALL); // 모든 에러 표시
display_errors = On; // 화면에 출력
; 프로덕션 환경
error_reporting(E_ALL & ~E_NOTICE); // Notice 제외
display_errors = Off; // 화면에 표시 안 함
error_log = /var/log/php-errors.log // 파일에 기록
코드에서 동적 설정
error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
| PHP 버전 | 권장 방법 | 예제 |
|---|---|---|
| PHP 5.x ~ 7.x | isset() / array_key_exists() | $val = isset($arr['key']) ? $arr['key'] : default |
| PHP 7.0+ | null coalescing (??) | $val = $arr['key'] ?? default |
| PHP 7.4+ | null coalescing + null safe | $val = $obj?->prop['key'] ?? default |
| PHP 8.0+ | null safe operator (?->) | $val = $obj?->nested?->prop |
"Undefined offset" 에러는 사소해 보이지만, 실제로는 코드의 안전성과 유지보수성을 크게 좌우한다. @ 연산자로 무시하는 것은 나쁜 습관이고, 매번 isset()을 붙이는 것도 코드를 지저분하게 만든다. PHP 7.0 이상을 사용한다면 null coalescing operator (??)를 기본으로, PHP 8.0 이상을 사용한다면 null safe operator (?->)를 활용해서 훨씬 깔끔한 코드를 쓸 수 있다. 이 글의 "5단계 헬퍼 함수" 부분을 참고해 자신의 프로젝트에 맞는 유틸리티 함수를 만들면, 반복되는 배열 접근 코드를 획기적으로 줄일 수 있을 것이다.