JavaScript Array Deep Dive (3) — 생성 및 조작 메서드
자바스크립트 배열은 실제 배열이 아니다? V8 엔진과 배열 최적화의 모든 것
3화: 자바스크립트 배열 함수 완전 정복 (1) — 생성 및 조작 메서드
배열을 만들 줄 알아야, 제대로 쓸 수 있다
배열은 선언만으로도 쉽게 사용할 수 있지만, 어떻게 만들었느냐에 따라 동작 방식이나 잠재적인 버그 발생 가능성이 완전히 달라집니다. 예를 들어, new Array(5)
는 실제로 값을 넣는 것이 아니라 “비어 있는 5칸짜리 배열”을 만들고, Array.of(5)
는 숫자 5 하나가 담긴 배열을 생성합니다.
이처럼 헷갈리기 쉬운 배열 생성 함수들과 기본적인 조작 메서드들을 이번 포스트에서 명확하게 정리해봅니다.
1. 배열 생성 방법 총정리
자바스크립트에서 배열을 생성하는 다양한 방법을 알아보고 각각의 특징을 살펴봅니다.
방식 | 설명 | 예제 |
---|---|---|
리터럴 [] |
가장 일반적이고 직관적인 배열 생성 방식입니다. | const a = [1, 2, 3]; |
Array() |
new 키워드 없이도 사용할 수 있지만, 인자에 따라 동작이 달라집니다. |
Array(3) → [ <3 empty items> ] Array('a', 'b') → ['a', 'b'] |
new Array() |
Array 생성자를 사용하는 방식입니다. Array() 와 동일하게 동작합니다. |
new Array(3) → [ <3 empty items> ] new Array(1, 2, 3) → [1, 2, 3] |
Array.of() |
전달된 모든 인자를 요소로 갖는 배열을 정확하게 생성합니다. | Array.of(3) → [3] Array.of(1, {}, 'a') → [1, {}, 'a'] |
Array.from() |
이터러블(iterable) 객체(문자열, Set, Map 등)나 유사 배열(Array-like) 객체(arguments, NodeList 등)를 새로운 배열로 변환합니다. | Array.from('abc') → ['a','b','c'] Array.from([1, 2, 3], x => x * 2) → [2, 4, 6] |
주의: Array(3)
과 Array.of(3)
는 완전히 다릅니다!
이 두 가지는 개발자들이 가장 혼동하기 쉬운 부분 중 하나입니다.
console.log(new Array(3)); // [ <3 empty items> ] - 길이가 3인 빈 배열
console.log(Array.of(3)); // [ 3 ] - 숫자 3을 유일한 요소로 갖는 배열
Array(3)
또는 new Array(3)
은 인자가 하나이고 그 인자가 숫자일 때, 해당 길이를 가진 빈 배열을 만듭니다. 반면, Array.of(3)
은 인자의 값 자체를 요소로 배열을 만듭니다.
2. 기본 조작 메서드 (배열 앞뒤 조작)
배열의 양 끝에 요소를 추가하거나 제거하는 기본적인 메서드들입니다.
push()
: 배열의 끝에 요소 추가
배열의 마지막에 하나 이상의 요소를 추가하고, 변경된 배열의 length
를 반환합니다. 원본 배열을 직접 변경(mutates)합니다.
const arr = [1, 2];
arr.push(3); // arr은 이제 [1, 2, 3]
console.log(arr); // [1, 2, 3]
arr.push(4, 5); // 여러 요소 추가 가능
console.log(arr); // [1, 2, 3, 4, 5]
pop()
: 배열의 끝 요소 제거
배열의 마지막 요소를 제거하고 그 요소를 반환합니다. 배열이 비어 있으면 undefined
를 반환합니다. 원본 배열을 직접 변경합니다.
const arr = [1, 2, 3];
const lastElement = arr.pop(); // lastElement는 3, arr은 이제 [1, 2]
console.log(lastElement); // 3
console.log(arr); // [1, 2]
unshift()
: 배열의 앞에 요소 추가
배열의 맨 앞에 하나 이상의 요소를 추가하고, 변경된 배열의 length
를 반환합니다. 원본 배열을 직접 변경합니다.
const arr = [1, 2];
arr.unshift(0); // arr은 이제 [0, 1, 2]
console.log(arr); // [0, 1, 2]
arr.unshift(-2, -1); // 여러 요소 추가 가능
console.log(arr); // [-2, -1, 0, 1, 2]
shift()
: 배열의 앞 요소 제거
배열의 첫 번째 요소를 제거하고 그 요소를 반환합니다. 배열이 비어 있으면 undefined
를 반환합니다. 원본 배열을 직접 변경합니다.
const arr = [0, 1, 2];
const firstElement = arr.shift(); // firstElement는 0, arr은 이제 [1, 2]
console.log(firstElement); // 0
console.log(arr); // [1, 2]
성능 팁:
shift()
와unshift()
는 배열의 맨 앞에서 요소를 추가하거나 제거하기 때문에, 내부적으로 모든 요소의 인덱스를 재조정해야 합니다. 이로 인해push()
나pop()
보다 상대적으로 느릴 수 있습니다. 성능이 중요한 대규모 배열 작업 시에는 이 점을 고려해야 합니다.
3. 배열 길이 관리: length
속성
length
속성은 배열의 요소 개수를 나타냅니다. 이 속성을 직접 조작하여 배열의 크기를 변경할 수도 있습니다.
const arr = [1, 2, 3, 4];
arr.length = 2; // 배열의 길이를 줄이면, 뒷부분의 요소가 잘립니다.
console.log(arr); // [1, 2]
arr.length = 5; // 배열의 길이를 늘리면, 추가된 공간은 'empty items'로 채워집니다.
console.log(arr); // [1, 2, <3 empty items>]
console.log(arr[3]); // undefined (empty item에 접근하면 undefined)
length
를 줄이면 배열이 잘립니다. (뒤에서부터 요소가 제거됩니다.)length
를 늘리면 추가된 공간은empty
항목으로 채워집니다. 이empty
항목은 실제undefined
값과는 다릅니다.
4. 빈 배열을 만드는 다양한 방법
단순히 비어 있는 배열을 만들거나 특정 크기로 초기화된 배열을 만드는 다양한 방법입니다.
const a = []; // 가장 일반적인 빈 배열 생성 (길이 0)
const b = new Array(10); // 길이 10을 가진 빈 배열 (10개의 <empty items>)
console.log(b); // [ <10 empty items> ]
// `Array.from()`을 사용하여 특정 값으로 초기화된 배열 생성
// 두 번째 인자로 매핑 함수를 넘겨 각 요소의 값을 정의할 수 있습니다.
const c = Array.from({ length: 5 }, (_, i) => i); // 인덱스를 값으로 사용
console.log(c); // [0, 1, 2, 3, 4]
const d = Array.from({ length: 3 }, () => 'hello'); // 모든 요소를 'hello'로 채움
console.log(d); // ['hello', 'hello', 'hello']
5. 요소 직접 조작 (인덱스 접근)
배열은 0부터 시작하는 인덱스를 사용하여 특정 위치의 요소에 직접 접근하고 수정할 수 있습니다.
const arr = [1, 2, 3];
arr[1] = 99; // 인덱스 1의 요소를 99로 변경
console.log(arr); // [1, 99, 3]
arr[10] = 5; // 존재하지 않는 인덱스에 값을 할당하면, 배열의 길이가 자동으로 늘어납니다.
console.log(arr); // [1, 99, 3, <7 empty items>, 5]
console.log(arr.length); // 11 (가장 높은 인덱스 + 1)
주의: 배열 중간에
empty items
가 생기는 것을 Hole이라고 합니다. 이러한 Hole이 많아지면 V8 엔진의 최적화가 비활성화되어 성능 저하가 발생할 수 있으므로, 배열 중간을 건너뛰어 할당하는 것은 피하는 것이 좋습니다.
6. 복사 vs 참조의 함정 (얕은 복사 vs 깊은 복사)
자바스크립트에서 배열을 다룰 때 가장 흔하게 실수하는 부분 중 하나가 ‘참조(reference)’와 ‘복사(copy)’의 개념입니다.
얕은 복사 (Shallow Copy): 같은 참조를 가리킴
배열을 다른 변수에 할당하면, 실제 배열의 복사본이 만들어지는 것이 아니라 동일한 배열을 가리키는 참조가 전달됩니다.
const original = [1, 2, 3];
const copy = original; // 'original' 배열의 참조를 'copy'에 할당
copy[0] = 99; // 'copy'를 통해 요소를 변경하면, 'original'도 함께 변경됩니다.
console.log(original[0]); // 99 (같은 배열을 참조하고 있기 때문!)
console.log(original); // [99, 2, 3]
안전한 복사 (Shallow Copy): 새로운 배열 생성
원본 배열의 내용만을 복사하여 새로운 배열을 만들려면 ‘얕은 복사’ 방법을 사용해야 합니다.
const original = [1, 2, 3, { name: '복잡객체' }];
// 1. 스프레드 연산자 (`...`) 활용
// 가장 간편하고 널리 사용되는 얕은 복사 방법입니다.
const cloned = [...original];
cloned[0] = 99;
console.log(original[0]); // 1 (원본 배열은 변경되지 않음)
cloned[3].name = '변경됨'; // 객체는 여전히 참조를 공유하므로 원본도 변경됨!
console.log(original[3].name); // 변경됨
// 2. `slice()` 메서드 활용
// 인자 없이 `slice()`를 호출하면 배열의 전체를 복사한 새 배열을 반환합니다.
const sliced = original.slice();
sliced[0] = 77;
console.log(original[0]); // 1 (원본 배열은 변경되지 않음)
주의: 위 방법들은 얕은 복사이므로, 배열 안에 객체(Object)나 배열(Array)과 같은 참조 타입의 요소가 있다면 해당 요소들은 여전히 원본과 복사본이 같은 참조를 공유합니다. 완전한 분리를 위해서는 ‘깊은 복사(Deep Copy)’가 필요하며, 이는
JSON.parse(JSON.stringify(original))
같은 방법을 고려해볼 수 있습니다 (단, 이 방법은 함수, Date 등 특정 타입은 처리하지 못하는 한계가 있습니다).
7. 배열 초기화 전략
특정 패턴이나 값으로 배열을 초기화하는 유용한 방법들입니다.
// 1. Array.from()을 사용하여 순차적인 값으로 초기화
const initArray = Array.from({ length: 5 }, (_, i) => i + 1);
console.log(initArray); // [1, 2, 3, 4, 5]
// 2. new Array().fill()을 사용하여 모든 요소를 동일한 값으로 채우기
const filled = new Array(3).fill(0);
console.log(filled); // [0, 0, 0]
fill()
의 중요한 주의점:fill()
메서드에 객체와 같은 참조 타입의 값을 전달하면, 배열의 모든 요소가 동일한 객체를 참조하게 됩니다. 이는 예상치 못한 버그로 이어질 수 있습니다.
const arr = new Array(3).fill({}); // 3개의 요소가 모두 동일한 빈 객체를 참조!
console.log(arr); // [ {}, {}, {} ]
arr[0].name = 'a'; // 첫 번째 요소의 name 속성을 변경하면...
console.log(arr[0]); // { name: 'a' }
console.log(arr[1]); // { name: 'a' } - 두 번째, 세 번째 요소도 변경되어 있음!
console.log(arr[2]); // { name: 'a' }
이러한 문제를 피하려면, Array.from()
과 매핑 함수를 사용하여 각 요소마다 새로운 객체를 생성하는 것이 안전합니다.
const safeArr = Array.from({ length: 3 }, () => ({})); // 각 요소마다 새로운 객체 생성
safeArr[0].name = 'a';
console.log(safeArr[0]); // { name: 'a' }
console.log(safeArr[1]); // {} - 이제 두 번째 요소는 변경되지 않습니다.
정리
지금까지 살펴본 배열 생성 및 기본 조작 메서드/속성을 요약해봅시다.
메서드/속성 | 설명 | 비고 |
---|---|---|
Array.of() |
전달된 값을 그대로 요소로 갖는 배열 생성 | Array.of(5) → [5] |
Array.from() |
이터러블 또는 유사 배열을 새 배열로 변환 | 매핑 함수 사용 가능, Array.from('hi') |
push() |
배열 끝에 요소 추가 | 빠르고 효율적 |
pop() |
배열 끝 요소 제거 | 빠르고 효율적 |
shift() |
배열 앞에 요소 추가 | 느릴 수 있음 (인덱스 재조정) |
unshift() |
배열 앞 요소 추가 | 느릴 수 있음 (인덱스 재조정) |
length |
배열의 크기 확인 및 설정 | 수동 조절 가능, empty items 생성/제거 |
fill() |
배열의 모든 요소를 특정 값으로 채움 | 객체 사용 시 얕은 복사 주의! |
slice() |
배열의 일부를 얕게 복사하여 새 배열 반환 | 인자 없이 사용 시 전체 배열 얕은 복사 |
... (스프레드 연산자) |
배열을 얕게 복사하거나 합칠 때 사용 | 가장 간편한 얕은 복사 방법 |
마무리: 배열 생성과 조작, 제대로 알아야 실수를 줄인다
자바스크립트에서 배열을 만드는 방법은 다양하며, 각각의 미묘한 차이점을 이해하는 것이 중요합니다. 특히 빈 배열의 생성 방식, 타입 혼합, 그리고 복사 vs 참조 문제는 개발자들이 의외로 실수하기 쉬운 부분입니다.
이 포스트를 통해 배열을 어떻게 생성하고 조작해야 하는지에 대한 확실한 감을 잡고 잘 활용해 봅시다!