Array

Array (배열)

Array 객체는 다른 프로그래밍 언어의 배열과 마찬가지로 여러 항목을 하나의 변수 이름 아래에 저장할 수 있게 하며, 일반적인 배열 연산을 수행하는 멤버들을 갖습니다. 원문은 MDN의 JavaScript 내장 객체 참고 문서입니다.

출처: Array - JavaScript | MDN

본문

Array 객체는 다른 프로그래밍 언어의 배열처럼 단일 변수 이름 아래 여러 항목의 컬렉션을 저장할 수 있고, 일반적인 배열 연산을 수행하는 멤버들을 가집니다.

JavaScript에서 배열은 기본형(primitive)이 아니라 다음과 같은 핵심 특성을 가진 Array 객체입니다.

  • JavaScript 배열은 크기를 조정할 수 있고 서로 다른 데이터 타입을 섞어 담을 수 있습니다. (이 특성이 바람직하지 않으면 typed array(타입 배열)를 대신 사용하세요.)
  • JavaScript 배열은 연관 배열(associative array)이 아니므로, 배열 요소를 임의의 문자열을 인덱스로 접근할 수 없고 반드시 0 이상의 정수(또는 그에 해당하는 문자열 형태)를 인덱스로 접근해야 합니다.
  • JavaScript 배열은 0부터 시작하는 인덱스(zero-indexed)입니다. 첫 요소는 인덱스 0, 두 번째는 1, ... 마지막 요소는 배열의 length 속성 값 - 1입니다.
  • JavaScript 배열 복사 연산은 얕은 복사(shallow copy)를 만듭니다. (여러 JavaScript 객체를 다루는 모든 표준 내장 복사 연산은 깊은 복사가 아니라 얕은 복사를 만듭니다.)

배열 인덱스 (Array indices)

배열 객체는 (연관 배열에서처럼) 임의의 문자열을 요소 인덱스로 쓸 수 없고, 0 이상의 정수(또는 그에 해당하는 문자열 형태)를 사용해야 합니다. 정수가 아닌 값으로 설정하거나 접근하면 배열 목록 자체의 요소를 설정하거나 가져오지 않고, 그 배열의 객체 속성 컬렉션과 연결된 변수를 설정하거나 접근하게 됩니다. 배열의 객체 속성과 배열 요소 목록은 분리되어 있으며, 배열의 순회 및 변경 연산은 이러한 명명된 속성에는 적용할 수 없습니다.

배열 요소는 toString이 속성인 것과 같은 방식으로 객체 속성입니다(정확히 말하면 toString()은 메서드). 그럼에도 다음처럼 배열 요소에 접근하면 속성 이름이 유효하지 않기 때문에 문법 오류가 발생합니다.

arr.0; // syntax error

JavaScript 문법은 숫자로 시작하는 속성은 점 표기법 대신 대괄호 표기법으로 접근하도록 요구합니다. 배열 인덱스에 따옴표를 붙이는 것도 가능합니다(예: years['2'] 대신 years[2]). 보통은 필요하지 않습니다. years[2]2는 JavaScript 엔진이 암묵적 toString 변환을 통해 문자열로 강제 변환합니다. 그 결과 '2''02'years 객체의 서로 다른 두 슬롯을 가리키며, console.log(years["2"] !== years["02"])는 true가 될 수 있습니다. 오직 years['2']만이 실제 배열 인덱스이고, years['02']는 배열 순회에서 방문되지 않는 임의의 문자열 속성입니다.

length와 숫자 속성의 관계

JavaScript 배열의 length 속성과 숫자 속성은 연결되어 있습니다. join(), slice(), indexOf() 등 여러 내장 배열 메서드는 호출될 때 배열의 length 속성 값을 고려합니다. push(), splice() 등의 다른 메서드는 배열의 length 속성을 갱신합니다.

const fruits = [];
fruits.push("banana", "apple", "peach");
console.log(fruits.length); // 3

속성이 유효한 배열 인덱스이고 현재 배열 범위 밖일 때 배열에 속성을 설정하면, 엔진은 그에 따라 length 속성을 갱신합니다.

fruits[5] = "mango";
console.log(fruits[5]);          // 'mango'
console.log(Object.keys(fruits)); // ['0', '1', '2', '5']
console.log(fruits.length);      // 6

length를 늘리면 새 요소를 만들지 않고 빈 슬롯을 추가해 배열을 확장합니다(undefined조차 만들지 않습니다).

fruits.length = 10;
console.log(fruits); // ['banana', 'apple', 'peach', empty x 2, 'mango', empty x 4]
console.log(Object.keys(fruits)); // ['0', '1', '2', '5']
console.log(fruits.length);      // 10
console.log(fruits[8]);          // undefined

반대로 length 속성을 줄이면 요소가 삭제됩니다.

fruits.length = 2;
console.log(Object.keys(fruits)); // ['0', '1']
console.log(fruits.length);      // 2

배열 메서드와 빈 슬롯 (empty slots)

배열 메서드는 희소 배열(sparse array)에서 빈 슬롯을 만났을 때 서로 다른 동작을 보입니다. 일반적으로 구형 메서드(예: forEach)는 undefined를 담은 인덱스와 빈 슬롯을 다르게 취급합니다.

빈 슬롯을 특별히 취급하는 메서드: concat(), copyWithin(), every(), filter(), flat(), flatMap(), forEach(), indexOf(), lastIndexOf(), map(), reduce(), reduceRight(), reverse(), slice(), some(), sort(), splice(). forEach 같은 순회 메서드는 빈 슬롯을 전혀 방문하지 않습니다. concat, copyWithin 등의 다른 메서드는 복사할 때 빈 슬롯을 보존하므로 최종적으로 배열은 여전히 희소합니다.

const colors = ["red", "yellow", "blue"];
colors[5] = "purple";
colors.forEach((item, index) => {
  console.log(`${index}: ${item}`);
});
// 0: red / 1: yellow / 2: blue / 5: purple
colors.reverse(); // ['purple', empty × 2, 'blue', 'yellow', 'red']

신형 메서드(예: keys)는 빈 슬롯을 특별히 취급하지 않고 마치 undefined를 담은 것처럼 취급합니다. 빈 슬롯을 undefined 요소와 동일시하는 메서드: entries(), fill(), find(), findIndex(), findLast(), findLastIndex(), includes(), join(), keys(), toLocaleString(), toReversed(), toSorted(), toSpliced(), values(), with().

const colors = ["red", "yellow", "blue"];
colors[5] = "purple";
const iterator = colors.keys();
for (const key of iterator) {
  console.log(`${key}: ${colors[key]}`);
}
// 0: red / 1: yellow / 2: blue / 3: undefined / 4: undefined / 5: purple
const newColors = colors.toReversed(); // ['purple', undefined, undefined, 'blue', 'yellow', 'red']

복사 메서드와 변경 메서드 (Copying and mutating methods)

일부 메서드는 호출된 기존 배열을 변경하지 않고 새 배열을 반환합니다. 먼저 새 배열을 만든 뒤 요소로 채우는 방식이며, 복사는 항상 얕게(shallowly) 일어납니다. 객체의 경우 객체 참조가 새 배열로 복사되어 원본과 새 배열이 같은 객체를 가리킵니다(참조된 객체가 수정되면 양쪽 모두에 보입니다). 문자열·숫자·불리언 같은 기본형(단, String·Number·Boolean 객체는 아님)은 값을 새 배열로 복사합니다.

다른 메서드는 호출된 배열 자체를 변경하며, 이 경우 반환 값은 메서드에 따라 다릅니다 — 어떤 때는 같은 배열에 대한 참조, 어떤 때는 새 배열의 길이입니다.

다음 메서드는 this.constructor[Symbol.species]에 접근해 사용할 생성자를 결정해 새 배열을 만듭니다: concat(), filter(), flat(), flatMap(), map(), slice(), splice() (제거된 요소의 배열을 만들 때). 다음 메서드는 항상 Array 기본 생성자로 새 배열을 만듭니다: toReversed(), toSorted(), toSpliced(), with().

원본 배열을 변경하는 메서드와 그 비-변경 대안은 다음과 같습니다: copyWithin() ↔ (단일 대안 없음), fill() ↔ (단일 대안 없음), pop()slice(0, -1), push(v1, v2)concat([v1, v2]), reverse()toReversed(), shift()slice(1), sort()toSorted(), splice()toSpliced(), unshift(v1, v2)toSpliced(0, 0, v1, v2). 변경 메서드를 비-변경 대안으로 쉽게 바꾸는 방법은 스프레드 문법이나 slice()로 먼저 복사본을 만드는 것입니다.

arr.copyWithin(0, 1, 2);                 // arr 변경
const arr2 = arr.slice().copyWithin(0, 1, 2);   // arr 변경 안 함
const arr3 = [...arr].copyWithin(0, 1, 2);      // arr 변경 안 함

반복 메서드 (Iterative methods)

많은 배열 메서드는 콜백 함수를 인자로 받습니다. 콜백 함수는 순차적으로, 배열의 각 요소에 대해 최대 한 번 호출되며, 콜백 함수의 반환 값을 이용해 메서드의 반환 값을 결정합니다. 모두 같은 시그니처를 공유합니다: method(callbackFn, thisArg). callbackFn은 세 인자를 받습니다 — element(현재 처리 중인 요소), index(현재 요소의 인덱스), array(해당 메서드가 호출된 배열). callbackFn이 반환해야 하는 값은 호출된 배열 메서드에 따라 다릅니다.

thisArg 인자(기본값 undefined)는 callbackFn을 호출할 때 this 값으로 사용됩니다. callbackFn이 실제로 관찰하는 this 값은 일반적인 규칙에 따라 결정됩니다 — 비-strict 함수이면 기본형 this 값은 객체로 감싸지고, undefined/nullglobalThis로 대체됩니다. 화살표 함수로 정의된 callbackFn에는 thisArg가 무의미합니다(화살표 함수는 자신의 this 바인딩이 없기 때문).

callbackFn에 전달되는 array 인자는 순회 중 다른 인덱스를 읽고 싶을 때 가장 유용합니다(현재 배열을 가리키는 기존 변수가 항상 있는 것은 아니기 때문). 순회 중 배열을 변경하는 것은 일반적으로 피해야 합니다. map(), filter(), flatMap()처럼 배열을 만드는 메서드의 경우 array 인자는 만들어지고 있는 배열이 아닙니다 — 콜백 함수에서 만들어지는 배열에 접근할 방법은 없습니다.

모든 반복 메서드는 복사형(copying)이고 제네릭(generic)이지만, 빈 슬롯에서는 다르게 동작합니다. 반복 메서드: every(), filter(), find(), findIndex(), findLast(), findLastIndex(), flatMap(), forEach(), map(), some(). 특히 every(), find(), findIndex(), findLast(), findLastIndex(), some()은 항상 모든 요소에 callbackFn을 호출하지는 않습니다 — 반환 값이 결정되는 즉시 순회를 멈춥니다.

reduce()reduceRight()도 콜백 함수를 받아 배열의 각 요소에 대해 최대 한 번 실행하지만, 일반적인 반복 메서드와는 시그니처가 약간 다릅니다(예: thisArg를 받지 않음). sort()도 콜백 함수를 받지만 반복 메서드가 아닙니다 — 배열을 제자리에서 변경하고, thisArg를 받지 않으며, 한 인덱스에 대해 콜백을 여러 번 호출할 수 있습니다.

반복 메서드는 다음과 같은 방식으로 배열을 순회합니다(많은 기술적 세부사항은 생략).

function method(callbackFn, thisArg) {
  const length = this.length;
  for (let i = 0; i < length; i++) {
    if (i in this) {
      const result = callbackFn.call(thisArg, this[i], i, this);
      // result로 뭔가 하기; 일찍 반환할 수도 있음
    }
  }
}

참고: 모든 메서드가 i in this 테스트를 수행하는 것은 아닙니다. find, findIndex, findLast, findLastIndex는 수행하지 않지만 다른 메서드는 수행합니다. length는 루프 시작 전에 기억됩니다(순회 중 삽입·삭제 처리에 영향). 메서드는 배열 내용을 기억하지 않으므로 순회 중 어떤 인덱스가 수정되면 새 값을 관찰할 수 있습니다. 위 코드는 인덱스 오름차순으로 순회합니다. 일부 메서드는 인덱스 내림차순으로 순회합니다(for (let i = length - 1; i >= 0; i--)): reduceRight(), findLast(), findLastIndex(). reducereduceRight는 시그니처가 약간 다르고 항상 첫/마지막 요소에서 시작하지 않습니다.

제네릭 배열 메서드 (Generic array methods)

배열 메서드는 항상 제네릭입니다 — 배열 객체의 내부 데이터에 접근하지 않습니다. 오직 length 속성과 인덱스된 요소를 통해서만 배열 요소에 접근합니다. 즉 배열-유사(array-like) 객체에서도 호출할 수 있습니다.

const arrayLike = { 0: "a", 1: "b", length: 2 };
console.log(Array.prototype.join.call(arrayLike, "+")); // 'a+b'

length 속성의 정규화. length 속성은 정수로 변환된 뒤 02^53 - 1 사이 범위로 고정(clamp)됩니다. NaN0이 되므로, length가 없거나 undefined여도 값이 0인 것처럼 동작합니다. 언어는 length를 안전하지 않은 정수로 설정하는 것을 피합니다. 모든 내장 메서드는 length2^53 - 1보다 큰 수로 설정되면 TypeError를 던집니다. 다만 배열의 length 속성은 2^32 - 1보다 크게 설정하면 오류를 던지므로, 배열이 아닌 객체에서 메서드를 호출하지 않는 한 안전 정수 임계값에 도달하는 일은 보통 없습니다.

Array.prototype.flat.call({}); // []

일부 배열 메서드는 배열 객체의 length 속성을 설정합니다. 항상 정규화 후의 값을 설정하므로 length는 항상 정수로 끝납니다.

const a = { length: 0.7 };
Array.prototype.push.call(a);
console.log(a.length); // 0

배열-유사 객체(Array-like objects). 배열-유사 객체란 위에서 설명한 length 변환 과정에서 오류를 던지지 않는 객체를 말합니다. 실무에서는 실제로 length 속성을 갖고, 0부터 length - 1 범위의 인덱스 요소를 가질 것으로 기대됩니다(모든 인덱스가 없으면 기능적으로 희소 배열과 동등). 배열 메서드가 배열-유사 객체에서 동작할 때 0보다 작거나 length - 1보다 큰 정수 인덱스는 무시됩니다. 많은 DOM 객체가 배열-유사입니다 — 예: NodeList, HTMLCollection. arguments 객체도 배열-유사입니다. 이 객체들에는 해당 메서드가 없어도 배열 메서드를 호출할 수 있습니다.

function f() {
  console.log(Array.prototype.join.call(arguments, "+"));
}
f("a", "b"); // 'a+b'

생성자 및 정적 속성/메서드 (Constructor and static members)

  • Array() — 새 Array 객체를 만듭니다.
  • Array[Symbol.species]Array 생성자를 반환합니다(정적 속성).
  • Array.from() — iterable 또는 배열-유사 객체에서 새 Array 인스턴스를 만듭니다.
  • Array.fromAsync() — async iterable, iterable 또는 배열-유사 객체에서 새 Array 인스턴스를 만듭니다.
  • Array.isArray() — 인자가 배열이면 true, 아니면 false를 반환합니다.
  • Array.of() — 인자의 수나 타입과 관계없이 가변 인자로 새 Array 인스턴스를 만듭니다.

인스턴스 속성 및 메서드 (Instance members)

인스턴스 속성은 Array.prototype에 정의되어 모든 Array 인스턴스가 공유합니다. constructor는 인스턴스 객체를 만든 생성자 함수이고, length는 배열의 요소 수를 반영하며, Array.prototype[Symbol.unscopables]는 ES2015 이전 ECMAScript 표준에 없었던 속성 이름들을 담아 with 문 바인딩에서 제외합니다(Array 인스턴스의 고유 속성).

주요 인스턴스 메서드: at()(음수 인덱스 지원), concat(), copyWithin(), entries(), every(), fill(), filter(), find(), findIndex(), findLast(), findLastIndex(), flat(), flatMap(), forEach(), includes(), indexOf(), join(), keys(), lastIndexOf(), map(), pop(), push(), reduce(), reduceRight(), reverse(), shift(), slice(), some(), sort(), splice(), toLocaleString(), toReversed(), toSorted(), toSpliced(), toString(), unshift(), values(), with(), 그리고 기본적으로 values()의 별칭인 [Symbol.iterator]().

예제 (Examples)

배열 만들기. 배열 리터럴, Array() 생성자, String.prototype.split() 세 가지 방식으로 배열을 만듭니다.

const fruits = ["Apple", "Banana"];           // 배열 리터럴
const fruits2 = new Array("Apple", "Banana"); // Array() 생성자
const fruits3 = "Apple, Banana".split(", ");  // split()

배열에서 문자열 만들기. join()을 사용합니다. 인덱스로 항목 접근. 첫 요소는 인덱스 0, 마지막 요소는 length - 1이며, length보다 큰 인덱스는 undefined를 반환합니다. 항목 찾기. indexOf()로 "Banana"의 위치를 찾습니다. includes()로 특정 항목 포함 여부를 확인합니다.

const fruits = ["Apple", "Banana"];
const fruitsString = fruits.join(", "); // "Apple, Banana"
fruits[0];                    // Apple
fruits[fruits.length - 1];    // Banana
fruits[99];                   // undefined
fruits.indexOf("Banana");     // 1

사양 및 호환성

Array는 ECMAScript 사양에서 정의되며, Baseline "Widely available"로 분류되어 2015년 7월부터 브라우저 전반에서 사용 가능했습니다. 기능의 일부 부분은 지원 수준이 각각 다를 수 있습니다.

더 알아보기