기본 콘텐츠로 건너뛰기

자바스크립트 배열 제대로 이해하기: 인덱스, length, 빈 슬롯, 메서드 선택까지

자바스크립트 배열 제대로 이해하기: 인덱스, length, 빈 슬롯, 메서드 선택까지

빠른 답

  • 배열은 0부터 시작하는 인덱스로 접근하는 순서 있는 값 목록이다.
  • length는 단순한 요소 개수처럼 보이지만 마지막 인덱스의 영향도 함께 받는다.
  • 중간 인덱스를 건너뛰어 값을 넣으면 undefined가 아니라 빈 슬롯이 생길 수 있다.
  • 실무에서는 직접 인덱스를 조작하기보다 push, splice, map, filter 같은 메서드 사용이 더 안전하다.

배열을 단순한 목록으로만 보면 놓치는 핵심

자바스크립트 배열은 값을 순서대로 담는 자료구조처럼 보이지만, 실제로는 숫자 인덱스와 length 규칙을 가진 특수한 객체입니다. 그래서 배열을 제대로 다루려면 “값이 몇 개 있나”만 보는 것이 아니라 “어떤 인덱스가 실제로 존재하나”까지 함께 봐야 합니다.

이 차이를 가장 쉽게 드러내는 것이 빈 슬롯입니다. 예를 들어 arr[5] = 'x'처럼 중간을 건너뛰고 값을 넣으면, 0부터 4까지가 모두 채워지는 것이 아닙니다. 읽을 때는 undefined처럼 보일 수 있어도, 실제로는 값이 없는 인덱스가 생길 수 있습니다. 이 상태는 순회, 검색, 직렬화에서 미묘하게 다른 결과를 만듭니다.

원문에 있던 “배열은 해시 테이블처럼 구현된다”는 식의 설명은 현재 기준으로는 너무 단정적입니다. 자바스크립트 엔진은 배열을 내부적으로 다양하게 최적화할 수 있고, 개발자가 실전에서 알아야 할 핵심은 엔진 내부 구조보다 언어 규칙입니다. 정확한 기준은 “배열이 무엇처럼 구현되는가”보다 “인덱스, length, 빈 슬롯, 메서드가 어떻게 동작하는가”에 있습니다. 기본 정의는 MDN ArrayMDN length 문서가 가장 명확합니다.

흐름으로 보기

자바스크립트 배열 제대로 이해하기: 인덱스, length, 빈 슬롯, 메서드 선택까지 흐름 다이어그램

배열은 이 순서로 이해하면 헷갈림이 줄어듭니다. 먼저 어떤 방식으로 배열을 만들지 정하고, 그다음 인덱스와 length의 관계를 이해한 뒤, 요소를 안전하게 추가하고 삭제하는 방법을 익히는 것이 좋습니다. 마지막으로 순회 메서드와 빈 슬롯 차이까지 잡아두면 실무에서 생기는 대부분의 혼란을 설명할 수 있습니다.

인덱스와 length는 어떻게 함께 동작할까

배열 인덱스는 0부터 시작합니다. 그래서 첫 번째 값은 arr[0], 두 번째 값은 arr[1]입니다. 여기까지는 익숙하지만, length를 무조건 “배열 안에 들어 있는 값 개수”라고 생각하면 곧바로 예외를 만나게 됩니다.

배열의 length는 보통 가장 큰 배열 인덱스에 맞춰 움직입니다. 따라서 중간 인덱스를 비워 둔 채 뒤쪽 인덱스에 값을 넣으면 length는 커지지만, 실제로 값이 채워진 칸 수와 정확히 일치하지 않을 수 있습니다. 반대로 length를 줄이면 뒤쪽 요소가 잘리고, 늘리면 새 값이 채워지는 것이 아니라 빈 슬롯이 늘어납니다.

const arr = ['a', 'b', 'c'];

console.log(arr.length); // 3

arr[5] = 'f';
console.log(arr.length); // 6
console.log(arr); // ['a', 'b', 'c', <2 empty items>, 'f']

arr.length = 2;
console.log(arr); // ['a', 'b']

arr.length = 5;
console.log(arr); // ['a', 'b', <3 empty items>]
console.log(arr[3]); // undefined

여기서 중요한 점은 arr[3]을 읽었을 때 undefined가 나오더라도, 그 인덱스가 실제로 존재한다고 단정할 수 없다는 점입니다. 값이 없어서 undefined처럼 읽힌 것일 수도 있고, 실제로 undefined가 들어 있는 것일 수도 있습니다.

또 하나 놓치기 쉬운 부분은 배열이 객체라는 사실입니다. 아래처럼 임의 프로퍼티를 붙일 수는 있지만, 이런 값은 배열 인덱스가 아니기 때문에 length에는 영향을 주지 않습니다.

const items = ['apple', 'banana'];
items.owner = 'kim';

console.log(items.length); // 2
console.log(items.owner); // 'kim'

이런 코드는 가능하지만 권장되지는 않습니다. 키-값 저장이 필요하면 객체나 Map을 쓰는 편이 의도를 더 분명하게 드러냅니다.

배열 생성과 초기값을 만드는 안전한 방법

가장 일반적인 생성 방식은 리터럴 문법인 []입니다. 읽기 쉽고, 의도가 분명하고, 대부분의 경우 가장 안전합니다. 배열은 여러 타입을 함께 담을 수 있지만, 실무에서는 의미가 비슷한 값끼리 한 배열에 넣는 편이 유지보수에 좋습니다.

헷갈리기 쉬운 구간은 Array(3) 같은 생성자 호출입니다. Array(3)은 숫자 3을 담은 배열이 아니라 길이만 3인 배열을 만듭니다. 반면 [3]은 숫자 3 하나를 담은 배열입니다. 문법이 비슷해 보여도 결과는 완전히 다릅니다.

const fruits = ['apple', 'banana', 'orange'];

console.log(fruits[0]); // 'apple'
console.log(fruits.length); // 3

console.log([3]); // [3]
console.log(Array(3)); // [ <3 empty items> ]
console.log(Array.of(3)); // [3]

console.log(Array.from({ length: 3 }, (_, i) => i)); // [0, 1, 2]
console.log(new Array(3).fill(0)); // [0, 0, 0]

실무 선택 기준은 단순합니다.

  • 값을 직접 나열할 때는 []
  • 일정한 길이와 초기값이 필요할 때는 fill
  • 인덱스를 이용해 값을 계산해 넣을 때는 Array.from
  • Array(숫자)는 빈 슬롯을 만들 수 있으므로 의도를 확실히 알고 쓸 때만 사용

특히 Array(3).map(...)이 기대대로 동작하지 않는 이유는 그 배열이 처음부터 값으로 채워져 있지 않기 때문입니다. 이미 값이 들어 있는 배열이 필요하면 Array.from이나 fill을 먼저 떠올리는 편이 좋습니다.

요소 추가와 삭제는 왜 메서드로 하는 편이 안전할까

배열을 직접 인덱스로 조작할 수도 있지만, 실무에서는 메서드가 훨씬 안전합니다. 메서드는 의도가 분명하고, 중간 상태를 덜 만들고, 리뷰할 때도 읽기 쉽습니다.

가장 많이 쓰는 기본 패턴은 다음과 같습니다.

  • 끝에 추가: push
  • 끝에서 제거: pop
  • 앞에 추가: unshift
  • 앞에서 제거: shift
  • 중간 삽입·삭제: splice
  • 일부 복사: slice

특히 삭제에서는 delete를 배열용 삭제 도구로 오해하는 경우가 많습니다. delete arr[1]은 요소를 당겨서 지우지 않고 그 자리에 빈 슬롯만 남깁니다. 배열에서 요소를 제거하고 뒤쪽 값을 앞으로 당기고 싶다면 대부분 splice를 써야 합니다.

const todos = ['write', 'test'];

todos.push('deploy');
console.log(todos); // ['write', 'test', 'deploy']

todos.splice(1, 1, 'review');
console.log(todos); // ['write', 'review', 'deploy']

delete todos[1];
console.log(todos); // ['write', <1 empty item>, 'deploy']
console.log(todos.length); // 3
console.log(1 in todos); // false

원본 변경 여부도 중요합니다. push, pop, shift, unshift, splice, sort, reverse는 기존 배열을 바꿉니다. 반면 slice, concat, map, filter는 새 배열을 만듭니다. 상태를 불변으로 다뤄야 하는 코드라면 이 차이가 버그를 줄이는 핵심 기준이 됩니다.

현대 자바스크립트에서는 원본을 바꾸지 않는 메서드도 함께 알아두면 좋습니다. 예를 들어 splice 대신 toSpliced, sort 대신 toSorted, reverse 대신 toReversed를 쓰면 기존 배열을 유지한 채 새 배열을 만들 수 있습니다. “원본을 건드릴지 말지”를 먼저 결정한 뒤 메서드를 고르면 선택이 훨씬 쉬워집니다.

순회, 검색, 변환 메서드는 어떻게 고를까

배열 메서드는 이름보다 반환값 모양으로 외우는 편이 실전적입니다. 결과가 새 배열인지, 단일 값인지, 불리언인지부터 먼저 정하면 대부분 빠르게 고를 수 있습니다.

  • 실행만 하고 끝내면 forEach
  • 같은 길이의 새 배열을 만들면 map
  • 조건에 맞는 것만 남기면 filter
  • 첫 번째 일치 항목 하나를 찾으면 find
  • 존재 여부만 확인하면 includes 또는 some
  • 하나의 값으로 누적하면 reduce
const prices = [1200, 5000, 3200];

const withTax = prices.map(price => Math.round(price * 1.1));
const expensive = prices.filter(price => price >= 3000);
const firstOver3000 = prices.find(price => price > 3000);
const hasCheapItem = prices.some(price => price < 1000);
const total = prices.reduce((sum, price) => sum + price, 0);

console.log(withTax); // [1320, 5500, 3520]
console.log(expensive); // [5000, 3200]
console.log(firstOver3000); // 5000
console.log(hasCheapItem); // false
console.log(total); // 9400

여기서 자주 나오는 실수는 map을 순수 변환이 아니라 부수 효과 용도로 쓰는 것입니다. 예를 들어 로그만 찍고 결과 배열은 쓰지 않는다면 map보다 forEach가 맞습니다. 메서드를 “돌릴 수 있는 것”으로 고르기보다 “내가 어떤 결과를 원하는가”로 고르면 의도가 흔들리지 않습니다.

검색 계열에서도 차이를 기억해야 합니다. find는 값을 반환하고, findIndex는 위치를 반환하고, includes는 같은 값이 있는지만 확인합니다. 객체 배열에서 조건 검색이 필요하면 대부분 findsome이 더 자연스럽습니다.

빈 슬롯과 undefined는 왜 다르게 봐야 할까

빈 슬롯과 undefined는 콘솔에서 얼핏 비슷해 보이지만 같은 상태가 아닙니다.

  • 빈 슬롯: 해당 인덱스 자체가 없음
  • undefined: 인덱스는 존재하지만 값이 undefined

이 차이는 실제 디버깅에서 반드시 확인해야 합니다. 단순히 arr[1]을 찍어서 undefined가 나왔다고 해서 두 상태를 구분할 수는 없습니다. 해당 인덱스가 실제로 존재하는지까지 확인해야 합니다.

다음은 Node REPL이나 브라우저 콘솔에서 바로 재현할 수 있는 예시입니다.

> const a = [1, , 3]
> a
[ 1, <1 empty item>, 3 ]
> a[1]
undefined
> 1 in a
false
> Object.hasOwn(a, 1)
false

> const b = [1, undefined, 3]
> b
[ 1, undefined, 3 ]
> b[1]
undefined
> 1 in b
true
> Object.hasOwn(b, 1)
true

이 차이는 메서드 동작에도 영향을 줍니다. 빈 슬롯이 있는 배열은 순회할 때 건너뛰는 경우가 있고, 펼치기나 변환에서는 undefined처럼 채워지는 경우도 있습니다. 그래서 “읽으면 undefined가 나온다”와 “실제로 undefined가 들어 있다”를 같은 말처럼 다루면 안 됩니다.

const sparse = [1, , 3];
const explicit = [1, undefined, 3];

console.log(sparse.forEach((v, i) => console.log('sparse', i, v)));
// sparse 0 1
// sparse 2 3

console.log([...sparse]); // [1, undefined, 3]
console.log(Array.from(sparse)); // [1, undefined, 3]

console.log(explicit.forEach((v, i) => console.log('explicit', i, v)));
// explicit 0 1
// explicit 1 undefined
// explicit 2 3

실무에서 이 차이가 드러나는 대표적인 순간은 데이터 정제와 UI 렌더링입니다. 예를 들어 “3개를 렌더링해야 한다”고 생각하고 length만 믿고 순회하면, 실제로는 그중 일부가 빈 슬롯이라 렌더링 함수가 예상보다 덜 호출될 수 있습니다. 특히 외부 데이터 변환이나 오래된 배열 조작 코드가 섞인 곳에서는 빈 슬롯 여부를 먼저 확인하는 편이 빠릅니다.

실무에서 자주 하는 실수와 선택 기준

배열은 문법보다 선택 기준이 더 중요합니다. 아래 항목은 실제로 자주 겪는 실수입니다.

  • delete로 배열 요소를 지우고 정렬된 삭제가 일어났다고 생각한다.
  • length를 실제 값 개수와 완전히 같은 개념으로 본다.
  • Array(10)을 “값 10개가 들어 있는 배열”처럼 사용한다.
  • map을 변환이 아니라 반복 실행 용도로 쓴다.
  • slicesplice를 헷갈린다.
  • 원본 변경 메서드인지 아닌지 확인하지 않고 사용한다.
  • 빈 슬롯과 undefined를 같은 상태라고 가정한다.

짧게 정리하면 이렇게 판단하면 됩니다.

  • 배열 끝을 다루면 pushpop
  • 중간 수정이 필요하면 splice
  • 원본 유지가 중요하면 slice, map, filter 같은 비변경 메서드 우선
  • 초기값이 있는 고정 길이 배열이 필요하면 fill 또는 Array.from
  • 데이터가 비어 있는지 의심되면 length만 보지 말고 in, Object.hasOwn, 콘솔 전체 출력까지 확인

배열은 쉬워 보이지만, 실제로는 인덱스 존재 여부와 메서드 계약을 정확히 이해해야 예측 가능한 코드를 만들 수 있습니다. “값이 순서대로 있다”라는 수준에서 멈추지 말고, “그 인덱스가 실제로 존재하는가”, “이 메서드가 원본을 바꾸는가”, “빈 슬롯을 어떻게 처리하는가”까지 함께 보는 습관이 중요합니다.

원문 참고

https://www.maeil-mail.kr/question/32

댓글

이 블로그의 인기 게시물

아이콘 폰트 (icomoon 사용법)

 장난감 프로젝트를 만들다 보면, 아이콘이 필요한 경우가 있다. 간단하게 아이콘을 인터넷에서 검색하여, 이미지로 넣어두고 이미지 태그를 이용하여, 사용하는 경우가 일반적이였지만...  요즘에는 대부분 폰트를 이용하여 아이콘을 노출 한다. 나 같은 경우에도 기본적으로  https://material.io/resources/icons 를 참고하여 아이콘 폰트를 이용할 수 있도록 처리하고, 추가적으로 필요한 아이콘이고, 일상적으로 사용 되지 않는 아이콘의 경우에는  https://icomoon.io 에서 제작하여, 아이콘 폰트로 이용 하곤 한다.  그래서 이번에는 아이콘  https://icomoon.io 의 사용법을 간단히 공유하고자 한다.   들어가자 마자 위의 icoMoonApp버튼을 누르면 아래와 같은 화면이 나타난다.  icomoon에서 무료로 제공하는 아이콘들이 보이면 위에 파란색으로 표시 되어있는 집 모양 세가지를 선택한 후, 아래의 빨간색으로 표시되어있는 Generate Font를 눌러보자.  그리고 나서 바로 다운로드를 요청해보자. icomoon.zip이 다운로드가 될텐데, 압축을 해제해 보면, 아래의 폴더 및 파일들이 있다. 아래에서 중요한 것은 font 폴더와 style.css이다. demo-files fonts demo.html Read Me.txt selection.json style.css <!doctype html > <html> <head> <link rel ="stylesheet" href ="style.css" ></head> </head> <body> <span class ="icon-home" ></span> <span class ="icon-home2" ></span> <span class ="icon-hom...

Chart js와 amchart 비교

Chart js 특징은 위의 그림으로 대체 할 수 있을 듯 하다. 오픈 소스이고, 기본으로 제공하는 차트 종류가 8가지 Canv a s를 이용해서 차트를 그리고, 반응형을 지원한다. amchart amchart는 기본적으로 유료이며, 기본으로 제공하는 차트 종류가 기본적인 차트 + 주식 처럼 보이는 차트 + 지도에 관련된 차트(?) 까지 하면, 기본 제공 하는 종류가 20개 내외 이려나, 일일이 세기에는 양이 좀 많아 보인다. 렌더링은 svg를 통하여 그려지고, 당연 반응형도 지원이 된다. 그러면, 이 둘중에 어떤것이 내 프로젝트에 적합 하냐는 것이 문제이다. 일단, 주식 처럼 보이는 차트나 지도에 관련된 차트(?)가 필요하면, amchart를 선택해야 되는 것은 맞다. 그건 당연한 것이니 빼고 얘기 해보자! 여러 종류의 차트가 필요하다면, 일단은 amchart를 염두해 두는 것이 좋다. 돈 낸 만큼은 하는 듯 하다. 하지만, 기본적인 막대 그래프, 도넛 차트 등, 아주 기본적인 차트들인데, Chart js도 amchart도 그러한 차트가 없을 때가 문제가 된다. 그렇다면, 조금이라도 커스텀이 용이한 것을 찾는 것이 좋을 것이다.  일단 amchart에서 custom이라고 검색 하였을 때, 검색 결과가 61가지가 나온다. 차트의 종류도 많고, 각 차트마다 들어가는 속성이 매우 많기 때문에, 웬만한 내용들은 속성 값을 어떻게 주느냐에 따라서 변경이 가능 하게 된다. 커스텀의 예를 들면, 기본적으로 도넛 파이의 형태를 띄면서, 화살표로 목표를 표시해주는 차트가 필요하다고 생각 해보자. 이것은 amchart로 만든 그래프이고 이것은 chart js로 만든 그래프이다. 모양이 살짝 다르긴 하지만, 완벽하게 똑같이 구현 할 수도 있다. amchart로 만든 그래프의 경우, 저것은 도넛그래프가 아닌 guage 그래프이다. 원래 게이지 그래프는 이와 같...

javascript 압축 파일 다운로드

이번에는 전 게시글의 응용판? 이라고 해야하나....? 어쨋든! 우리는 각각의 파일들을 다운로드 해보았다. 그런데 생각보다 귀찮음?을 느꼇을 것이다. 파일을 각각 다운 받아야 한다는 현실때문에! 그래 파일 두개야 뭐 그렇다 치지... 하지만, 개발자도 사용자도 게으름뱅이이다. 자 결국, 우리가 해야 하는 것은 파일을 한 번에 둘다! 다운 받는 것이다. 물론, 클릭 한번에 여러개의 함수를 엮어서 다운받게 하면 되지만! 크롬에서 자주 봤듯이, 여러개의 파일을 다운로드를 시도하면 <- 여러개의 파일을 다운로드 합니다. 허용 합니까? 하고 물어보는 것을 볼 수 있다. 게다가 다운로드 한 파일들을 찾기도 귀찮다는 것. 자 해결책을 제시해보자면, https://github.com/Stuk/jszip 클라이언트 단에서 파일을 zip파일로 압축을 할 수가 있다! 필요한 작업은 아래와 같다. 0. 데이터 준비 1. BLOB(binary large object)를 만든다. 2. Blob을 URL.createObjectURL을 사용하여, 해당 binary의 주소를 생성. 3. 다운로드가 필요한 파일들을 Zip 객체에 셋팅! 4. a태그를 이용하여, 해당 url 셋팅 하고, 다운로드. 전 게시물과 별로 달라진게 없네... 자 그럼 샘플! 샘플을 보자! http://embed.plnkr.co/NMprnRxqYG0fkHa2J55D/ var util = {} function fixBinary(bin) { //binary to arrayBuffer var length = bin.length var buf = new ArrayBuffer(length) var arr = new Uint8Array(buf) for (var i = 0; i < length; i++) { arr[i] = bin.charCodeAt(i) } return buf } window.onload = function() { ...