유사 배열 객체 - 배열인 듯 배열 아닌 배열 같은 너

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 조작을 하면서 NodeListarguments 객체를 다루다 보니, 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

“어? 분명히 배열처럼 생겼는데?” 하지만 buttonsNodeList이고, 배열 메소드(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)는 배열처럼 보이고 일부 배열처럼 동작하지만, 실제로는 배열이 아닌 객체입니다.

유사 배열 객체의 조건

객체가 유사 배열로 간주되려면 다음 조건을 만족해야 합니다:

  1. length 속성을 가지고 있어야 합니다
  2. 인덱스로 접근 가능해야 합니다 (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()
                  └─ ... (배열 메소드 없음!)

왜 유사 배열이 존재할까?

역사적인 이유와 성능상의 이유가 있습니다.

  1. 역사적 이유: JavaScript 초기에는 배열이 지금처럼 강력하지 않았습니다. arguments 같은 특별한 객체들은 배열이 아니면서도 순회 가능한 구조가 필요했습니다.

  2. 성능 최적화: 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); // 한 번에 하나씩 처리
}

결론: 유사 배열을 언제 어떻게 다룰까?

핵심 요약

  1. 유사 배열 객체는 배열처럼 보이지만 배열이 아닙니다
    • length 속성과 인덱스 접근만 가능
    • 배열 메소드(map, filter 등)가 없음
    • Array.isArray()false를 반환
  2. JavaScript에서 자주 만나는 유사 배열들
    • arguments (함수 인자)
    • NodeList (querySelectorAll)
    • HTMLCollection (getElementsByClassName)
    • String (문자열도 유사 배열!)
  3. 배열로 변환하는 방법
    • 🎯 일반적: Array.from(arrayLike)
    • 🎯 간결함: [...arrayLike] (이터러블에만)
    • 🎯 레거시: [].slice.call(arrayLike)
    • 🎯 성능: for...of 직접 사용 (변환 없음)
  4. 주의사항
    • 라이브 컬렉션은 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 공식 문서

심화 학습

관련 문서

추가 자료

댓글