shift(): 배열의 첫 번째 요소를 꺼내는 법

배열의 첫 번째 요소를 가져와서 처리하고 싶은데, 그 요소는 배열에서 제거하고 싶었던 적 있으신가요?

const tasks = ["이메일 확인", "회의 참석", "보고서 작성"];
// 첫 번째 작업을 가져오고, 남은 작업 목록도 업데이트하고 싶어요

“첫 요소를 가져오는 건 tasks[0]으로 할 수 있는데, 배열에서는 어떻게 제거하지?”, “제거하면서 동시에 그 값을 받을 수는 없나?” 같은 고민을 하게 됩니다.

shift()는 바로 이 문제를 해결합니다. MDN 문서에 따르면, “배열의 첫 번째 요소를 제거하고 그 요소를 반환”하는 메서드입니다. 마치 줄 서 있는 사람들 중 맨 앞 사람이 빠져나가는 것과 같죠.

왜 shift()가 필요한가?

배열의 첫 요소를 다루는 방법은 여러 가지가 있습니다. 하지만 각각 다른 목적을 가지고 있죠.

실제 사용 사례

사례 1: 작업 큐(Queue) 처리

const taskQueue = ["작업1", "작업2", "작업3"];

// 작업을 순서대로 처리하면서 큐에서 제거하고 싶어요
function processNextTask() {
  const task = taskQueue.shift(); // 첫 작업 꺼내기
  console.log(`처리 중: ${task}`);
  console.log(`남은 작업:`, taskQueue);
}

processNextTask(); // "처리 중: 작업1", 남은: ["작업2", "작업3"]
processNextTask(); // "처리 중: 작업2", 남은: ["작업3"]

왜 필요할까?:

  • ✅ 처리한 작업은 큐에서 제거
  • ✅ 다음에 처리할 작업이 명확
  • ✅ 큐가 비었는지 쉽게 확인

사례 2: 순차 데이터 소비

const messages = ["안녕하세요", "반갑습니다", "잘 부탁드립니다"];

// 메시지를 하나씩 표시하면서 목록에서 제거
while (messages.length > 0) {
  const message = messages.shift();
  displayMessage(message);
}
// messages는 이제 빈 배열 []

사례 3: FIFO(First In, First Out) 패턴

생각해보면, 실생활에서 FIFO 패턴은 흔합니다:

  • 은행 창구 대기열: 먼저 온 사람이 먼저 서비스 받음
  • 프린터 인쇄 대기: 먼저 요청한 문서가 먼저 인쇄됨
  • 고객 지원 티켓: 먼저 접수된 요청이 먼저 처리됨

shift()는 이런 FIFO 패턴을 코드로 구현할 때 핵심 도구입니다.

먼저, 기초부터 이해하기

shift()가 어떻게 작동하는지 이해하려면, 배열에서 요소를 제거하는 다양한 방법을 알아야 합니다.

배열에서 요소 제거하기

JavaScript에서 배열 요소를 제거하는 방법들:

const arr = [1, 2, 3, 4, 5];

// 방법 1: delete 연산자 (비추천!)
delete arr[0]; // [empty, 2, 3, 4, 5] - 길이는 그대로!

// 방법 2: splice (복잡함)
arr.splice(0, 1); // [2, 3, 4, 5] - 작동하지만 복잡

// 방법 3: slice (새 배열 생성)
const newArr = arr.slice(1); // [2, 3, 4, 5] - 원본 유지

// 방법 4: shift (간단하고 직관적!)
const first = arr.shift(); // first = 1, arr = [2, 3, 4, 5]

shift()의 기본 동작

MDN 문서에 따르면, shift()는 다음과 같이 작동합니다:

  1. 첫 번째 요소를 제거
  2. 제거된 요소를 반환
  3. 배열의 길이를 1 감소
  4. 나머지 요소들을 왼쪽으로 이동 (인덱스 재조정)
const numbers = [10, 20, 30];
const removed = numbers.shift();

console.log(removed);  // 10
console.log(numbers);  // [20, 30]
console.log(numbers.length); // 2

핵심: MDN 명시대로, “이것은 변경하는 메서드(mutating method)입니다. 길이와 내용을 변경”시킵니다.

빈 배열에서는?

const empty = [];
const result = empty.shift();

console.log(result); // undefined
console.log(empty);  // []

MDN 문서: “길이가 0이면 undefined를 반환합니다.”

기본 문법

const removedElement = array.shift();

특징:

  • 매개변수: 없음
  • 반환값: 제거된 첫 번째 요소 (배열이 비어있으면 undefined)
  • 부수 효과: 원본 배열 변경

예제: 단계별 이해

const fruits = ["사과", "바나나", "오렌지", "포도"];

console.log("1단계:", fruits); // ["사과", "바나나", "오렌지", "포도"]

const first = fruits.shift();
console.log("2단계 - 제거된 요소:", first); // "사과"
console.log("2단계 - 배열:", fruits); // ["바나나", "오렌지", "포도"]

const second = fruits.shift();
console.log("3단계 - 제거된 요소:", second); // "바나나"
console.log("3단계 - 배열:", fruits); // ["오렌지", "포도"]

다른 메서드와의 비교

배열의 양 끝을 다루는 메서드 4형제를 이해하면 언제 무엇을 써야 할지 명확해집니다.

4가지 메서드 비교

메서드 작용 위치 작업 반환값 원본 변경
shift() 제거 제거된 요소
unshift() 추가 새 길이
pop() 제거 제거된 요소
push() 추가 새 길이

시각적 비교

const arr = [1, 2, 3];

// shift(): 앞에서 제거
arr.shift(); // [2, 3] 반환값: 1

// unshift(): 앞에 추가
arr.unshift(0); // [0, 2, 3] 반환값: 3 (새 길이)

// pop(): 뒤에서 제거
arr.pop(); // [0, 2] 반환값: 3

// push(): 뒤에 추가
arr.push(4); // [0, 2, 4] 반환값: 3 (새 길이)

❌ shift() vs ✅ pop()

const tasks = ["A", "B", "C"];

// shift() - 시간 복잡도: O(n)
const first = tasks.shift(); // 모든 요소를 왼쪽으로 이동!
// ["B", "C"] - B가 인덱스 0으로, C가 인덱스 1로

// pop() - 시간 복잡도: O(1)
const last = tasks.pop(); // 마지막 요소만 제거
// ["A", "B"] - 다른 요소들은 그대로

성능 차이: shift()는 나머지 요소들을 모두 이동시켜야 하므로 큰 배열에서는 느릴 수 있습니다.

shift() vs slice()

const original = [1, 2, 3];

// shift() - 원본 변경
const removed = original.shift();
console.log(original); // [2, 3] - 변경됨!
console.log(removed);  // 1

// slice() - 원본 유지
const newArr = original.slice(1);
console.log(original); // [1, 2, 3] - 유지됨
console.log(newArr);   // [2, 3]

MDN 문서: “원본을 변경하지 않으려면 arr.slice(1)을 사용하세요.”

실전 활용 예제

실무에서 자주 사용되는 패턴들을 살펴봅시다.

1. 큐(Queue) 구현

FIFO 자료구조의 기본입니다:

class Queue {
  constructor() {
    this.items = [];
  }

  // 큐에 추가 (뒤에)
  enqueue(element) {
    this.items.push(element);
  }

  // 큐에서 제거 (앞에서)
  dequeue() {
    return this.items.shift();
  }

  // 다음 요소 확인 (제거 안 함)
  peek() {
    return this.items[0];
  }

  // 큐가 비었는지 확인
  isEmpty() {
    return this.items.length === 0;
  }

  // 큐 크기
  size() {
    return this.items.length;
  }
}

// 사용 예시
const queue = new Queue();
queue.enqueue("고객1");
queue.enqueue("고객2");
queue.enqueue("고객3");

console.log(queue.dequeue()); // "고객1"
console.log(queue.dequeue()); // "고객2"
console.log(queue.size());    // 1

2. While 루프와 함께 사용

MDN 문서의 예제를 실전에 적용:

const names = ["Andrew", "Tyrone", "Paul"];
let i;

while (typeof (i = names.shift()) !== "undefined") {
  console.log(i);
}
// "Andrew"
// "Tyrone"
// "Paul"

console.log(names); // [] - 빈 배열

패턴 설명:

  1. shift()로 첫 요소를 꺼내서 i에 할당
  2. iundefined가 아닌 동안 반복
  3. 배열이 비면 shift()undefined를 반환하며 종료

3. 배치 처리

일정 개수씩 나눠서 처리:

function processBatch(items, batchSize) {
  const results = [];

  while (items.length > 0) {
    const batch = [];

    // batchSize만큼 또는 남은 개수만큼
    for (let i = 0; i < batchSize && items.length > 0; i++) {
      batch.push(items.shift());
    }

    // 배치 처리
    const processed = batch.map(item => item.toUpperCase());
    results.push(processed);
  }

  return results;
}

const tasks = ["a", "b", "c", "d", "e", "f", "g"];
const batches = processBatch(tasks, 3);

console.log(batches);
// [["A", "B", "C"], ["D", "E", "F"], ["G"]]
console.log(tasks); // [] - 원본은 비었음

4. 순차 애니메이션

const animations = [
  { element: ".box1", duration: 300 },
  { element: ".box2", duration: 500 },
  { element: ".box3", duration: 200 }
];

function playNextAnimation() {
  if (animations.length === 0) {
    console.log("모든 애니메이션 완료!");
    return;
  }

  const anim = animations.shift();

  // 애니메이션 실행
  animate(anim.element, anim.duration);

  // 완료 후 다음 애니메이션
  setTimeout(playNextAnimation, anim.duration);
}

playNextAnimation();

5. 브레드크럼(Breadcrumb) 네비게이션

class BreadcrumbNav {
  constructor(maxItems = 3) {
    this.items = [];
    this.maxItems = maxItems;
  }

  addPage(page) {
    this.items.push(page);

    // 최대 개수 초과 시 가장 오래된 것 제거
    if (this.items.length > this.maxItems) {
      this.items.shift(); // 첫 번째 제거
    }
  }

  getPath() {
    return this.items.join(" > ");
  }
}

const nav = new BreadcrumbNav(3);
nav.addPage("");
nav.addPage("카테고리");
nav.addPage("상품목록");
console.log(nav.getPath()); // "홈 > 카테고리 > 상품목록"

nav.addPage("상품상세");
console.log(nav.getPath()); // "카테고리 > 상품목록 > 상품상세"
// "홈"이 자동으로 제거됨!

함정과 주의사항

실제로 사용하면서 주의해야 할 점들입니다.

함정 1: 원본 배열 변경

const original = [1, 2, 3, 4, 5];

function processFirst(arr) {
  const first = arr.shift(); // 원본 변경!
  return first * 2;
}

const result = processFirst(original);
console.log(result);   // 2
console.log(original); // [2, 3, 4, 5] - 어? 변했네!

해결책:

// ❌ 나쁜 예: 원본 변경
function processFirst(arr) {
  return arr.shift() * 2;
}

// ✅ 좋은 예: 복사본 사용
function processFirst(arr) {
  const copy = [...arr]; // 또는 arr.slice()
  return copy.shift() * 2;
}

// ✅ 또는 slice() 사용 (원본 유지)
function processFirst(arr) {
  return arr[0] * 2; // 제거 없이 접근만
}

함정 2: 성능 문제

// ❌ 큰 배열에서 반복 shift() - 느림! O(n²)
const bigArray = Array.from({ length: 10000 }, (_, i) => i);

console.time("shift");
while (bigArray.length > 0) {
  bigArray.shift(); // 매번 모든 요소 이동!
}
console.timeEnd("shift"); // 상당히 느림

해결책:

// ✅ pop() 사용 - 빠름! O(n)
const bigArray = Array.from({ length: 10000 }, (_, i) => i);

console.time("pop");
while (bigArray.length > 0) {
  bigArray.pop(); // 마지막 요소만 제거
}
console.timeEnd("pop"); // 훨씬 빠름

// ✅ 또는 인덱스 사용
const bigArray = Array.from({ length: 10000 }, (_, i) => i);
let index = 0;

console.time("index");
while (index < bigArray.length) {
  const item = bigArray[index++]; // 제거 없이 순회
  // 처리...
}
console.timeEnd("index"); // 가장 빠름

함정 3: 빈 배열 체크 누락

// ❌ undefined 처리 누락
function processQueue(queue) {
  const item = queue.shift();
  return item.toUpperCase(); // item이 undefined면 에러!
}

const emptyQueue = [];
processQueue(emptyQueue); // TypeError: Cannot read property 'toUpperCase' of undefined

해결책:

// ✅ 명시적 체크
function processQueue(queue) {
  if (queue.length === 0) {
    return null; // 또는 적절한 기본값
  }

  const item = queue.shift();
  return item.toUpperCase();
}

// ✅ 또는 옵셔널 체이닝
function processQueue(queue) {
  const item = queue.shift();
  return item?.toUpperCase() ?? "빈 큐";
}

// ✅ 또는 while 패턴
function processQueue(queue) {
  let item;
  while ((item = queue.shift()) !== undefined) {
    console.log(item.toUpperCase());
  }
}

함정 4: 문자열에 사용 불가

MDN 명시: “문자열은 불변이므로 이 메서드에 적합하지 않습니다”

const str = "hello";

// ❌ 문자열에는 shift()가 없음
// str.shift(); // TypeError: str.shift is not a function

// ✅ 배열로 변환 후 사용
const chars = str.split('');
const first = chars.shift(); // 'h'
const result = chars.join(''); // "ello"

함정 5: 유사 배열 객체 사용 시

// arguments는 유사 배열 객체
function test() {
  // ❌ 직접 사용 불가
  // arguments.shift(); // TypeError

  // ✅ Array.prototype.shift.call 사용
  const first = Array.prototype.shift.call(arguments);
  console.log(first);
  console.log(arguments); // 나머지 인수들
}

test(1, 2, 3); // 1, [2, 3]

// ✅ 또는 Array.from으로 변환
function test2() {
  const args = Array.from(arguments);
  const first = args.shift();
  console.log(first);
}

MDN 명시: “길이 속성과 정수 키 속성만 필요”하므로 유사 배열 객체에 call()로 적용 가능합니다.

성능 고려사항

시간 복잡도

const arr = [1, 2, 3, 4, 5];

// shift() - O(n)
arr.shift();
// 내부적으로:
// arr[0] = arr[1]  // 2를 인덱스 0으로
// arr[1] = arr[2]  // 3을 인덱스 1로
// arr[2] = arr[3]  // 4를 인덱스 2로
// arr[3] = arr[4]  // 5를 인덱스 3로
// length = 4

// pop() - O(1)
arr.pop();
// 내부적으로:
// length = 3
// (마지막 요소만 제거, 이동 없음)

대안: 인덱스 추적

큰 배열에서 성능이 중요하다면:

class EfficientQueue {
  constructor() {
    this.items = [];
    this.head = 0; // 시작 인덱스
  }

  enqueue(item) {
    this.items.push(item);
  }

  // shift() 대신 인덱스 이동
  dequeue() {
    if (this.head >= this.items.length) {
      return undefined;
    }

    const item = this.items[this.head];
    this.head++; // O(1)!

    // 주기적으로 정리 (선택사항)
    if (this.head > 100 && this.head > this.items.length / 2) {
      this.items = this.items.slice(this.head);
      this.head = 0;
    }

    return item;
  }

  size() {
    return this.items.length - this.head;
  }
}

정리하며

shift()는 배열의 첫 번째 요소를 제거하고 반환하는 간단하지만 강력한 메서드입니다.

핵심 요약

  1. 목적: 배열의 첫 요소 제거 + 반환
  2. 원본 변경: ✅ 원본 배열 변경됨
  3. 반환값: 제거된 요소 (빈 배열이면 undefined)
  4. 시간 복잡도: O(n) - 큰 배열에서 주의
  5. 주요 용도: 큐(Queue) 구현, FIFO 패턴

실무 체크리스트

  • 원본 배열 변경이 의도한 동작인지 확인
  • 빈 배열 처리 (undefined 체크)
  • 큰 배열(수천 개 이상)이면 대안 고려
  • 큐 구현 시 shift() + push() 조합
  • 성능 중요하면 인덱스 추적 방식 고려
  • 문자열은 배열로 변환 후 사용

메서드 선택 가이드

// 첫 요소 제거하며 반환
arr.shift();

// 첫 요소만 확인 (제거 안 함)
arr[0];

// 첫 요소 제외한 새 배열 (원본 유지)
arr.slice(1);

// 마지막 요소 제거하며 반환 (빠름!)
arr.pop();

마지막 조언

배열을 줄 서 있는 사람들로 생각하세요. shift()는 맨 앞 사람이 빠져나가는 것이고, push()는 맨 뒤에 새로운 사람이 들어오는 것입니다.

shift()순서가 중요한 작업에 완벽합니다. 작업 큐, 메시지 처리, 순차 애니메이션 등 “먼저 들어온 것을 먼저 처리”해야 하는 모든 상황에서 빛을 발합니다.

다만, 큰 배열에서 반복 사용 시에는 성능을 고려해야 합니다. 상황에 맞는 도구를 선택하는 것이 현명한 개발자의 자세입니다!

참고 자료

댓글