배열에서 요소를 제거해야 할 때 대부분의 개발자는 unset()을 먼저 떠올린다. 하지만 unset()은 배열 키를 그대로 남겨두기 때문에 예상과 다른 결과가 나올 수 있다. 특히 큐(Queue)나 스택(Stack) 같은 자료구조를 구현할 때는 array_pop()과 array_shift()를 알아야 한다. 이번에는 이 두 함수가 정확히 뭔지, 언제 써야 하는지, 그리고 언제 함수가 문제를 일으키는지 완벽하게 정리해서 소개하겠다.

 

array_pop()과 array_shift() 기본 개념

array_pop()은 배열의 마지막 요소를 제거하고 그 값을 반환한다. array_shift()는 배열의 첫 번째 요소를 제거하고 반환한다. 둘 다 원본 배열을 직접 수정한다는 점이 중요하다.

간단한 예시를 보자.

$fruits = ['apple', 'banana', 'cherry', 'date'];

// array_pop() 사용
$last = array_pop($fruits);
echo $last; // 출력: date
print_r($fruits);
// 출력: Array ( [0] => apple [1] => banana [2] => cherry )

// array_shift() 사용
$fruits = ['apple', 'banana', 'cherry', 'date'];
$first = array_shift($fruits);
echo $first; // 출력: apple
print_r($fruits);
// 출력: Array ( [0] => banana [1] => cherry [2] => date )

보이는가? array_pop()은 요소를 제거할 뿐 나머지 인덱스는 변하지 않는다. 하지만 array_shift()는 첫 번째 요소를 제거한 후 나머지 요소들의 인덱스를 자동으로 리셋한다. 이것이 두 함수의 가장 큰 차이다.

 

차이점 깊게 이해하기: 인덱스 리셋 문제

array_shift()가 인덱스를 리셋하는 이유는 PHP의 배열 특성 때문이다. 연관 배열(문자열 키를 가진 배열)에서는 shift()가 동작하지만 인덱스는 리셋되지 않는다.

// 숫자 키 배열에서 array_shift()
$items = [0 => 'first', 1 => 'second', 2 => 'third'];
array_shift($items);
print_r($items);
// 출력: Array ( [0] => second [1] => third )
// 인덱스가 자동으로 리셋됨

// 연관 배열에서 array_shift()
$user = ['name' => 'John', 'age' => 30, 'city' => 'Seoul'];
array_shift($user);
print_r($user);
// 출력: Array ( [age] => 30 [city] => Seoul )
// 키는 변하지 않음

반면 array_pop()은 숫자 키든 연관 배열이든 인덱스를 절대 리셋하지 않는다.

 

실전 예제 1: 스택(Stack) 구현

스택은 LIFO(Last In First Out) 자료구조다. 마지막에 들어온 요소를 가장 먼저 꺼낸다. array_pop()이 정확히 이런 동작을 한다.

class Stack {
    private $items = [];
    
    public function push($item) {
        $this->items[] = $item;
    }
    
    public function pop() {
        if (empty($this->items)) {
            return null;
        }
        return array_pop($this->items);
    }
    
    public function peek() {
        return end($this->items) ?: null;
    }
}

$stack = new Stack();
$stack->push('first');
$stack->push('second');
$stack->push('third');

echo $stack->pop(); // 출력: third
echo $stack->pop(); // 출력: second
echo $stack->peek(); // 출력: first

이렇게 구현하면 스택이 완벽하게 동작한다. array_pop()은 마지막 요소를 빠르게 제거하므로 성능도 좋다.

 

실전 예제 2: 큐(Queue) 구현

큐는 FIFO(First In First Out) 자료구조다. 가장 먼저 들어온 요소를 가장 먼저 꺼낸다. array_shift()가 정확히 이 역할을 한다.

class Queue {
    private $items = [];
    
    public function enqueue($item) {
        $this->items[] = $item;
    }
    
    public function dequeue() {
        if (empty($this->items)) {
            return null;
        }
        return array_shift($this->items);
    }
    
    public function front() {
        return $this->items[0] ?? null;
    }
}

$queue = new Queue();
$queue->enqueue('customer1');
$queue->enqueue('customer2');
$queue->enqueue('customer3');

echo $queue->dequeue(); // 출력: customer1
echo $queue->dequeue(); // 출력: customer2
echo $queue->front(); // 출력: customer3

큐는 주문 처리, 작업 스케줄링, 채팅 메시지 순서 보장 등 실무에서 자주 쓰인다. 다만 array_shift()는 배열의 모든 요소를 리인덱싱해야 하므로 대용량 데이터에서는 성능이 떨어진다.

 

흔한 실수와 성능 문제

✗ 잘못된 코드: 루프에서 array_shift() 반복 호출

$tasks = range(1, 10000);
$start = microtime(true);

while (!empty($tasks)) {
    $task = array_shift($tasks);
    // 처리 로직
}

$elapsed = microtime(true) - $start;
echo "시간: {$elapsed}초"; // 약 1초 이상 소요

왜 느린가? array_shift()를 호출할 때마다 남은 모든 요소의 인덱스를 다시 정렬한다. 10000개 요소에서 이 작업이 10000번 반복되면 O(n²) 복잡도가 된다.

✓ 올바른 코드: foreach 루프 사용

$tasks = range(1, 10000);
$start = microtime(true);

foreach ($tasks as $task) {
    // 처리 로직
}

$elapsed = microtime(true) - $start;
echo "시간: {$elapsed}초"; // 0.001초 미만

foreach는 배열을 그대로 순회하므로 훨씬 빠르다. 만약 처리 후 요소를 정말 제거해야 한다면 array_splice()를 고려하자.

 

array_pop()과 array_shift() vs unset()

array_pop()과 array_shift()는 unset()과 비슷하지만 중요한 차이가 있다.

함수 반환값 인덱스 리셋 용도
array_pop() 제거된 요소값 끝에서만 안전 스택, 마지막 요소 제거
array_shift() 제거된 요소값 숫자 키만 자동 리셋 큐, 첫 요소 제거
unset() 없음(void) 안 함 특정 키 제거, 메모리 정리
array_splice() 제거된 요소 배열 항상 리셋 중간 요소 제거, 삽입

예를 들어 배열 중간에서 요소를 제거하면서 인덱스도 자동으로 리셋하려면 array_splice()를 써야 한다.

$items = ['a', 'b', 'c', 'd', 'e'];

// unset 사용: 인덱스 안 리셋
unset($items[2]);
print_r($items);
// 출력: Array ( [0] => a [1] => b [3] => d [4] => e )

// array_splice 사용: 인덱스 자동 리셋
$items = ['a', 'b', 'c', 'd', 'e'];
array_splice($items, 2, 1);
print_r($items);
// 출력: Array ( [0] => a [1] => b [2] => d [3] => e )

 

빈 배열에서 함수 호출할 때

array_pop()과 array_shift()를 빈 배열에서 호출하면 NULL을 반환한다. 이것을 false로 착각하면 버그가 난다.

✗ 잘못된 코드:

$items = [];
if (array_pop($items)) {
    echo "요소 있음";
} else {
    echo "요소 없음";
}
// 출력: 요소 없음 (맞게 나왔지만...)

$items = [0]; // 정수 0도 배열에 들어갈 수 있다
if (array_pop($items)) { // 0을 팝했는데 0은 falsy!
    echo "요소 있음";
} else {
    echo "요소 없음"; // 잘못된 판단!
}
// 출력: 요소 없음

✓ 올바른 코드:

$items = [0];
if (!empty($items)) {
    $value = array_pop($items);
    echo "제거된 값: {$value}";
} else {
    echo "배열이 비어있음";
}
// 또는
if (count($items) > 0) {
    $value = array_pop($items);
    echo "제거된 값: {$value}";
}

 

정리와 실무 팁

array_pop()과 array_shift()는 배열의 끝과 시작에서 요소를 제거하는 가장 직관적인 방법이다. 스택과 큐 같은 자료구조를 구현할 때 필수적이다. 하지만 대용량 반복에서는 성능이 떨어질 수 있으므로 상황에 따라 foreach나 array_splice() 같은 대안을 고려해야 한다. 특히 array_shift()를 루프 안에서 반복 호출하는 패턴은 피하고, 필요하면 인덱스를 직접 관리하거나 SplQueue 같은 전문 자료구조를 사용하자. 이런 작은 최적화가 모여서 응답 시간 수 초를 아낄 수 있다는 점을 잊지 말자. 이 글의 스택과 큐 구현 예제를 참고해 자신의 프로젝트에 맞는 자료구조를 설계하면, 데이터 처리의 논리적 명확성과 성능 모두를 얻을 수 있을 것이다.