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 감소
- 나머지 요소들을 왼쪽으로 이동 (인덱스 재조정)
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); // [] - 빈 배열
패턴 설명:
shift()로 첫 요소를 꺼내서i에 할당i가undefined가 아닌 동안 반복- 배열이 비면
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()는 배열의 첫 번째 요소를 제거하고 반환하는 간단하지만 강력한 메서드입니다.
핵심 요약
- 목적: 배열의 첫 요소 제거 + 반환
- 원본 변경: ✅ 원본 배열 변경됨
- 반환값: 제거된 요소 (빈 배열이면
undefined) - 시간 복잡도: O(n) - 큰 배열에서 주의
- 주요 용도: 큐(Queue) 구현, FIFO 패턴
실무 체크리스트
- 원본 배열 변경이 의도한 동작인지 확인
- 빈 배열 처리 (
undefined체크) - 큰 배열(수천 개 이상)이면 대안 고려
- 큐 구현 시
shift()+push()조합 - 성능 중요하면 인덱스 추적 방식 고려
- 문자열은 배열로 변환 후 사용
메서드 선택 가이드
// 첫 요소 제거하며 반환
arr.shift();
// 첫 요소만 확인 (제거 안 함)
arr[0];
// 첫 요소 제외한 새 배열 (원본 유지)
arr.slice(1);
// 마지막 요소 제거하며 반환 (빠름!)
arr.pop();
마지막 조언
배열을 줄 서 있는 사람들로 생각하세요.
shift()는 맨 앞 사람이 빠져나가는 것이고,push()는 맨 뒤에 새로운 사람이 들어오는 것입니다.
shift()는 순서가 중요한 작업에 완벽합니다. 작업 큐, 메시지 처리, 순차 애니메이션 등 “먼저 들어온 것을 먼저 처리”해야 하는 모든 상황에서 빛을 발합니다.
다만, 큰 배열에서 반복 사용 시에는 성능을 고려해야 합니다. 상황에 맞는 도구를 선택하는 것이 현명한 개발자의 자세입니다!
댓글