파트 1 - 4) 자료구조와 자료형 - 4) 배열

Lee·2021년 10월 28일
0

배열 파트 링크 : https://ko.javascript.info/array

순서가 있는 요소들을 저장하기에 객체만으로는 무리가 있다.
순서가 있는 컬렉션을 저장할 때는 '배열'이라는 자료구조를 쓴다.

배열 선언

배열은 대괄호와 new 생성자를 사용해서 선언할 수 있다.
대괄호를 이용해 선언할 때는 빈 대괄호를 사용할 수도 있고 대괄호 안에 초기 요소를 넣어줄 수도 있다.

<script>
  let arr = new Array();
  let arr = [];
  
  let fruits = ["사과", "오렌지", "자두"];
</script>

배열 안의 특정 요소를 얻거나 수정하거나 추가할 때'인덱스'를 사용할 수 있다.
대괄호 안에 인덱스를 넣어주면 된다!
fruits[0]; or fruits[1]='배';

문자열에서처럼 length를 이용하면 배열에 담긴 요소가 몇개인지 알아낼 수 있다.

<script>
  let fruits = ["사과", "오렌지", "자두"];

  alert( fruits.length ); // 3
</script>

alert에 배열명을 넘기면 그 배열 안에 든 모든 요소를 얻을 수 있다.

<script>
  let fruits = ["사과", "오렌지", "자두"];

  alert( fruits ); // 사과, 오렌지, 자두 출력
</script>

배열의 요소에는 자료형에 제약이 없다. (객체도 넣을 수 있다!)

<script>
  let arr = [ '사과', { name: '이보라' }, true, function() { alert('안녕하세요.'); } ];

  // 인덱스가 1인 요소(객체)의 name 프로퍼티를 출력
  alert( arr[1].name ); // 이보라

  // 인덱스가 3인 요소(함수)를 실행
  arr[3](); // 안녕하세요.
</script>

업무 중 주로 api로부터 사용자 정보를 받을 때 사용자 정보를 담은 객체들을 요소로 하는 배열 형태로 받았던 게 생각난다.
배열도 마찬가지로 trailing 쉼표(마지막 요소 끝에 찍는 쉼표)를 사용할 수 있다.


pop/push와 shift/unshift

이 내용은 초반에 큐(queue)와 스택(stack)에 대해 설명하고 있다.
큐와 스택은 배열을 사용해 구현할 수 있는 가장 대표적인 자료구조다!
큐는 선입선출, 스택은 후입선출 구조로 둘이 줄곧 헷갈렸었는데 큐는 발음처럼 '큐-> 한 큐에 뚫는다' 라고 생각한 뒤로는 헷갈리지 않는다.

큐의 push와 shift
push : 맨 끝에(마지막에) 요소를 추가한다.
shift : 제일 앞(처음) 요소를 꺼내 제거한 후 남아있는 요소들을 앞으로 밀어준다. 이렇게 하면 두 번째 요소가 첫 번째 요소가 된다.

배열에도 위의 두 연산을 가능하게 해주는 push와 pop 메서드가 있다!
더 재밌는 건 스택에서도 push와 pop을 사용한다!

스택의 push와 pop
push : 요소를 스택 끝에 집어넣는다.
pop : 스택 끝 요소를 추출한다.

스택은 '한쪽 끝'에 요소를 더하거나 뺄 수 있게 하는 자료구조다.
스택은 주로 카드뭉치로 설명된다고 한다.
쌓여있는 카드 맨 위에 새로운 카드를 더해주거나 빼는 것처럼 스택도 '한쪽 끝’에 요소를 집어넣거나 추출 할 수 있기 때문이다.
획기적인 설명이다.

위 설명을 마친 뒤에는 선입선출과 후입선출을 영어로도 알려준다.
선입선출(First-In-First-Out, FIFO)
후입선출(Last-In-First-Out, LIFO)

이렇게 처음이나 끝에 요소를 더하거나 빼주는 연산을 제공하는 자료구조를 과학 분야에선 데큐(deque, Double Ended Queue)라고 부른다.

이제 배열에서 pop, push, shift, unshift를 보자

1) pop '끝'
배열 끝의 요소를 제거하고, 제거한 요소를 반환한다.

<script>
  let fruits = ["사과", "오렌지", "배"];

  alert( fruits.pop() ); 
  // 배열에서 "배"를 제거하고 제거된 요소를 얼럿창에 띄운다. 즉, 배를 띄운다!

  alert( fruits ); // 사과,오렌지
</script>

2) push '끝'
배열 끝에 요소를 추가한다.

<script>
  let fruits = ["사과", "오렌지"];

  fruits.push("배");

  alert( fruits ); // 사과,오렌지,배
</script>

이렇다보니 push의 동작은 fruits[fruits.length] = 추가할 요소; 와 같은 효과를 보인다.

그리고 한가지 요소만 추가할 수 있는 건 아니다! 한 번에 여러가지 요소를 추가할 수도 있다. ^^
그냥 인자를 여러개 넣어주면 된다.
ex) fruits.push("오렌지", "배");

3) shift '앞'
배열 앞쪽 요소를 제거하고, 제거한 요소를 반환한다.

<script>
  let fruits = ["사과", "오렌지", "배"];

  alert( fruits.shift() ); 
  // 배열에서 "사과"를 제거하고 제거된 요소인 '사과'를 출력

  alert( fruits ); // 오렌지,배
</script>

4) unshift '앞'
배열 앞쪽에 요소를 추가한다.

<script>
  let fruits = ["오렌지", "배"];

  fruits.unshift('사과');

  alert( fruits ); // 사과,오렌지,배
</script>

unshift도 앞서 본 push와 마찬가지로 요소를 한번에 여러개 추가할 수 있다.
ex) fruits.unshift("파인애플", "레몬")

shift는 위처럼 여러개 요소를 한번에 삭제하려면 shift만으로는 불가능하고 반복문을 이용해야 한다.


배열의 내부 동작 원리

배열의 근본은 객체다.
다만 일반 객체와 차이점이 있다면 key가 어떤 문자가 아니라 숫자다.
따라서 배열은 원시형에 속하지 않고 객체형에 속하여 객체처럼 동작한다!

예를들면 배열은 객체와 마찬가지로 참조를 통해서 복사된다. 멋지다.

<script>
  let fruits = ["바나나"]

  let arr = fruits; 
  // fruits의 참조를 복사(두 변수가 같은 객체인 ["바나나"]를 참조한다)

  alert( arr === fruits ); // true

  arr.push("배"); // 참조를 이용해 배열을 수정

  alert( fruits ); 
  // 바나나,배 => arr은 fruits와 같은 객체를 참조하고 있기 때문에 arr을 수정하면 fruits도 수정되는 것이다.
</script>

배열을 배열답게 만들어주는 것은 특수 내부 표현방식이다.
자바스크립트 엔진은 배열의 요소를 인접한 메모리 공간에 차례로 저장해 연산 속도를 높인다. 이 방법 외에도 배열 관련 연산을 더 빠르게 해주는 최적화 기법은 다양하다.

단, 배열을 '순서가 있는 자료의 컬렉션’처럼 다루지 않고 일반 객체처럼 다루면 이런 기법들이 제대로 동작하지 않는다.

<script>
  let fruits = []; 

  fruits[99999] = 5; 
  // 배열의 길이보다 훨씬 큰 숫자를 사용해 프로퍼티를 만든다.

  fruits.age = 25; 
  // 임의의 이름을 사용해 프로퍼티를 만든다.
  //배열은 기본적으로 객체이므로 위처럼 원하는 프로퍼티를 추가해도 문제가 발생하지는 않는다.
</script>

하지만 위 예시처럼 코드를 작성하면 자바스크립트 엔진이 배열을 일반 객체처럼 다루게 되어 배열을 다룰 때만 적용되는 최적화 기법이 동작하지 않아 배열 특유의 이점이 사라진다. => 이게 핵심이다.

배열을 배열답게 쓰지 않고 일반 객체처럼 다루는 모습은 아래와 같다.

1) arr.test = 5 같이 숫자가 아닌 값을 프로퍼티 키로 사용
2) arr[0]과 arr[1000]만 추가하고 그사이에 아무런 요소도 없는 경우
3) arr[1000], arr[999]같이 요소를 역순으로 채우는 경우

배열에서 임의의 키를 사용해야 한다면 배열보단 일반 객체 {}가 적합한 자료구조일 확률이 높다고한다.
배열은 배열답게 쓰자!


성능

push와 pop은 빠르지만 shift와 unshift는 느리다.

그 shift와 unshift가 그냥 값을 뱉는 게 아니라 그 과정이 있기 때문이다.

shift 연산은 아래 3가지 동작을 모두 수행함으로써 완성된다.

1) 인덱스가 0인 요소(제일 앞의 요소)를 제거
2) 모든 요소를 왼쪽으로 이동시킨다. 이때 인덱스 1은 0, 2는 1로 변합니다.
3) length 프로퍼티 값을 갱신

배열에 요소가 많다면 요소가 이동하는 데 걸리는 시간이 길고 메모리 관련 연산도 많아진다.

unshift를 실행했을 때도 마찬가지다.
요소를 배열 앞에 추가하려면 기존 요소들을 오른쪽으로 이동시키고 빈 왼쪽 자리에 값을 넣는데, 이때 요소들의 인덱스도 바꿔줘야 한다.

반면 push나 pop은 이런 요소를 이동시키는 과정이 없다!
pop 메서드로 요소를 끝에서 제거하려면 마지막 요소를 제거하고 length 프로퍼티의 값을 줄여주기만 하면 되죠.

pop 메서드는 요소를 옮기지 않으므로 요소들이 기존 인덱스를 그대로 유지한다.
배열 끝에 무언가를 추가하거나 빼는 메서드(push, pop)의 실행 속도가 빠른 이유는 바로 여기에 있다.


반복문

배열을 순회할 때는 반복문과 인덱스를 사용하여 순회할 수 있다.

<script>
  let arr = ["사과", "오렌지", "배"];

  for (let i = 0; i < arr.length; i++) {
    alert( arr[i] );
  }
</script>

배열에 적용할 수 있는 또 다른 순회 문법으론 for..of가 있다.
자바에서 배웠던 개선된 for문이 생각난다.

<script>
  let fruits = ["사과", "오렌지", "자두"];

  // 배열 요소를 대상으로 반복 작업을 수행한다.
  for (let fruit of fruits) {
    alert( fruit ); //fruits 배열의 요소들을 fruit이라는 변수로 읽어온다.
  }
</script>

for..of를 사용하면 현재 요소의 인덱스는 얻을 수 없고 값만 얻을 수 있다.

배열은 객체형에 속하므로 for..in을 사용하는 것도 가능하다. 참 다재다능하다.

<script>
let arr = ["사과", "오렌지", "배"];

for (let key in arr) {
  alert( arr[key] ); // 사과, 오렌지, 배
}
</script>

하지만 for...in을 마음놓고 배열에 사용하기에는 문제가 있다!

1) for..in 반복문은 모든 프로퍼티를 대상으로 순회하여 키가 숫자가 아닌 프로퍼티도 순회 대상에 포함된다.

브라우저나 기타 호스트 환경에서 쓰이는 객체 중, 배열과 유사한 형태를 보이는 ‘유사 배열(array-like)’ 객체가 있다. 이런 유사 배열들은 키가 숫자가 아닌 프로퍼티와 메서드를 요소로 가질 수 있다.

2) for..in 반복문은 객체와 함께 사용할 때 최적화되어 있어서 배열에 사용하면 객체에 사용하는 것 대비 10~100배 정도 느리다. for..in 반복문의 속도가 대체로 빠른 편이기 때문에 병목 지점에서만 문제가 되긴 한다고는 하지만... 되도록 배열에는 for..in를 쓰지 말자!


‘length’ 프로퍼티

length 프로퍼티는 배열 내 요소의 개수가 아니라 가장 큰 인덱스에 1을 더한 값입니다.
배열의 어떤 요소의 인덱스가 아주 큰 정수라면 배열의 length 프로퍼티도 굉장히 큰 값을 갖게 된다.
따라서 아래 예시와 같이 배열을 사용하는 것은 옳지 않다.

<script>
  let fruits = [];
  fruits[123] = "사과";

  alert( fruits.length ); // 124
</script>

length 프로퍼티는 쓰기도 가능하다! 전혀 몰랐다.

length의 값을 수동으로 증가시키면 아무 일도 일어나지 않지만 감소시키면 배열이 잘린다.
짧아진 배열은 다시 되돌릴 수 없다.

<script>
  let arr = [1, 2, 3, 4, 5];

  arr.length = 2; 
  alert( arr ); // [1, 2] 배열에 2개의 요소만 남는다!

  arr.length = 5; // 배열을 원래 길이로 되돌림.
  alert( arr[3] ); // undefined 출력. 삭제된 기존 요소들이 복구되지 않는다.
</script>

이런 특징을 이용하면 arr.length = 0;을 사용해 아주 간단하게 배열을 비울 수 있다.


new Array()

숫자형 인수 하나를 넣어서 new Array를 호출하면 배열이 만들어지는데, 이 배열엔 요소가 없는 반면 길이는 인수와 같아진다. (생성할 배열의 크기를 인수를 통해 미리 정하는 것)

<script>
  let arr = new Array(2);

  alert( arr[0] ); //빈 배열을 만든다.

  alert( arr.length ); // 길이가 2인 빈 배열이 만들어졌다!
</script>

다차원 배열

처음에 다차원 배열을 배웠을 때 매번 헷갈려한 기억이 있다.
배열 요소의 자료형에는 제약이 없기 때문에 배열 또한 배열의 요소가 될 수 있다.
이런 배열을 다차원 배열(multidimensional array)이라고 부른다. 다차원 배열은 행렬을 저장하는 용도로 쓰인다.

<script>
  let matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
  ];

  alert( matrix[1][1] ); // 5, 중심에 있는 요소
  // 지금 다시 보니 1행의 1열을 의미하는군요!! 이제 확실히 알겠어요.
</script>

toString

배열엔 toString 메서드가 있고 이를 호출하면 요소를 쉼표로 구분한 문자열이 반환된다.

<script>
  let arr = [1, 2, 3];

  alert( arr ); // 1,2,3
  alert( String(arr) === '1,2,3' ); // true
  아래 예시를 실행해 봅시다.

  alert( [] + 1 ); // "1"
  alert( [1] + 1 ); // "11"
  alert( [1,2] + 1 ); // "1,21"
</script>

배열에는 Symbol.toPrimitive나 valueOf 메서드가 없어서 alert 함수에 넣으면 문자열로의 형 변환이 일어나고 []는 빈 문자열, [1]은 문자열 "1", [1,2]는 문자열 "1,2"로 변환된다.

이항 덧셈 연산자 "+"는 피연산자 중 하나가 문자열인 경우 나머지 피연산자도 문자열로 변환한다.
따라서 위 예시는 아래 예시와 동일하게 동작한다.

<script>
  alert( "" + 1 ); // "1"
  alert( "1" + 1 ); // "11"
  alert( "1,2" + 1 ); // "1,21"
</script>
profile
하고 싶은 게 너무 많습니다.

0개의 댓글

관련 채용 정보