JavaScript 자료구조와 자료형 - 배열

Leejunyoung·2022년 7월 25일
0

JavaScript

목록 보기
34/49

오늘은 자바스크립트의 자료구조와 자료형 파트의 배열에 대해 공부하겠습니다.

배열

키를 사용해 식별할 수 있는 값을 담은 컬렉션은 객체라는 자료구조를 이용해 저장하는데, 객체만으로도 다양한 작업을 할 수 있다.

그런데 개발을 진행하다 보면 첫 번째 요소, 두 번째 요소, 세 번째 요소 등과 같이 순서가 있는 컬렉션이 필요할 때가 생기곤 한다. 사용자나 물건, HTML 요소 목록 같이 일목요연하게 순서를 만들어 정렬하기 위해서이다.

순서가 있는 컬렉션을 다뤄야 할 때 객체를 사용하면 순서와 관련된 메서드가 없어 그다지 편리하지 않다.
객체는 태생이 순서를 고려하지 않고 만들어진 자료구조이기 때문에 객체를 이용하면 새로운 프로퍼티를 기존 프로퍼티 '사이에' 끼워 넣는 것도 불가능하다.

이럴 땐 순서가 있는 컬렉션을 저장할 때 쓰는 자료구조인 배열을 사용할 수 있다.


배열 선언

아래 두 문법을 사용하면 빈 배열을 만들 수 있다.

대부분 두 번째 방법으로 배열을 선언하는데, 이때 대괄호 안에 초기 요소를 넣어주는 것도 가능하다.

각 배열 요소엔 0부터 시작하는 숫자(인덱스)가 매겨져 있다. 이 숫자들은 배열 내 순서를 나타낸다.

배열 내 특정 요소를 얻고 싶다면 대괄호 안에 순서를 나타내는 숫자인 인덱스를 넣어주면 된다.

같은 방법으로 요소를 수정할 수 있다.

새로운 요소를 배열에 추가하는 것도 가능하다.

length를 사용하면 배열에 담긴 요소가 몇 개인지 알아낼 수 있다.

alert를 사용해 요소 전체를 출력하는 것도 가능하다.

배열 요소의 자료형에 제약이 없다.


TMI - trailing 쉼표

배열의 마지막 요소는 객체와 마찬가지로 쉼표로 끝날 수 있다.

trailing(길게 늘어지는) 쉼표를 사용하면 모든 줄의 생김새가 유사해지기 때문에 요소를 넣거나 빼기가 쉬워진다.


pop·push와 shift·unshift

큐(queue)는 배열을 사용해 만들 수 있는 대표적인 자료구조로, 배열과 마찬가지로 순서가 있는 컬렉션을 저장하는 데 사용한다. 큐에서 사용하는 주요 연산은 아래와 같다.

배열엔 두 연산을 가능케 해주는 내장 메서드 push와 pop이 있다.

화면에 순차적으로 띄울 메시지를 비축해 놓을 자료 구조를 만들 때 큐를 사용하는 것처럼 큐는 실무에서 상당히 자주 쓰이는 자료구조이다.

배열은 큐 이외에 스택(stack)이라 불리는 자료구조를 구현할 때도 쓰인다.

스택에서 사용하는 연산은 아래와 같다.

  • push - 요소를 스택 끝에 집어넣는다.
  • pop - 스택 끝 요소를 추출한다.
  • 스택은 이처럼 '한쪽 끝'에 요소를 더하거나 뺄 수 있게 해주는 자료구조이다.

    스택은 흔히 카드 한 벌과 비교된다. 쌓여있는 카드 맨 위에 새로운 카드를 더해주거나 빼는 것처럼 스택도 '한쪽 끝'에 요소를 집어넣거나 추출 할 수 있기 때문이다.

    스택을 사용하면 가장 나중에 집어넣은 요소가 먼저 나온다. 이런 특징을 줄여서 후입선출(Last-In-First-Out, LIFO)이라고 부른다. 반면 큐를 사용하면 먼저 집어넣은 요소가 먼저 나오기 때문에 큐는 선입선출(First-In-First-Out, FIFO)자료 구조라고 부른다.

    자바스크립트 배열을 사용하면 큐와 스택 둘 다를 만들 수 있다. 이 자료구조들은 배열의 처음이나 끝에 요소를 더하거나 빼는 데 사용된다.

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

    아래는 배열 끝에 무언가를 해주는 메서드이다.

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

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

    fruits.push(...)를 호출하는 것은 fruits[fruits.length] = ... 하는 것과 같은 효과를 보인다.

    아래는 배열 앞에 무언가를 해주는 메서드이다.

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

    unshift
    배열 앞에 요소를 추가한다.

    push와 unshift는 요소 여러 개를 한 번에 더해줄 수도 있다.


    배열의 내부 동작 원리

    배열은 특별한 종류의 객체이다. 배열 arr의 요소를 arr[0] 처럼 대괄호를 사용해 접근하는 방식은 객체 문법에서 나왔다. 다만 배열은 키가 숫자라는 점만 다르다.

    숫자형 키를 사용함으로써 배열은 객체 기본 성능 이외에도 순서가 있는 컬렉션을 제어하게 해주는 특별한 메서드를 제공한다. length라는 프로퍼티도 제공한다.
    그렇지만 어쨋든 배열의 본질은 객체이다.

    이렇게 배열은 자바스크립트의 일곱가지 원시 자료형에 해당하지 않고, 원시 자료형이 아닌 객체형에 속하기 때문에 객체처럼 동작한다.

    예시를 하나 살펴보자. 배열은 객체와 마찬가지로 참조를 통해 복사된다.

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

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

    배열은 객체이므로 예시처럼 원하는 프로퍼티를 추가해도 문제가 발생하지 않는다.

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

    잘못된 방법의 예는 다음과 같다.

  • arr.test = 5 같이 숫자가 아닌 값을 프로퍼티 키로 사용하는 경우
  • arr[0]과 arr[1000]만 추가하고 그 사이에 아무런 요소도 없는 경우
  • arr[1000], arr[999]같이 요소를 역순으로 채우는 경우
  • 배열은 순서가 있는 자료를 저장하는 용도로 만들어진 특수한 자료구조이다. 배열 내장 메서드들은 이런 용도에 맞게 만들어졌다. 자바스크립트 엔진은 이런 특성을 고려하여 배열을 신중하게 조정하고, 처리하므로 배열을 사용할 땐 이런 목적에 맞게 사용해야한다. 임의의 키를 사용해야 한다면 배열보단 일반 객체 {}가 적합한 자료구조일 확률이 높다고 한다.


    성능

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

    배열 앞에 무언가를 해주는 메서드가 배열 끝에 무언가를 해주는 메서드보다 느린 이유를 실행 흐름을 살펴보면서 알아보자.

    shift 메서드를 호출한 것과 동일한 효과를 보려면 인덱스가 0인 요소를 제거하는 것만으론 충분하지 않다. 제거 대상이 아닌 나머지 요소들의 인덱스를 수정해 줘야한다.

    shift 연산은 아래 3가지 동작을 모두 수행해야 이뤄진다.

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

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

    unshift를 실행했을 때도 이와 유사한 일이 일어난다. 요소를 배열 앞에 추가하려면 일단 기존 요소들을 오른쪽으로 이동시켜야 하는데, 이때 인덱스도 바꿔줘야한다.

    그렇다면 push나 pop은 어떨까 이 둘은 요소 이동을 수반하지 않는다. pop 메서드로 요소를 끝에서 제거하려면 마지막 요소를 제거하고 length 프로퍼티의 값을 줄여주기만 하면된다.

    pop 메서드를 호출하면 다음과 같은 동작이 일어난다.

    pop 메서드는 요소를 옮기지 않으므로 각 요소는 기존 인덱스를 그대로 유지한다. 배열 끝에 무언가를 해주는 메서드의 실행 속도가 빠른 이유는 바로 여기에 있다.

    push 메서드를 쓸 때도 유사한 동작이 일어나므로 속도가 빠르다.


    반복문

    for문은 배열을 순회할 때 쓰는 가장 오래된 방법이다. 순회시엔 인덱스를 사용한다.

    배열에 적용할 수 있는 또 다른 순회 문법으론 for..of가 있다.

    for..of를 사용하면 현재 요소의 인덱스는 얻을 수 없고 값만 얻을 수 있다. 이 정도 기능이면 원하는 것을 충분히 구현할 수 있고 문법도 짧기 때문에 배열의 요소를 대상으로 충분히 구현할 수 있고 문법도 짧기 때문에 배열의 요소를 대상으로 반복 작업 할 땐 for..of를 사용하는 것이 좋다.

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

    그런데 for..in은 다음과 같은 특징을 지니기 때문에 for..in을 사용하면 문제가 발생하므로 되도록 다른 반복문을 사용하는것이 좋다.

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

    브라우저나 기타 호스트 환경에서 쓰이는 객체 중, 배열과 유사한 형태를 보이는 '유사 배열(array-like)' 객체가 있다. 유사 배열 객체엔 배열처럼 length 프로퍼티도 있고 요소마다 인덱스도 붙어 있다. 그런데 여기에 더하여 유사 배열 객체엔 배열과는 달리 키가 숫자형이 아닌 프로퍼티와 메서드가 있을 수 있다. 유사 배열 객체와 for..in을 함께 사용하면 이 모든 것을 대상으로 순회가 이뤄진다. 따라서 '필요 없는' 프로퍼티들이 문제를 일으킬 가능성이 생긴다.

    1. for..in 반복문은 배열이 아니라 객체와 함께 사용할 때 최적화 되어 있어서 배열에 사용하면 객체에 사용하는 것 대비 10~100배 정도 느리다. for..in 반복문의 속도가 대채로 빠른 편이기 때문에 병목 지점에서만 문제가 되긴 한다만, for..in 반복문을 사용할 땐 이런 차이를 알고 적절한 곳에 사용하는 것이 좋다.

    'length' 프로퍼티

    배열에 무언가 조작을 가하면 length 프로퍼티가 자동으로 갱신된다. length 프로퍼티는 배열 내 요소의 개수가 아니라 가장 큰 인덱스에 1을 더한 값이다.

    따라서 배열에 요소가 하나 있고, 이 요소의 인덱스가 아주 큰 정수라면 배열의 length 프로퍼티도 아주 커진다.

    배열을 이렇게 사용하지 않도록 하자.

    length 프로퍼티의 또 다른 독특한 특징 중 하나는 쓰기가 가능하다는 점이다.

    length의 값을 수동으로 증가시키면 아무 일도 일어나지 않는다.
    그런데 값을 감소시키면 배열이 잘린다. 짧아진 배열은 다시 되돌릴 수 없다.
    예시를 통해 살펴보자.

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


    new Array()

    위에서도 잠시 언급했던 new Array() 문법을 사용해도 배열을 만들 수 있다.

    대괄호 []를 사용하면 더 짧은 문법으로 배열을 만들 수 있기 때문에 new Array()는 잘 사용되지 않는 편이다. new Array()엔 다루기 까다로운 기능도 있어서 더욱더 그렇다.

    숫자형 인수 하나를 넣어서 new Array를 호출하면 배열이 만들어 지는데, 이 배열엔 요소가 없는 반면 길이는 인수와 같아진다.

    예시를 통해 new Array()의 이런 특징이 어떻게 실수를 유발할 수 있는지 알아보자.

    위 예시에서 확인해 본 것처럼 new Array(number)를 이용해 만든 배열의 요소는 모두 undefined이다.

    이런 뜻밖의 상황을 마주치지 않기 위해 new Array의 기능을 잘 알지 않는 한 대부분의 개발자가 대괄호를 써서 배열을 만든다.


    다차원 배열

    배열 역시 배열의 요소가 될 수 있다. 이런 배열을 가리키는 말로 '다차원 배열(multidimensional array)'이라고 부른다. 다차원 배열은 행렬을 저장하는 용도로 쓰인다.


    toString

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

    아래 예시를 실행해보자.

    배열엔 Symbol.toPrimitive나 valueOf 메서드가 없다. 따라서 위 예시에선 문자열로의 형변환이 일어나 []는 빈문자열, [1]은 문자열, "1", [1,2]는 문자열 "1,2"로 변환된다.

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

    profile
    안녕하세요

    0개의 댓글