유사 배열 객체 - 배열인 듯 배열 아닌 배열 같은 너
JavaScript를 사용하다가 이런 경험을 해보신 적 있나요?
const divs = document.querySelectorAll('div');
console.log(divs); // NodeList(3) [div, div, div]
// "오, 배열이네? 그럼 map을 써볼까?"
divs.map(div => div.textContent); // ❌ TypeError: divs.map is not a function
“어? 분명히 배열처럼 생겼는데, 왜 map이 안 되지?” 이런 당황스러운 순간을 마주한 적이 있다면, 여러분은 유사 배열 객체(Array-like Object)와 조우한 것입니다.
처음 JavaScript를 배울 때는 “대괄호로 감싸져 있고, 인덱스로 접근할 수 있으면 배열 아닌가?”라고 생각했습니다. 하지만 실무에서 DOM 조작을 하면서 NodeList나 arguments 객체를 다루다 보니, JavaScript에는 “배열처럼 보이지만 배열이 아닌” 특별한 객체들이 있다는 것을 알게 되었습니다.
이 문서에서는 유사 배열 객체가 무엇인지, 왜 존재하는지, 그리고 실전에서 어떻게 다뤄야 하는지를 자세히 설명하겠습니다.
목차
- 왜 유사 배열 객체를 이해해야 할까요?
- 먼저, 문제 상황을 보면서 시작해볼까요?
- 유사 배열 객체란 무엇인가?
- JavaScript의 대표적인 유사 배열 객체들
- 유사 배열을 진짜 배열로 변환하기
- 실전에서 유사 배열 다루기
- 유사 배열의 함정과 주의사항
- 유사 배열 객체를 직접 만들기
- 성능 고려사항
- 결론: 유사 배열을 언제 어떻게 다룰까?
- 참고 자료
왜 유사 배열 객체를 이해해야 할까요?
1. DOM 조작에서 자주 만납니다
웹 개발을 하다 보면 유사 배열 객체를 피할 수 없습니다.
// 이 모든 것들이 유사 배열 객체입니다
const divs = document.querySelectorAll('div'); // NodeList
const buttons = document.getElementsByTagName('button'); // HTMLCollection
const form = document.querySelector('form');
const inputs = form.elements; // HTMLFormControlsCollection
// "배열처럼 보이지만" 배열 메소드가 없습니다
console.log(Array.isArray(divs)); // false
console.log(Array.isArray(buttons)); // false
유사 배열을 이해하지 못하면, DOM 조작 코드에서 계속 에러를 만나게 됩니다.
2. 함수의 인자를 다룰 때 필요합니다
ES6 이전에는 함수의 모든 인자를 다루기 위해 arguments 객체를 사용했습니다.
function sum() {
console.log(arguments); // Arguments(3) [1, 2, 3]
console.log(Array.isArray(arguments)); // false
// ❌ 배열 메소드가 없습니다
// return arguments.reduce((a, b) => a + b); // TypeError!
// ✅ 배열로 변환해야 합니다
return Array.from(arguments).reduce((a, b) => a + b);
}
sum(1, 2, 3); // 6
레거시 코드를 읽거나 유지보수할 때, arguments를 이해하는 것은 필수입니다.
3. 라이브러리 코드를 이해하는 데 도움이 됩니다
많은 라이브러리들이 유사 배열 객체를 다룹니다.
// jQuery는 유사 배열 객체를 반환합니다
const $divs = $('div'); // jQuery 객체 (유사 배열)
console.log($divs.length); // 3
console.log($divs[0]); // 첫 번째 div 요소
console.log(Array.isArray($divs)); // false
// 하지만 jQuery의 자체 메소드는 사용 가능
$divs.each(function() { /* ... */ });
먼저, 문제 상황을 보면서 시작해볼까요?
시나리오: 모든 버튼에 이벤트 리스너 추가하기
페이지의 모든 버튼에 클릭 이벤트를 추가하는 간단한 작업을 해봅시다.
접근 1: 배열 메소드를 바로 사용하기 (실패)
const buttons = document.querySelectorAll('button');
// ❌ 에러 발생!
buttons.map(button => {
button.addEventListener('click', () => {
console.log('클릭!');
});
return button;
});
// TypeError: buttons.map is not a function
“어? 분명히 배열처럼 생겼는데?” 하지만 buttons는 NodeList이고, 배열 메소드(map, filter, reduce 등)가 없습니다.
접근 2: for 루프 사용하기 (작동하지만…)
const buttons = document.querySelectorAll('button');
// ✅ 작동합니다
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', () => {
console.log('클릭!');
});
}
작동은 하지만, 코드가 장황하고 함수형 프로그래밍 스타일을 사용할 수 없습니다.
접근 3: 배열로 변환하기 (권장)
const buttons = document.querySelectorAll('button');
// ✅ 배열로 변환
const buttonArray = Array.from(buttons);
// 이제 모든 배열 메소드 사용 가능!
for (const button of buttonArray) {
button.addEventListener('click', () => {
console.log('클릭!');
});
}
// 또는 더 간결하게
for (const button of Array.from(buttons)) {
button.addEventListener('click', () => {
console.log('클릭!');
});
}
이제 모든 배열 메소드(map, filter, reduce 등)를 자유롭게 사용할 수 있습니다!
유사 배열 객체란 무엇인가?
기본 개념
유사 배열 객체(Array-like Object)는 배열처럼 보이고 일부 배열처럼 동작하지만, 실제로는 배열이 아닌 객체입니다.
유사 배열 객체의 조건
객체가 유사 배열로 간주되려면 다음 조건을 만족해야 합니다:
length속성을 가지고 있어야 합니다- 인덱스로 접근 가능해야 합니다 (0, 1, 2, …)
// ✅ 유사 배열 객체
const arrayLike = {
0: 'first',
1: 'second',
2: 'third',
length: 3
};
console.log(arrayLike[0]); // 'first'
console.log(arrayLike.length); // 3
console.log(Array.isArray(arrayLike)); // false
// ❌ 배열 메소드가 없습니다
// arrayLike.map(x => x); // TypeError!
배열 vs 유사 배열 비교
// 진짜 배열
const realArray = ['a', 'b', 'c'];
// 유사 배열
const arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};
// 공통점: 인덱스 접근, length
console.log(realArray[0]); // 'a'
console.log(arrayLike[0]); // 'a'
console.log(realArray.length); // 3
console.log(arrayLike.length); // 3
// 차이점: Prototype
console.log(realArray.__proto__ === Array.prototype); // true
console.log(arrayLike.__proto__ === Array.prototype); // false
console.log(arrayLike.__proto__ === Object.prototype); // true
// 차이점: 배열 메소드 존재 여부
console.log(typeof realArray.map); // 'function' ✅
console.log(typeof arrayLike.map); // 'undefined' ❌
// 차이점: Array.isArray()
console.log(Array.isArray(realArray)); // true
console.log(Array.isArray(arrayLike)); // false
시각화:
진짜 배열:
realArray
├─ 0: 'a'
├─ 1: 'b'
├─ 2: 'c'
├─ length: 3
└─ __proto__: Array.prototype
├─ map()
├─ filter()
├─ reduce()
└─ ...
유사 배열:
arrayLike
├─ 0: 'a'
├─ 1: 'b'
├─ 2: 'c'
├─ length: 3
└─ __proto__: Object.prototype
├─ toString()
├─ hasOwnProperty()
└─ ... (배열 메소드 없음!)
왜 유사 배열이 존재할까?
역사적인 이유와 성능상의 이유가 있습니다.
-
역사적 이유: JavaScript 초기에는 배열이 지금처럼 강력하지 않았습니다.
arguments같은 특별한 객체들은 배열이 아니면서도 순회 가능한 구조가 필요했습니다. -
성능 최적화: DOM API는 실시간으로 업데이트되는 “살아있는” 컬렉션(
HTMLCollection)을 반환합니다. 이를 배열로 만들면 DOM이 변경될 때마다 배열도 다시 만들어야 하므로 비효율적입니다.
// HTMLCollection은 "살아있는" 컬렉션
const divs = document.getElementsByTagName('div');
console.log(divs.length); // 3
// DOM에 새 div 추가
document.body.appendChild(document.createElement('div'));
console.log(divs.length); // 4 (자동으로 업데이트!)
JavaScript의 대표적인 유사 배열 객체들
1. arguments 객체
함수 내부에서 사용할 수 있는 특별한 객체입니다.
function logArguments() {
console.log(arguments);
console.log(typeof arguments); // 'object'
console.log(Array.isArray(arguments)); // false
// 인덱스로 접근 가능
console.log(arguments[0]); // 'a'
console.log(arguments[1]); // 'b'
// length 속성이 있음
console.log(arguments.length); // 3
// ❌ 배열 메소드 없음
// arguments.map(arg => arg.toUpperCase()); // TypeError!
}
logArguments('a', 'b', 'c');
arguments의 특징:
function demonstrate(a, b) {
console.log('매개변수:', a, b);
console.log('arguments:', arguments);
// 실제 전달된 인자 개수
console.log('전달된 인자 수:', arguments.length);
// 모든 인자 출력
for (let i = 0; i < arguments.length; i++) {
console.log(`arguments[${i}]:`, arguments[i]);
}
}
demonstrate(1, 2, 3, 4, 5);
// 매개변수: 1 2
// arguments: Arguments(5) [1, 2, 3, 4, 5]
// 전달된 인자 수: 5
// arguments[0]: 1
// arguments[1]: 2
// arguments[2]: 3
// arguments[3]: 4
// arguments[4]: 5
주의: 화살표 함수에는 arguments가 없습니다!
const arrowFunc = () => {
console.log(arguments); // ❌ ReferenceError!
};
// 화살표 함수에서는 rest 파라미터를 사용하세요
const arrowFunc2 = (...args) => {
console.log(args); // ✅ 진짜 배열!
};
2. NodeList
querySelectorAll() 또는 childNodes가 반환하는 객체입니다.
// querySelectorAll은 NodeList를 반환
const divs = document.querySelectorAll('div');
console.log(divs); // NodeList(3) [div, div, div]
console.log(Array.isArray(divs)); // false
console.log(divs.length); // 3
console.log(divs[0]); // <div>...</div>
// ❌ 배열 메소드가 없습니다
// divs.map(div => div.textContent); // TypeError!
// divs.filter(div => div.classList.contains('active')); // TypeError!
// ✅ for...of는 사용 가능
for (const div of divs) {
console.log(div.textContent);
}
NodeList의 두 가지 타입:
// 1. Static NodeList (querySelectorAll)
const staticList = document.querySelectorAll('div');
console.log(staticList.length); // 3
document.body.appendChild(document.createElement('div'));
console.log(staticList.length); // 3 (변하지 않음)
// 2. Live NodeList (childNodes)
const liveList = document.body.childNodes;
console.log(liveList.length); // 10
document.body.appendChild(document.createElement('div'));
console.log(liveList.length); // 11 (자동으로 업데이트!)
3. HTMLCollection
getElementsByClassName(), getElementsByTagName() 등이 반환하는 객체입니다.
const buttons = document.getElementsByTagName('button');
console.log(buttons); // HTMLCollection(3) [button, button, button]
console.log(Array.isArray(buttons)); // false
console.log(buttons.length); // 3
// ❌ 배열 메소드가 없습니다!
// buttons.map(btn => btn.id); // TypeError!
// buttons.filter(btn => btn.disabled); // TypeError!
// ✅ for...of는 사용 가능
for (const button of buttons) {
console.log(button);
}
// ✅ 배열로 변환하면 모든 메소드 사용 가능
const buttonArray = Array.from(buttons);
const ids = buttonArray.map(btn => btn.id);
HTMLCollection은 항상 “살아있는” 컬렉션입니다:
const divs = document.getElementsByClassName('box');
console.log(divs.length); // 3
// 새 요소 추가
const newDiv = document.createElement('div');
newDiv.className = 'box';
document.body.appendChild(newDiv);
console.log(divs.length); // 4 (자동 업데이트!)
// 첫 번째 요소의 클래스 제거
divs[0].className = '';
console.log(divs.length); // 3 (다시 줄어듦!)
4. 문자열 (String)
놀랍게도 문자열도 유사 배열 객체입니다!
const str = 'hello';
// 유사 배열의 특징
console.log(str.length); // 5
console.log(str[0]); // 'h'
console.log(str[1]); // 'e'
// 배열처럼 순회 가능
for (let i = 0; i < str.length; i++) {
console.log(str[i]);
}
// for...of 사용 가능
for (const char of str) {
console.log(char);
}
// 일부 배열 메소드는 작동하지 않음
// str.push('!'); // ❌ TypeError!
// str.reverse(); // ❌ TypeError!
// 하지만 배열로 변환 가능
const charArray = Array.from(str);
console.log(charArray); // ['h', 'e', 'l', 'l', 'o']
console.log(charArray.reverse().join('')); // 'olleh'
// 또는 스프레드 연산자
const charArray2 = [...str];
console.log(charArray2); // ['h', 'e', 'l', 'l', 'o']
유사 배열을 진짜 배열로 변환하기
유사 배열을 배열로 변환하는 다양한 방법들이 있습니다. 각 방법의 장단점을 알아봅시다.
방법 1: Array.from() (가장 권장)
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
const arr = Array.from(arrayLike);
console.log(arr); // ['a', 'b', 'c']
console.log(Array.isArray(arr)); // true
// 🎯 매핑 함수를 두 번째 인자로 전달 가능
const doubled = Array.from([1, 2, 3], x => x * 2);
console.log(doubled); // [2, 4, 6]
// DOM 예제
const divs = document.querySelectorAll('div');
const texts = Array.from(divs, div => div.textContent);
console.log(texts); // ['첫 번째', '두 번째', '세 번째']
장점:
- ✅ 가장 명확하고 직관적
- ✅ 매핑 함수를 바로 적용 가능
- ✅ ES6+ 표준
- ✅ 모든 유사 배열과 이터러블에 작동
단점:
- ❌ IE11 이하에서 polyfill 필요
방법 2: 스프레드 연산자 [...]
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
// ❌ 일반 객체에는 사용 불가!
// const arr = [...arrayLike]; // TypeError!
// ✅ 이터러블 객체에만 작동
const divs = document.querySelectorAll('div');
const arr = [...divs];
console.log(arr); // [div, div, div]
// ✅ arguments에 사용
function sum(...args) {
return args.reduce((a, b) => a + b);
}
// 또는 기존 함수에서
function sumOld() {
const args = [...arguments];
return args.reduce((a, b) => a + b);
}
장점:
- ✅ 간결한 문법
- ✅ ES6+ 표준
단점:
- ❌ 이터러블 객체에만 작동 (일반 유사 배열 객체는 안 됨)
- ❌ 매핑 함수를 바로 적용할 수 없음
방법 3: Array.prototype.slice.call() (레거시)
const arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
const arr = Array.prototype.slice.call(arrayLike);
console.log(arr); // ['a', 'b', 'c']
// 짧게 쓰기
const arr2 = [].slice.call(arrayLike);
console.log(arr2); // ['a', 'b', 'c']
// arguments 예제
function oldSchool() {
const args = [].slice.call(arguments);
return args.map(x => x * 2);
}
장점:
- ✅ 모든 유사 배열에 작동
- ✅ ES5 환경에서도 작동 (IE9+)
단점:
- ❌ 읽기 어렵고 직관적이지 않음
- ❌ 현대적이지 않음
방법 4: 메소드 빌려쓰기 (변환 없이 사용)
배열로 변환하지 않고 배열 메소드를 직접 빌려 쓰는 방법입니다.
const divs = document.querySelectorAll('div');
// 배열로 변환하지 않고 map 사용
const texts = Array.prototype.map.call(divs, div => div.textContent);
console.log(texts); // ['첫 번째', '두 번째', '세 번째']
// 짧게 쓰기
const texts2 = [].map.call(divs, div => div.textContent);
console.log(texts2); // ['첫 번째', '두 번째', '세 번째']
// filter도 가능
const activeDivs = [].filter.call(divs, div => div.classList.contains('active'));
장점:
- ✅ 배열 생성 오버헤드가 없음
- ✅ 메모리 효율적 (매우 큰 컬렉션에서)
단점:
- ❌ 읽기 어려움
- ❌ 체이닝이 불가능
성능 비교
const divs = document.querySelectorAll('div'); // 10,000개
// 1. Array.from + for...of
console.time('Array.from');
const arr1 = Array.from(divs);
for (const div of arr1) {
div.textContent;
}
console.timeEnd('Array.from'); // ~5ms
// 2. 스프레드 + for...of
console.time('Spread');
const arr2 = [...divs];
for (const div of arr2) {
div.textContent;
}
console.timeEnd('Spread'); // ~5ms
// 3. slice.call + for...of
console.time('slice.call');
const arr3 = [].slice.call(divs);
for (const div of arr3) {
div.textContent;
}
console.timeEnd('slice.call'); // ~6ms
// 4. for...of 직접 사용 (변환 없음)
console.time('for...of');
for (const div of divs) {
div.textContent;
}
console.timeEnd('for...of'); // ~3ms (가장 빠름!)
권장사항:
- 🎯 일반적인 경우:
Array.from()사용 - 🎯 성능이 중요한 경우:
for...of직접 사용 - 🎯 레거시 지원:
slice.call()사용 - 🎯 간결함 우선: 스프레드 연산자 (이터러블에만)
실전에서 유사 배열 다루기
예제 1: 여러 요소에 이벤트 리스너 추가
// ❌ 초보자가 자주 하는 실수
const buttons = document.querySelectorAll('.btn');
buttons.addEventListener('click', handleClick); // ❌ TypeError!
// ✅ 올바른 방법 1: for...of (가장 권장)
for (const button of buttons) {
button.addEventListener('click', handleClick);
}
// ✅ 올바른 방법 2: Array.from + for...of
const buttonArray = Array.from(buttons);
for (const button of buttonArray) {
button.addEventListener('click', handleClick);
}
// ✅ 올바른 방법 3: 스프레드 + for...of
for (const button of [...buttons]) {
button.addEventListener('click', handleClick);
}
예제 2: DOM 요소 필터링
// 모든 div 중에서 특정 클래스를 가진 것만 찾기
const allDivs = document.querySelectorAll('div');
// ✅ 배열로 변환 후 filter 사용
const activeDivs = Array.from(allDivs).filter(div => {
return div.classList.contains('active');
});
console.log(activeDivs); // [div.active, div.active]
// 🎯 한 줄로 줄이기
const activeDivs2 = [...document.querySelectorAll('div')]
.filter(div => div.classList.contains('active'));
예제 3: DOM 요소 변환
// 모든 링크의 href 추출
const links = document.querySelectorAll('a');
const hrefs = Array.from(links, link => link.href);
console.log(hrefs); // ['https://...', 'https://...', ...]
// 또는
const hrefs2 = [...links].map(link => link.href);
// 텍스트 내용만 추출
const texts = Array.from(links, link => link.textContent.trim());
console.log(texts); // ['Home', 'About', 'Contact']
예제 4: arguments를 활용한 유연한 함수
// 가변 인자를 받는 sum 함수 (레거시 방식)
function sum() {
// arguments를 배열로 변환
const numbers = Array.from(arguments);
// 배열 메소드 사용 가능
return numbers.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40)); // 100
// 🎯 현대적 방법: rest 파라미터 (권장)
function sumModern(...numbers) {
// 이미 배열입니다!
return numbers.reduce((acc, num) => acc + num, 0);
}
// 더 나은 예: 첫 번째 인자는 곱하고 나머지는 더하기
function calculate(multiplier, ...numbers) {
const sum = numbers.reduce((acc, num) => acc + num, 0);
return sum * multiplier;
}
console.log(calculate(2, 1, 2, 3)); // (1+2+3) * 2 = 12
예제 5: 폼 요소 다루기
const form = document.querySelector('form');
const inputs = form.elements; // HTMLFormControlsCollection
// ✅ 모든 입력값 가져오기
const values = Array.from(inputs)
.filter(input => input.name) // name 속성이 있는 것만
.map(input => ({
name: input.name,
value: input.value
}));
console.log(values);
// [
// { name: 'username', value: 'john' },
// { name: 'email', value: 'john@example.com' },
// ...
// ]
// ✅ 객체로 변환
const formData = Array.from(inputs)
.filter(input => input.name)
.reduce((acc, input) => {
acc[input.name] = input.value;
return acc;
}, {});
console.log(formData);
// {
// username: 'john',
// email: 'john@example.com',
// ...
// }
예제 6: 라이브 컬렉션 주의하기
// ❌ 위험한 코드!
const divs = document.getElementsByClassName('box');
for (let i = 0; i < divs.length; i++) {
// 각 div의 클래스를 제거
divs[i].className = ''; // HTMLCollection이 실시간으로 변경됨!
}
// 문제: divs.length가 계속 줄어들어서 모든 요소를 순회하지 못함
// 첫 번째 요소의 클래스를 제거하면 그 요소가 컬렉션에서 빠지므로
// 두 번째 요소가 첫 번째 자리로 이동함
// ✅ 안전한 방법: 배열로 복사
const divsArray = Array.from(divs);
for (const div of divsArray) {
div.className = ''; // 이제 안전!
}
// ✅ 대안: 역순으로 순회
for (let i = divs.length - 1; i >= 0; i--) {
divs[i].className = '';
}
유사 배열의 함정과 주의사항
함정 1: 타입 체크의 어려움
function processArray(arr) {
if (Array.isArray(arr)) {
// 배열 처리
return arr.map(x => x * 2);
} else {
throw new Error('배열이 아닙니다!');
}
}
const realArray = [1, 2, 3];
const arrayLike = { 0: 1, 1: 2, 2: 3, length: 3 };
processArray(realArray); // ✅ [2, 4, 6]
processArray(arrayLike); // ❌ Error!
// ✅ 유사 배열도 처리하려면
function processArrayLike(arr) {
// 배열이 아니면 변환
const realArray = Array.isArray(arr) ? arr : Array.from(arr);
return realArray.map(x => x * 2);
}
processArrayLike(realArray); // ✅ [2, 4, 6]
processArrayLike(arrayLike); // ✅ [2, 4, 6]
함정 2: length 속성이 이상한 값일 수 있음
// ❌ 잘못된 유사 배열
const weird = {
0: 'a',
1: 'b',
2: 'c',
length: 100 // 실제 요소보다 큰 값!
};
const arr = Array.from(weird);
console.log(arr.length); // 100
console.log(arr); // ['a', 'b', 'c', undefined, undefined, ..., undefined]
// ✅ 안전한 처리
function safeArrayFrom(arrayLike) {
const arr = Array.from(arrayLike);
// undefined 제거
return arr.filter(item => item !== undefined);
}
console.log(safeArrayFrom(weird)); // ['a', 'b', 'c']
함정 3: 일부 메소드만 지원하는 경우
const nodeList = document.querySelectorAll('div');
// ❌ 배열 메소드가 없습니다
// nodeList.map(div => div.textContent); // TypeError!
// nodeList.filter(div => div.classList.contains('active')); // TypeError!
// ✅ 안전한 방법: 배열로 변환
const texts = Array.from(nodeList).map(div => div.textContent);
const activeDivs = Array.from(nodeList).filter(div => div.classList.contains('active'));
함정 4: 라이브 vs 정적 컬렉션
// Live Collection (HTMLCollection, childNodes)
const liveList = document.getElementsByClassName('item');
console.log(liveList.length); // 3
document.body.appendChild(createItem());
console.log(liveList.length); // 4 (자동 업데이트)
// Static Collection (NodeList from querySelectorAll)
const staticList = document.querySelectorAll('.item');
console.log(staticList.length); // 4
document.body.appendChild(createItem());
console.log(staticList.length); // 4 (변하지 않음)
// ✅ 예측 가능한 코드를 위해: 항상 배열로 복사
const items = Array.from(document.getElementsByClassName('item'));
// 이제 items는 변하지 않음
함정 5: this 바인딩 문제
const arrayLike = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
prefix: '>> '
};
// ❌ Array.from의 매핑 함수에서 this 사용
const arr = Array.from(arrayLike, function(item) {
return this.prefix + item; // this가 undefined!
});
console.log(arr); // [NaN, NaN, NaN]
// ✅ 세 번째 인자로 this 전달
const arr2 = Array.from(arrayLike, function(item) {
return this.prefix + item;
}, arrayLike); // thisArg
console.log(arr2); // ['>> a', '>> b', '>> c']
// 🎯 화살표 함수 사용 (권장)
const arr3 = Array.from(arrayLike, item => arrayLike.prefix + item);
console.log(arr3); // ['>> a', '>> b', '>> c']
유사 배열 객체를 직접 만들기
실무에서 유사 배열 객체를 직접 만들 일은 드물지만, 라이브러리를 만들 때 유용할 수 있습니다.
기본 유사 배열 만들기
function createArrayLike(...items) {
const arrayLike = { length: items.length };
for (let i = 0; i < items.length; i++) {
arrayLike[i] = items[i];
}
return arrayLike;
}
const arrayLike = createArrayLike('a', 'b', 'c');
console.log(arrayLike); // { 0: 'a', 1: 'b', 2: 'c', length: 3 }
console.log(arrayLike[0]); // 'a'
console.log(arrayLike.length); // 3
메소드가 있는 유사 배열 (jQuery 스타일)
function MyCollection(...items) {
this.length = items.length;
for (let i = 0; i < items.length; i++) {
this[i] = items[i];
}
}
// 유사 배열처럼 동작하지만 메소드도 있음
MyCollection.prototype.each = function(callback) {
for (let i = 0; i < this.length; i++) {
callback.call(this[i], i, this[i]);
}
return this; // 체이닝 지원
};
MyCollection.prototype.map = function(callback) {
const results = [];
for (let i = 0; i < this.length; i++) {
results.push(callback.call(this[i], this[i], i));
}
return new MyCollection(...results);
};
MyCollection.prototype.toArray = function() {
return Array.from(this);
};
// 사용
const collection = new MyCollection(1, 2, 3);
console.log(collection.length); // 3
console.log(collection[0]); // 1
collection.each((index, item) => {
console.log(`Item ${index}: ${item}`);
});
const doubled = collection.map(x => x * 2);
console.log(doubled.toArray()); // [2, 4, 6]
이터러블한 유사 배열 만들기
function IterableArrayLike(...items) {
this.length = items.length;
for (let i = 0; i < items.length; i++) {
this[i] = items[i];
}
// 이터레이터 프로토콜 구현
this[Symbol.iterator] = function* () {
for (let i = 0; i < this.length; i++) {
yield this[i];
}
};
}
const iterable = new IterableArrayLike('a', 'b', 'c');
// 유사 배열 특성
console.log(iterable[0]); // 'a'
console.log(iterable.length); // 3
// 이터러블 특성
for (const item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
// 스프레드 연산자 사용 가능
const arr = [...iterable];
console.log(arr); // ['a', 'b', 'c']
성능 고려사항
1. 변환 비용
const huge = document.querySelectorAll('div'); // 10,000개
// ❌ 불필요한 변환
function processNodes(nodes) {
// 여러 번 변환하면 비효율적
const arr1 = Array.from(nodes);
const arr2 = Array.from(nodes);
const arr3 = Array.from(nodes);
// ...
}
// ✅ 한 번만 변환
function processNodesOptimized(nodes) {
const arr = Array.from(nodes);
// arr을 재사용
const filtered = arr.filter(/* ... */);
const mapped = arr.map(/* ... */);
// ...
}
// 🎯 변환 없이 처리 (가장 빠름)
function processNodesOptimized2(nodes) {
for (const node of nodes) {
// 직접 처리
}
}
2. 메모리 사용
const huge = document.querySelectorAll('div'); // 10,000개
// ❌ 메모리 낭비
const arr = Array.from(huge); // 10,000개 요소를 복사
arr.forEach(div => {
console.log(div.id); // 단순 읽기 작업
});
// ✅ 메모리 효율적
[].forEach.call(huge, div => {
console.log(div.id); // 복사 없음
});
// 또는
for (const div of huge) {
console.log(div.id); // for...of도 복사 없음
}
3. 라이브 컬렉션 vs 정적 변환
// Live Collection은 항상 최신 상태
const live = document.getElementsByClassName('item');
// DOM이 변경될 때마다 자동 업데이트
// Static Array는 한 번만 생성
const static = Array.from(document.getElementsByClassName('item'));
// DOM이 변경되어도 static은 변하지 않음
// ✅ 사용 시나리오
// 1. DOM이 변경될 것이라면: Live Collection 사용
// 2. 한 번만 사용한다면: 배열로 변환
// 예: 동적으로 추가되는 요소도 처리
setInterval(() => {
const items = document.getElementsByClassName('item');
console.log(`현재 아이템 수: ${items.length}`);
}, 1000);
성능 권장사항
// 🎯 단순 순회만 필요: for...of 직접 사용
for (const div of nodeList) { /* ... */ }
// 🎯 배열 메소드 필요 (filter, map 등): Array.from
const texts = Array.from(nodeList).map(div => div.textContent);
// 🎯 매핑과 변환을 동시에: Array.from의 두 번째 인자
const texts = Array.from(nodeList, div => div.textContent);
// 🎯 매우 큰 컬렉션: 스트리밍 처리 고려
function* processLarge(nodes) {
for (let i = 0; i < nodes.length; i++) {
yield nodes[i].textContent;
}
}
for (const text of processLarge(hugeNodeList)) {
console.log(text); // 한 번에 하나씩 처리
}
결론: 유사 배열을 언제 어떻게 다룰까?
핵심 요약
- 유사 배열 객체는 배열처럼 보이지만 배열이 아닙니다
length속성과 인덱스 접근만 가능- 배열 메소드(
map,filter등)가 없음 Array.isArray()는false를 반환
- JavaScript에서 자주 만나는 유사 배열들
arguments(함수 인자)NodeList(querySelectorAll)HTMLCollection(getElementsByClassName)String(문자열도 유사 배열!)
- 배열로 변환하는 방법
- 🎯 일반적:
Array.from(arrayLike) - 🎯 간결함:
[...arrayLike](이터러블에만) - 🎯 레거시:
[].slice.call(arrayLike) - 🎯 성능:
for...of직접 사용 (변환 없음)
- 🎯 일반적:
- 주의사항
- 라이브 컬렉션은 DOM 변경 시 자동 업데이트됨
- 배열 메소드(
map,filter등)가 없음 - 타입 체크 시
Array.isArray()사용 불가
실전 가이드라인
// ✅ DOM 요소를 다룰 때
const elements = document.querySelectorAll('.item');
// 단순 순회만 필요하면
for (const el of elements) { /* ... */ }
// 배열 메소드가 필요하면
const array = Array.from(elements);
array.filter(/* ... */).map(/* ... */);
// 변환과 매핑을 동시에
const texts = Array.from(elements, el => el.textContent);
// ✅ 함수 인자를 다룰 때 (레거시)
function oldFunc() {
const args = Array.from(arguments);
// ...
}
// 🎯 현대적 방법 (권장)
function modernFunc(...args) {
// args는 이미 진짜 배열!
}
// ✅ 라이브러리 만들 때
class MyCollection {
constructor(...items) {
this.length = items.length;
items.forEach((item, i) => this[i] = item);
}
// 유사 배열처럼 동작하지만 커스텀 메소드도 제공
toArray() {
return Array.from(this);
}
}
마지막 조언
유사 배열 객체는 JavaScript의 역사적 산물이자 실용적인 설계입니다. DOM API와 레거시 코드에서 필연적으로 만나게 되므로, 이해하고 있어야 합니다.
현대 JavaScript에서는:
- ✅
Array.from()을 기본으로 사용하세요 - ✅ Rest/Spread 문법으로 배열을 직접 다루세요
- ✅
arguments대신 rest 파라미터를 사용하세요 - ✅ 성능이 중요하면 변환 없이 직접 순회하세요
유사 배열은 “배열이 아니지만 배열처럼 다룰 수 있는 객체”라는 것을 기억하고, 필요할 때 적절히 변환하여 사용하세요! 🎯
참고 자료
MDN 공식 문서
심화 학습
- You Don’t Know JS: Types & Grammar - Kyle Simpson
- JavaScript: The Definitive Guide - David Flanagan
- Eloquent JavaScript: Data Structures
관련 문서
- prototype.md - Prototype과 상속
- this.md - this 키워드 이해
- function-vs-arrow-function.md - 화살표 함수와 arguments
댓글