notes
27장. 배열
모던 JS Deep Dive — 배열 생성·요소 접근·고차 메서드 핵심 정리.
참고: 이웅모 《모던 자바스크립트 Deep Dive》 27장 · [MDN Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)
JS 배열은 진짜 밀집 배열이 아니라 객체 + length에 가깝다. 인덱스는 문자열 키처럼 취급되고, 중간이 비면 hole이 생긴다. “배열답게” 쓰려면 메서드·hole·희소/밀집을 구분해야 한다.
flowchart LR
A[Array] --> LEN[length]
A --> IDX["0..n 인덱스 키"]
A --> PROTO[Array.prototype 메서드]
HOLE[희소 hole] --> SKIP[일부 메서드가 건너뜀]
27.1–27.2 배열이란 / JS 배열은 배열이 아니다
전형적인 자료구조 배열은 동일 크기 슬롯의 연속 메모리. JS 배열은 해시 친화적 객체에 length가 붙은 형태에 가깝다. 그래서 중간 삭제가 가능하고, 희소 배열이 생긴다.
const a = [1, 2, 3];
console.log(typeof a, Array.isArray(a)); // object true
a[100] = 1;
console.log(a.length); // 101 — hole 포함 길이
생성
const a = [1, 2];
const b = Array.of(1, 2);
const c = Array.from('hi'); // ['h','i']
const d = new Array(3); // 길이 3 희소 — 실수 많음
const e = Array(3).fill(0);
new Array(3) vs Array.of(3) — 전자는 길이, 후자는 원소 하나.
length
writable. 줄이면 뒤가 잘리고, 늘리면 hole.
const a = [1, 2, 3];
a.length = 1;
console.log(a); // [1]
희소 vs 밀집
hole은 in/forEach/map 등에서 다르게 취급된다. for…of는 hole을 undefined처럼 도는 편.
const a = [1, , 3];
a.forEach((v, i) => console.log(i, v)); // 0,1 / 2,3 — 1 건너뜀
for (const v of a) console.log(v); // 1, undefined, 3
요소 추가·삭제
- push/pop — 끝
- unshift/shift — 앞 (비쌈)
- splice — 중간
- delete a[i] — hole 남김. splice로 지우는 편이 낫다
고차 메서드
const nums = [1, 2, 3, 4];
console.log(nums.map((n) => n * 2));
console.log(nums.filter((n) => n % 2 === 0));
console.log(nums.reduce((a, b) => a + b, 0));
console.log(nums.find((n) => n > 2));
console.log(nums.some((n) => n > 3), nums.every((n) => n > 0));
순수하게 쓰려면 원본을 변이하지 않는 메서드를 고른다. sort/reverse/splice는 뮤테이션.
sort 함정
[10, 2, 1].sort(); // [1, 10, 2] — 문자열 비교 기본
[10, 2, 1].sort((a, b) => a - b); // [1, 2, 10]
유사 배열
arguments, NodeList 등 — length와 인덱스는 있으나 Array 메서드 없음 → Array.from / spread.
function f() {
return Array.from(arguments);
}
예제
const users = [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
];
const byId = Object.fromEntries(users.map((u) => [u.id, u]));
const names = users.map((u) => u.name).join(', ');
// 불변 push
const next = [...a, 4];
const without = a.filter((_, i) => i !== 1);
헷갈리던 장면 / 함정
- new Array(n)으로 값 n 넣으려다 희소
- sort 기본 비교
- map이 hole을 건너뛰어 결과도 희소
- const 배열 = 내용 불변이라고 착각(15)
- sparse + JSON.stringify — hole이 null로
표
질문 → 자답
Q. 배열 vs 객체? A. 순서·length·배열 메서드가 필요하면 배열. 키-값 사전은 Map/객체.
Q. for…of vs forEach? A. for…of는 break/async 유리. forEach는 콜백, hole 스킵.
웹 보강 메모
MDN Array 메서드표의 mutating 열을 북마크. toSorted/toReversed(새 불변 메서드) 지원 환경을 확인.
실무로 바로
- 생성: 리터럴 / from / of
- 변환: map/filter/reduce
- 정렬: 복사 후 compare
- 유사 배열: Array.from
- hole 만들지 않기
변이 vs 비변이 선택
상태를 스토어에 두면 변이 메서드가 구독 시스템을 깨뜨린다. 새 배열을 만들고 참조를 바꾼다.
const push = (arr, x) => [...arr, x];
const setAt = (arr, i, x) => arr.map((v, idx) => (idx === i ? x : v));
const removeAt = (arr, i) => arr.filter((_, idx) => idx !== i);
reduce로 그룹핑
const list = [
{ t: 'a', n: 1 },
{ t: 'b', n: 2 },
{ t: 'a', n: 3 },
];
const grouped = list.reduce((acc, cur) => {
(acc[cur.t] ||= []).push(cur);
return acc;
}, {});
Array.isArray의 위치
typeof [] === 'object'이므로 배열 판별은 Array.isArray. iframe/realm을 건너면 프로토타입이 달라질 수 있어 isArray가 안전장치.
copyWithin / fill
const a = [1, 2, 3, 4];
a.fill(0, 1, 3); // [1,0,0,4]
[1, 2, 3, 4].copyWithin(0, 2); // 변이 복사
저수준. 대부분 map/slice로 충분.
이터러블과의 연결
배열은 이터러블. spread, for…of, Array.from이 같은 규약 위에 있다(Iterator 노트).
const set = new Set([1, 2, 2]);
console.log([...set], Array.from(set));
큐·스택을 배열로
const q = [];
q.push(1); q.push(2);
q.shift(); // 큐 — 앞 제거 비용
const stack = [];
stack.push(1); stack.pop(); // 스택 — 끝만
대량 큐는 인덱스를 옮기는 deque 구현이 나을 수 있다.
at / with (모던)
const a = [1, 2, 3];
console.log(a.at(-1)); // 3
const b = a.with(1, 99); // 복사본 [1,99,3] — 지원 환경
음수 인덱스는 at. 불변 업데이트는 with/toSorted 계열.
정렬 안정성·비교자
모던 엔진 sort는 안정 정렬인 경우가 많다. 비교자는 음/0/양만 지키면 된다. 객체 배열:
users.sort((a, b) => a.name.localeCompare(b.name, 'ko'));
JSON과 배열
JSON.stringify는 hole을 null로. 복원 후 length는 유지될 수 있으나 의미가 달라진다. hole 금지 정책이 속 편하다.
여기서 잠깐 💁♂️
28–30은 Number·Math·Date. 배열 인덱스·length와 숫자 강제 변환이 맞물린다.
보강 — 다차원·flatten
const matrix = [[1, 2], [3, 4]];
console.log(matrix.flat());
console.log([[1, [2]]].flat(2));
console.log([1, 2, 3].flatMap((n) => [n, n * 2]));
보강 — 인덱스 검색
indexOf/includes는 동일 비교. NaN은 includes가 잡아주고 indexOf는 못 잡는 차이가 있다. 객체는 참조 비교 — find로 조건 검색.