...
자바스크립트 배열
자바스크립트에서 배열은 여러개의 값을 저장하고 관리하는데 사용되는 아주 기본적인 자료구조이다. 자바스크립트 배열은 타입이 고정되어 있지 않아서, 서로 다른 타입의 원소들을 적재할 수가 있다. 그리고 배열은 동적으로 크기가 변할 수 있어 자바(Java)와는 달리 크기를 지정할 필요가 없다.
배열의 생성
자바스크립트에서 배열을 생성하는 방법은 여러가지가 있지만, 주로 대괄호 [] 안에 값을 넣어서 선언하는 문법을 사용한다.
// 배열 리터럴을 이용하는 방법 (추천)
var arrLit = [1, true, "JavaScript"];
// Array 객체의 생성자를 이용하는 방법
var arrObj = Array(1, true, "JavaScript");
// new 연산자를 이용한 Array 객체 생성 방법
var arrNewObj = new Array(1, true, "JavaScript");
// 빈 배열 length 3 생성
var arrNewObj = new Array(3);
배열의 접근
배열의 각 요소를 가져오기 위해 인덱스를 사용한다. 인덱스는 0부터 시작하며, 대괄호 [] 안에 인덱스 값을 넣어서 요소에 접근할 수 있다.
// 요소가 하나뿐인 배열을 생성함.
var arr = ["JavaScript"];
// 배열의 첫 번째 요소를 읽어서 대입함.
var element = arr[0];
// 배열의 두 번째 요소에 숫자 10을 대입함. 배열의 길이는 1에서 2로 늘어남.
arr[1] = 10;
// 배열의 세 번째 요소에 변수 element의 값을 대입함. 배열의 길이는 2에서 3으로 늘어남.
arr[2] = element;
console.log(arr); // 배열의 요소를 모두 출력함. [JavaScript,10,JavaScript]
console.log(arr.length); // 배열의 길이를 출력함. 3
배열 요소의 추가
배열에 요소를 추가하는데 있어 다음 3가지 방법이 존재한다. push() 메소드와 length 프로퍼티를 이용한 방법은 배열의 제일 끝에 새로운 요소를 추가하게 된다.
// 특정 인덱스를 지정하여 추가하는 방법
arr[특정인덱스] = 추가할 요소;
// push() 메소드를 이용하는 방법
arr.push(추가할 요소);
// length 프로퍼티를 이용하는 방법. 가장 빠르다.
arr[arr.length] = 추가할 요소;
var arr = [1, true, "Java"];
// push() 메소드를 이용하는 방법
arr.push("Script");
document.write(arr + "<br>"); // 1,true,Java,Script
// length 프로퍼티를 이용하는 방법
arr[arr.length] = 100;
document.write(arr + "<br>"); // 1,true,Java,Script,100
// 특정 인덱스를 지정하여 추가하는 방법
arr[10] = "자바스크립트";
document.write(arr + "<br>"); // 1,true,Java,Script,100,,,,,,자바스크립트
// 배열의 홀(hole)
document.write(arr[7]); // undefined
위의 예제에서 배열 arr의 길이는 최종적으로 11이 되게 된다. 왜냐하면 세번째 삽입 때 특정 인덱스를 지정하여 요소를 추가하였기 때문에 특정 인덱스만큼 배열의 크기가 늘어난 것이다. 그런데 이때 배열 요소가 존재하는 인덱스는 0, 1, 2, 3, 4, 10뿐이며, 나머지 인덱스에는 배열 요소가 존재하지 않게 된다.
이렇게 인덱스에 대응하는 배열 요소가 없는 부분을 배열의 홀(hole)이라고 한다. 그리고 자바스크립트에서는 이러한 배열의 홀(hole)을 undefined 값을 가지는 요소처럼 취급합니다.
배열 요소의 삭제
배열은 객체이기 때문에 배열의 요소를 삭제하기 위해 delete 연산자를 사용할 수 있다. 그런데 이때 length에는 변함이 없다. 따라서 해당 요소를 완전히 삭제하여 length 프로퍼티에도 반영되게 하기 위해서는 Array.prototype.splice 메소드를 사용해야 한다.
const numbersArr = ['zero', 'one', 'two', 'three'];
// 요소의 값만 삭제된다
delete numbersArr[2]; // (4) ["zero", "one", empty, "three"]
// 요소 값만이 아니라 요소 자체를 완전히 삭제하여 배열 크기가 줄어든다
// splice(시작 인덱스, 삭제할 요소수)
numbersArr.splice(2, 1); // (3) ["zero", "one", "three"]
배열의 length 프로퍼티
자바스크립트 배열의 length 프로퍼티는 사실 배열 내 요소의 개수가 아니라, 가장 큰 인덱스에 1을 더한 값이다. 그래서 배열에 무언가 조작을 가하면 length 프로퍼티가 자동으로 갱신된다.
length 프로퍼티의 또 다른 독특한 특징 중 하나는 쓰기가 가능하다는 점이다. length의 값을 수동으로 증가시키면 아무 일도 일어나지 않지만, 값을 감소시키면 배열이 잘려버린다. 짧아진 배열은 다시 되돌릴 수 없다.
let arr = [1, 2, 3, 4, 5];
arr.length = 2; // 요소 2개만 남기고 잘라봅시다.
alert( arr ); // [1, 2]
arr.length = 5; // 본래 길이로 되돌려 봅시다.
alert( arr[3] ); // undefined: 삭제된 기존 요소들이 복구되지 않습니다.
이런 특징을 이용하면 만일 배열을 완전히 비우고 싶을때 arr.length = 0 을 사용해 아주 간단하게 배열을 비울 수 있다.
let arr = [1, 2, 3, 4, 5];
arr.length = 0; // 배열 초기화
arr = []; // 빈 배열을 할당해줘도 된다
배열 요소의 정렬
// sort(): 배열의 요소를 정렬
let arr = [3, 1, 4, 2, 5];
arr.sort();
console.log(arr); // [1, 2, 3, 4, 5]
// reverse(): 배열의 요소를 역순으로 정렬
let arr2 = [1, 2, 3, 4, 5];
arr2.reverse();
console.log(arr2); // [5, 4, 3, 2, 1]
배열 여부 확인
자바스크립트에서는 배열이라는 타입(type)을 별도로 제공하지 않는다. 자바스크립트의 타입을 확인하는 typeof 연산자를 사용해도 'object'를 반환할 뿐이다. 자바스크립트 배열은 일종의 객체(object) 타입이기 때문이다.
var arr = [1, true, "JavaScript"]; // 배열 생성
document.write(typeof arr); // object
따라서 자바스크립트에서는 해당 변수가 배열인지 여부를 확인할 수 있도록 다음과 같은 방법들을 제공하고 있다.
- Array.isArray() 메소드
- instanceof 연산자
- constructor 프로퍼티
document.write(Array.isArray(arr)); // true
document.write(Array.isArray("문자열")); // false
ECMAScript 5부터는 Array 클래스에 isArray()라는 배열 여부를 확인할 수 있는 메소드를 추가 하였다. 하지만 구형 버전의 브라우저는 ECMAScript 5를 지원하지 않으므로, Array.isArray() 메소드가 정상적으로 동작하지 않을 수도 있다. 따라서 이때는 instanceof 연산자를 사용하여 해당 변수가 Array 객체인지를 판단하여 배열 여부를 확인할 수 있다.
document.write(arr instanceof Array); // true
document.write(123 instanceof Array); // false
자바스크립트 다차원 배열
자바스크립트에서 다차원 배열은 2차원 이상의 배열을 의미한다. 다차원 배열은 배열 안에 배열을 포함하여 구성되는 중첩 배열이라고 보면 된다. 예를 들어, 2차원 배열은 배열 안에 배열이 포함된 형태이며, 3차원 배열은 배열 안에 배열 안에 배열이 포함된 형태이다.
자바스크립트에서는 모든 것이 객체이기 때문에, 배열의 각 요소를 다시 배열로 정의해 중첩하는 식으로 다차원 배열을 구현할 수 있는 것이다. 그러나 중첩 배열 형태이기 때문에, 다차원이 되면 배열 탐색 속도가 느려지기도 하고, 언어적인 특성상 배열 요소 자체가 객체이기 때문에 대량의 데이터 처리에 불리하다는 단점이 있다. 그래서 자바스크립트에서 3차원 이상의 배열은 거의 사용하지 않는다고 보면 된다.
다차원 배열 생성
// 2차원 배열
const twoDimensionalArray = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// 3차원 배열
const threeDimensionalArray = [
[
[1, 2],
[3, 4]
],
[
[5, 6],
[7, 8]
]
];
다차원 배열 접근
// 2차원 배열
const twoDimensionalArray = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// 1행 2열의 값 가져오기
const value = twoDimensionalArray[0][1];
console.log(value); // 2
// 3차원 배열
const threeDimensionalArray = [
[
[1, 2],
[3, 4]
],
[
[5, 6],
[7, 8]
]
];
// 1번째 배열의 2번째 배열의 1번째 값 가져오기
const value2 = threeDimensionalArray[0][1][0];
console.log(value2); // 3
다차원 배열 추가
let animals = [
["고양이", 1],
["강아지", 2],
["개구리", 3],
];
animals.push(["코끼리", 4]);
console.log(animals);
/*
[
["고양이", 1],
["강아지", 2],
["개구리", 3],
["코끼리", 4]
]
*/
다차원 배열 제거
animals.pop(); // 상위 배열 맨 끝 요소 삭제
animals.shift(); // 상위 배열 맨 앞 요소 삭제
animals.splice(2,2); // 상위 배열 인덱스[2] 요소부터 2개 자식 배열을 삭제
자바스크립트 희소 배열
자바스크립트에서 배열은 일반적으로 연속된 값의 리스트이다. 반면, 희소 배열(Sparse Array)이란 배열에 속한 요소의 위치가 연속적이지 않은 배열을 의미한다. 즉, 배열의 요소 중간 중간에 빈 값이 들어있는 배열이라 보면 된다. 나머지 요소는 undefined로 초기화되어 있게 된다.
let arr = [];
arr[0] = 'apple';
arr[3] = 'banana';
console.log(arr); // ["apple", undefined, undefined, "banana"] 희소배열
let arr = []; // 빈 배열을 생성함.
arr[99] = "JavaScript" // 배열 arr의 100번째 위치에 문자열을 삽입함.
// 100번째 요소를 삽입했기 때문에 배열의 길이는 100으로 늘어남.
document.write("배열의 길이는 " + arr.length + "입니다.");
희소 배열(Sparse Array) 개념은 자바스크립트 언어 만의 특징이기도 하다. 희소 배열은 일반적인 배열보다 메모리를 덜 차지할 수 있지만 배열의 길이가 길어질수록 메모리 사용량이 증가하고, 배열의 요소를 접근할 때 인덱스를 찾는 과정이 느려질 수 있게 된다. 따라서, 희소 배열은 필요한 경우에만 사용하는 것이 좋다.
예를 들면, 일반적인 배열은 모든 요소를 메모리에 할당하고 초기화 하는데, 대용량의 데이터를 다룰 때 당장 다루지도 않는 요소들을 모두 메모리에 할당하면 성능이 저하될 수 있다. 이때, 희소 배열을 사용하여 현재 사용할 필요한 요소만 배열에 할당하고 나머지는 빈 자리로 두면 메모리 사용량을 줄일 수 있다.
이처럼 희소 배열을 사용하여 성능을 개선할 수 있지만 일반적인 배열을 사용하는 것이 더 편리하고 안전한 경우에는 희소 배열을 사용하지 않는 것이 좋다.
자바스크립트 배열의 자료구조
자바스크립트 배열에서는 다양한 메서드를 제공한다. 이 중 일부 자료구조 메서드를 제공하기도 하는데 대표적으로 스택(Stack)과 큐(Queue) 가 있다.
큐(queue) - FIFO
Queue란, 선입선출(First In First Out) 구조로서, 가장 먼저 들어온 요소가 가장 먼저 나가는 자료구조를 말한다. 이른바 매표소에서 표를 구하기 위해 줄을 서는 것과 같다. 가장 먼저 줄을 선 사람이 표를 구입하고 가장 먼저 나간다.
- push – 맨 끝에 요소를 추가합니다.
- shift – 제일 앞 요소를 꺼내 제거한 후 남아있는 요소들을 앞으로 밀어줍니다. 이렇게 하면 두 번째 요소가 첫 번째 요소가 됩니다.
let fruits = ["사과", "오렌지", "배"];
fruits.shift(); // 배열에서 첫요소 "사과"를 제거
alert( fruits ); // 오렌지,배
fruits.unshift('망고');
alert( fruits ); // 망고,오렌지,배
스택(stack) - LIFO
Stack이란, 후입선출(Last In First Out) 구조로서, 마지막에 들어온 요소가 가장 먼저 나가는 자료구조를 말한다. 이른바 라이플의 탄창을 예를 들수 있는데, 탄창에 마지막에 넣은 탄알이 가장 먼저 총기 약실에 들어가 발사된다.
- push – 요소를 스택 끝에 집어넣습니다.
- pop – 스택 끝 요소를 추출합니다.
let fruits = ["사과", "오렌지", "배"];
fruits.pop(); // 배열에서 마지막 요소 "배"를 제거
alert( fruits ); // 사과,오렌지
fruits.push("망고");
alert( fruits ); // 사과,오렌지,망고
물론 이러한 자료구조에 따르지 않고 메소드를 섞어서도 사용이 가능하다.
let fruits = ["사과"];
fruits.push("오렌지", "배");
fruits.unshift("파인애플", "레몬");
alert( fruits );
// ["파인애플", "레몬", "사과", "오렌지", "배"]
자바스크립트 문자열 배열
자바스크립트에서 문자열은 어떻게 보면 각 문자들을 이어 붙인 자료로도 볼 수 있다. 그래서 문자열 자체를 읽기 전용 배열로서 다룰 수 있다. 따라서 배열 처럼 문자열을 구성하는 각 문자에 바로 접근할 수 있다.
var str = "안녕하세요!"; // 문자열 생성
document.write(str[2]); // 하
배열로 취급되기 때문에 자바스크립트 배열(Array) 객체가 제공하는 모든 배열 관련 범용 메소드도 문자열에 사용할 수 있다. 하지만 이렇게 문자열을 배열처럼 접근하는 방법은 문자열을 배열처럼 착각하게 하여, 다음과 같은 실수를 유발할 수도 있어 조심해야 한다.
var str = "안녕하세요!"; // 문자열 생성
str[0] = ""; // 자바스크립트의 문자열은 읽기 전용이므로, 이 문장은 오류를 발생시킵니다.
따라서 문자열을 바로 배열처럼 사용하지 말고, Array.prototype.split() 메소드 등을 이용해 먼저 배열로 변환한 후 사용하는 것이 좋다.
자바스크립트 배열의 내부 구조
배열은 연속적인 컬렉션
보통 일반적으로 배열이라고하면, 동일한 크기의 메모리 공간이 연속적으로 나열된 밀집 목록을 말한다. 실제로 자바스크립트 엔진은 배열의 요소를 인접한 메모리 공간에 차례로 저장해놓는다. 이러한 배열의 특징 덕분에 인덱스를 이용하여 임의의 요소에 접근하는 것이 매우 빠르고 고속으로 동작한다.
왜냐하면 인덱스는 배열의 요소가 저장된 메모리 주소와 일치하며, 배열은 이러한 요소들이 연속적으로 메모리에 저장되어 있기 때문에 인덱스를 이용하여 배열의 요소에 접근하면 컴퓨터는 해당 인덱스에 해당하는 메모리 주소를 계산하고, 그 메모리 주소에 저장된 값을 읽어오기 때문이다. 즉, 배열의 요소가 연속적으로 저장되어 있기 때문에 한 번의 연산으로 원하는 요소에 접근할 수 있는 것이다.
예를 들어, 위 그림처럼 메모리 주소 1000에서 시작하고 각 요소의 크기가 8byte 인 배열을 생각해 보자. 인덱스 숫자에 요소 크기만 합산해주면 바로 검색 대상의 요소를 찾기 때문에 연산 속도가 높은 것이다.
- 검색 대상 요소의 메모리 주소 = 배열의 시작 메모리 주소 + 인덱스 * 요소의 바이트 수
- 인덱스가 0인 요소의 메모리 주소 : 1000 + 0 * 8 = 1000
- 인덱스가 1인 요소에 메모리 주소 : 1000 + 1 * 8 = 1008
- 인덱스가 2인 요소에 메모리 주소 : 1000 + 2 * 8 = 1016
그런데 배열을 '순서가 있는 자료의 컬렉션’처럼 다루지 않고 일반 객체처럼 다루면 이러한 고속 연산이 제대로 동작되지 않게 된다. 잘못된 방법의 예는 다음과 같다.
arr.test = 5같이 숫자가 아닌 값을 프로퍼티 키로 사용하는 경우arr[0]과arr[1000]만 추가하고 그사이에 아무런 요소도 없는 경우arr[1000],arr[999]같이 요소를 역순으로 채우는 경우
let fruits = []; // 빈 배열을 하나 만듭니다.
fruits[99999] = 5; // 배열의 길이보다 훨씬 큰 숫자를 사용해 프로퍼티를 만듭니다.
fruits.age = 25; // 임의의 이름을 사용해 프로퍼티를 만듭니다.
뒤에서 다루겠지만 배열을 객체이므로 놀랍게도 arr.test = 5 같이 객체 프로퍼티 접근해도 문제가 발생하지 않는다. 그런데 이런식으로 코드를 작성하게 되면, 자바스크립트 엔진이 배열을 일반 객체처럼 다루게 되어 배열을 다룰 때만 적용되는 최적화 기법이 동작하지 않아 배열 특유의 이점이 사라지게 된다. 이외에도 위에서 배운 자바스크립트 희소 배열을 만드는 행위 또한 마찬가지이다.
만일 임의의 키를 사용해야 한다면 배열보단 일반 객체가 적합한 자료구조일 확률이 높다.
배열 중간 요소 삭제
만일 배열의 마지막 요소가 아닌 중간에 위치한 요소를 삭제하게 된다면, 요소의 이동이 일어나기 때문에 연산 비용이 커져 성능적으로 문제가 될 수 있다. 배열에 요소를 삽입하거나 삭제하는 경우, 배열 요소를 연속적으로 유지하기 위해 요소를 이동시키기 때문이다.
따라서 만약 배열에서 중간 요소를 삭제해야 할 경우, 대안으로는 해당 요소를 삭제하지 않고 유효하지 않은 값으로 대체하거나, 해당 요소를 삭제하는 대신 배열을 빠르게 탐색 수 있는 또다른 자료구조를 사용하는 것이 좋다. 지금은 아직 익숙치는 않겠지만, 이러한 자료구조에는 연결 리스트(linked list), 트리(tree), 해시 테이블(hash table) 등이 있다.
자바스크립트 배열은 사실 객체
자바스크립트의 배열은 엄밀히 말해 일반적 의미의 배열이 아니다. 사실 자바스크립트에서의 배열은 조금 특수 처리된 배열의 동작을 흉내낸 일반 객체이다.
자바스크립트 객체의 모든 속성의 설명을 보여주는 Object.getOwnPropertyDescriptors() 메서드를 사용해 배열을 조회해보면 아래와 같이 나온다.
const arr = [1, 2, 3];
console.log(Object.getOwnPropertyDescriptors(arr));
/*
{
'0': { value: 1, writable: true, enumerable: true, configurable: true },
'1': { value: 2, writable: true, enumerable: true, configurable: true },
'2': { value: 3, writable: true, enumerable: true, configurable: true },
length: { value: 3, writable: true, enumerable: false, configurable: false }
}
*/
배열 자료형임에도 꼭 생김새가 객체처럼 생김을 볼 수 있다. 다만, 인덱스를 프로퍼티 키로 갖고 있으며 length 프로퍼티를 갖는 특수한 객체임을 확인 할 수 있다. 즉, 자바스크립트 배열은 객체이며, 배열의 인덱스는 사실 프로퍼티 값 인 것이다.
이러한 특징 때문에 자바스크립트 배열은 요소를 위한 각각의 메모리 공간은 동일한 크기를 갖지 않아도 되며 연속적으로 이어져 있지 않을 수도 있게 된다. 위에서 배운 희소 배열(sparse array)가 대표적인 예이다.
조금 난이도 있는 설명을 하자면, 자바스크립트 배열인 우리가 알던 C언어나 Java와 같은 배열이 아닌 해시 테이블(Hash Table)로 구현된 객체이다. 해시 테이블은 각각의 키 값에 해시 함수를 적용해 배열의 고유한 인덱스를 생성하고, 이 인덱스를 활용해 값을 저장하거나 검색을 하는 자료구죠이다. 따라서 자바스크립트 배열은 키 값이 숫자인 해시 테이블로 볼 수 있다.
그래서 자바스크립트 배열은 C언어나 Java와 같은 일반적인 배열보다 접근 시간이 상대적으로 느리다. 일반적인 배열은 인덱스로 요소에 바로 접근 할 수 있지만, 해시 테이블은 키 값을 해시 함수에 넣어서 인덱스를 계산한 후에 접근하기 때문이다. 하지만 어디까지나 상대적으로 느릴뿐이지 여타 다른 자료구조에 비해 매우 빠른편에 속한다.
# 참고자료
http://tcpschool.com/javascript/js_array_application
https://poiemaweb.com/js-array-is-not-arrray
https://ko.javascript.info/array
https://blog.devgenius.io/arrays-and-array-in-javascript-345b4f87a232
https://dev.to/samanthaming/4-ways-to-convert-string-to-character-array-in-javascript-iij/comments
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.