배열에서 존재하지 않는 인덱스에 접근하면 화면에 빨간 경고가 떴던 경험 있을까? 대부분의 개발자들은 이 에러를 무시하거나, 인터넷에서 @(에러 억제 연산자)를 붙여서 대충 넘어간다. 하지만 이건 근본적인 해결책이 아니다. 이번에는 PHP의 가장 흔한 에러 중 하나인 "Undefined offset" 에러가 정확히 뭔지, 왜 발생하는지, 그리고 어떻게 안전하게 처리하는지 완벽하게 정리해서 소개하겠다.

 

1단계: Undefined offset 에러란?

"Undefined offset"은 배열에 존재하지 않는 인덱스(또는 키)에 접근하려고 할 때 발생하는 Notice 레벨 에러다. 에러 메시지는 보통 이런 식이다:

PHP Notice: Undefined offset: 5 in /var/www/html/test.php on line 10

여기서 "offset 5"는 배열의 인덱스 5번이 없다는 뜻이다. 에러 자체는 심각하지 않지만, 프로덕션 환경에서는 로그가 쌓이고 응답 속도를 떨어뜨린다.

 

2단계: 왜 이 에러가 발생할까?

가장 흔한 시나리오들을 봐보자. 먼저 아래는 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 에러

이 모든 경우의 공통점은 "미리 배열이 어떤 구조인지, 어떤 데이터를 가질지 보장하지 않는다"는 것이다.

 

3단계: 안전한 접근 방법들

방법 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'], '서울');

장점: 코드 중복 제거, 일관성 있는 처리, 재사용 가능

 

4단계: 실전 예제 - API 응답 안전하게 처리하기

현실에서는 복잡한 중첩 배열을 다룬다. 예를 들어 외부 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"

장점: 도트 표기법으로 경로 지정, 매우 깔끔한 코드
단점: 함수 구현 필요

 

5단계: 주의사항 - 피해야 할 패턴들
패턴문제점해결책
@$arr['key'] (@ 연산자)에러는 숨기지만 로그에 쌓임, 성능 저하isset() / ?? 연산자 사용
empty($arr['key'])'0', '0.0', false 값도 true로 처리됨isset() 후 별도 검증
에러 로깅 없이 무시버그 원인 파악 어려움error_log() 또는 로그 기록
모든 접근을 isset()으로만 검증코드가 복잡해짐헬퍼 함수나 ?? 연산자 활용

 

6단계: 개발/프로덕션 환경 설정

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);

 

7단계: 최신 PHP 버전별 권장 방법
PHP 버전권장 방법예제
PHP 5.x ~ 7.xisset() / 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단계 헬퍼 함수" 부분을 참고해 자신의 프로젝트에 맞는 유틸리티 함수를 만들면, 반복되는 배열 접근 코드를 획기적으로 줄일 수 있을 것이다.