thumbnail
27.배열
JavaScript
2022.03.22.

제로베이스 네카라쿠배 온라인스쿨 1기 과정(2021. 11 ~ 2021. 12)에서 공부했던 Javascript Deepdive 교재의 내용 요약 및 필자의 생각을 정리한 내용입니다. 원서의 내용 챕터의 일부를 요약한 내용으로 보다 자세한 내용은 원서를 참조 바랍니다.


27장 배열

27.1 배열이란?

  • 배열(Array)은 여러 개의 값을 순차적으로 나열한 자료구조다.
  • 배열은 사용 빈도가 매우 높은 가장 기본적인 자료구조다.
  • 배열이 갖고 있는 값을 요소(element)라고 부른다.
    • 자바스크립트의 모든 값은 배열의 요소가 될 수 있다.
  • 배열의 요소는 배열에서 자신의 위치를 나타내는 0 이상의 정수인 인덱스(index)를 갖는다.
    • 인덱스는 배열의 요소에 접근할 때 사용한다. (제로 베이스)
  • 요소에 접근할 때는 대괄호 표기법을 사용한다.
  • 배열은 요소의 개수(배열의 길이)를 나타내는 length 프로퍼티를 갖는다.
  • 자바스크립트에 배열이라는 타입은 존재하지 않으며 배열은 객체 타입이다.
  • 배열은 배열 리터럴, Array 생성자 함수, Array.of, Array.from 메서드로 생성할 수 있다.
    • 생성자 함수는 Array 이며, 프로토타입 객체는 Array.prototype이다.
    • Array.prototype은 배열을 위한 빌트인 메서드를 제공한다.
  • 배열은 객체지만 일반 객체와 달리 특징이 있다.
    • 인덱스와 요소로 구성되어 있다
    • 값의 참조는 인덱스로 한다.
    • 값의 순서가 있다.
    • length 프로퍼티가 있다.
  • 배열의 장점은 처음부터 순차적으로 요소에 접근할 수도 있고, 마지막부터 역순으로 요소에 접근할 수도 있다. (인덱스, length 프로퍼티 덕분에 가능)

27.2 자바스크립트 배열은 배열이 아니다.

  • 자료구조(Data Structure)에서 말하는 배열은 동일한 크기의 메모리 공간이 빈틈없이 연속적으로 나열된 자료구조를 말한다.
    • 하나의 데이터 타입으로 통일되어 있고 서로 연속적으로 인접해 있다. (밀집 배열)
  • 일반적인 의미의 배열은 각 요소가 동일한 데이터 크기를 가지며, 빈틈없이 연속적으로 이어져 있으므로 단 한번의 연산으로 임의의 요소에 접근할 수 있다.
  • 하지만 정렬되지 않은 배열에서 특정한 요소를 검색하는 경우, 배열의 모든 요소를 처음부터 특정 요소를 발견할 때까지 차례대로 검색해야 한다. (선형 검색)
  • 또한 배열에 요소를 삽입하거나 삭제하는 경우 배열의 요소를 연속적으로 유지하기 위해 요소를 이동시켜야 하는 단점도 있다.
  • 자바스크립트 배열은 배열의 요소를 위한 메모리 공간은 동일한 크기를 갖지 않아도 되며, 연속적으로 이어져 있지 않을수도 있다. 이러한 배열을 **희소 배열(sparse array)**이라 한다.
  • 자바스크립트 배열은 엄밀히 말해 일반적 의미의 배열이 아닌, 일반적인 배열의 동작을 흉내 낸 특수한 객체다.

일반적 배열과 자바스크립트 배열의 장단점

  • 일반적 배열은 인덱스로 요소에 빠르게 접근할 수 있다. 하지만 특정 요소를 검색하거나 요소를 삽입 또는 삭제하는 경우 효율적이지 않다.
  • 자바스크립트 배열은 해시 테이블로 구현된 객체이므로 인덱스로 요소에 접근하는 경우 일반적 배열보다 성능적인 면에서 느릴 수밖에 없는 구조적 단점이 있다.
    • 특정 요소를 검색하거나 요소를 삽입 또는 삭제하는 경우 일반적 배열보다 빠른 성능을 기대할 수 없다.
  • 자바스크립트 배열은 인덱스로 배열 요소에 접근하는 경우 일반적인 배열보다 느리지만 특정 요소를 검색하거나 요소를 삽입 또는 삭제하는 경우에는 일반적인 배열보다 빠르다.

27.3 length 프로퍼티와 희소 배열

  • length 프로퍼티는 요소의 개수, 즉 배열의 길이를 나타내는 0 이상의 정수를 값으로 갖는다.
  • 빈 배열일 경우 0이며, 빈 배열이 아닐 경우 가장 큰 인덱스에 +1을 더한 것과 같다.
  • length 프로퍼티 값은 0과 2의 32제곱 - 1(4,294,967,296 -1) 미만의 양의 정수다. 즉, 배열 요소를 최대 4,294,967,295개(약 42억 9천개) 가질수 있다.
  • length 프로퍼티 값은 배열에 요소를 추가하거나 삭제하면 자동 갱신된다.
  • length 프로퍼티 값은 요소 개수, 즉 배열의 길이를 바탕으로 결정되지만 임의의 숫자값을, 명시적으로 할당할 수도 있다.
    • 만약 length 프로퍼티 값보다 작은 숫자 값을 할당하면 배열의 길이가 줄어든다.
  • 주의할 점은 length 프로퍼티 값보다 큰 숫자값을 할당하는 경우다.
    • 이 때 length 프로퍼티값은 변경되지만 실제로 배열의 길이가 늘어나진 않는다.
const arr = [1]

arr.length = 3 // 3

console.log(arr) // [1, empty x 2]
  • 위 예제에서 출력된 empty x 2는 실제로 추가된 배열 요소가 아니며, arr[1], arr[2]에는 값이 존재하지 않는다.
  • 즉, 현재 length 프로퍼티 값보다 큰 숫자 값을 length 프로퍼티에 할당하는 경우 length 프로퍼티 값은 성공적으로 변경되지만 실제 배열에는 아무런 변함이 없다.
    • 값 없이 비어있는 요소를 위해 메모리 공간을 확보하지 않으며 빈 요소를 생성하지도 않는다.
  • 위와 같은 배열을 희소 배열이라고 하며, 자바스크립트는 희소 배열을 허용한다.
  • 일반적인 배열의 length는 배열 요소의 개수, 배열의 길이와 언제나 일치한다.
    • 하지만 희소 배열은 length와 배열 요소의 개수가 일치하지 않는다.
    • 희소 배열의 length는 희소 배열의 실제 요소 개수보다 언제나 크다.
  • 희소 배열은 사용하지 않는 것이 좋다.
    • 배열에는 같은 타입의 요소를 연속적으로 위치시키는 것이 최선이다.

27.4 배열 생성

27.4.1 배열 리터럴

  • 배열 리터럴은 0개 이상의 요소를 쉼표로 구분하여 대괄호([])로 묶는다. 배열 리터럴은 객체 리터럴과 달리 프로퍼티 키가 없고 값만 존재한다.

27.4.2 Array 생성자 함수

  • Object 생성자 함수를 통해 객체를 생성할 수 있듯이, Array 생성자 함수를 통해 배열을 생성할 수도 있다.
  • Array 생성자 함수는 전달된 인수의 개수에 따라 다르게 동작하므로 주의가 필요하다.
    • 전달된 인수가 1개고 숫자인 경우 length 프로퍼티 값이 인수인 배열을 생성한다.
    const arr = new Array(10)
    console.log(arr) // [empty x 10]
    console.log(arr.length) // 10
    • 이때 생성된 배열은 희소 배열이다. length 프로퍼티 값은 0이 아니지만 실제로 배열의 요소는 존재하지 않는다.
    • 전달된 인수가 배열 요소 허용 범위를 벗어나면 RangeError가 발생한다.
    • 전달된 인수가 없는 경우 빈 배열을 생성한다. (배열 리터럴과 동일)
    • 전달된 인수가 2개 이상이거나 숫자가 아닌 경우 인수를 요소로 갖는 배열을 생성한다.
  • Array 생성자 함수는 new 연산자와 함께 호출하지 않더라도(일반 함수로 호출해도) 배열을 생성하는 생성자 함수로 동작한다. (내부적으로 new.target을 확인하기 때문)

27.4.3 Array.of

  • ES6에서 도입된 Array.of 메서드는 전달된 인수를 요소로 갖는 배열을 생성한다. Array.of는 Array 생성자 함수와 다르게 전달된 인수가 1개이고 숫자이더라도 인수를 요소로 갖는 배열을 생성한다.
Array.of(1) // [1]

Array.of(1, 2, 3) // [1,2,3]

Array.of("string") // ['string']

27.4.4 Array.from

  • ES6에서 도입된 Array.from 메서드는 유사 배열 객체 또는 이터러블 객체를 인수로 전달받아 배열로 변환하여 반환한다.
// 유사 배열 객레를 변환하여 배열을 생성한다.

Array.from({ length: 2, 0: "a", 1: "b" }) // -> ['a', 'b']

// 이터러블을 변환하여 배열을 생성한다.
Array.from("Hello")
  • Array.from 을 사용하면 두 번째 인수로 전달한 콜백 함수를 통해 값을 만들면서 요소를 채울 수 있다.
  • Array.from 메서드는 두 번째 인수로 전달한 콜백 함수에 첫 번째 인수에 의해 생성된 배열의 요소값과 인덱스를 순차적으로 전달하면서 호출하고, 콜백 함수의 반환값으로 구성된 배열을 반환한다.
Array.from({ length: 3 }) // [undefined, undefined, undefined]

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

27.5 배열 요소의 참조

  • 배열의 요소를 참조할 때는 대괄호([]) 표기법을 사용한다. 대괄호 안에는 인덱스가 와야하며, 정수로 평가되는 표현식이라면 인덱스 대신 사용할 수 있다.
  • 존재하지 않은 요소에 접근하면 undefined가 반환된다.
  • 배열은 사실 인덱스를 나타내는 문자열을 프로퍼티 키로 갖는 객체다.

27.6 배열 요소의 추가와 갱신

  • 객체처럼 배열에도 요소를 동적으로 추가할 수 있다.
    • 존재하지 않는 인덱스를 사용해 값을 할당하면 새로운 요소가 추가된다.
    • length 프로퍼티는 자동 갱신된다.
  • 인덱스로 요소에 접근하여 명시적으로 값을 할당하지 않은 요소는 생성되지 않는다.
  • 이미 존재하는 요소에 값을 재할당하면 요소값이 갱신된다.
  • 인덱스는 요소의 위치를 나타내므로 반드시 0 이상의 정수를 사용해야 한다.
    • 만약 정수 이외의 값을 사용하면 요소가 생성되는 것이 아니라 프로퍼티가 생성된다.
      • 이때 추가된 프로퍼티는 length 프로퍼티 값에 영향을 주지 않는다.

27.7 배열 요소의 삭제

  • 배열의 특정 요소를 삭제하기 위해 delete 연산자를 사용할 수 있다.
  • 하지만 delete 연산자는 객체의 프로퍼티를 삭제하고, 희소 배열로 만들기 때문에 length 프로퍼티 값은 변하지 않으므로 delete연산자는 사용하지 않는 것이 좋다.
  • 희소 배열을 만들지 않으면서 배열의 특정 요소를 완전히 삭제하려면 Array.prototype.splicec 메서드를 사용한다.

27.8 배열 메서드

  • 배열 메서드는 결과물을 반환하는 패턴이 두 가지이다.
    • 원본 배열(배열 메서드를 호출한 배열, 배열 메서드의 구현체 내부에서 this가 가리키는 객체)을 직접 변경하는 메서드(mutator method)
    • 원본 배열을 직접 변경하지 않고 새로운 배열을 생성하여 반환하는 메서드(accessor method)

27.8.1 Array.isArray

  • Array 생성자 함수의 정적 메서드로 전달된 인수가 배열이면 true, 아니면 false를 반환한다.

27.8.2 Array.indexOf

  • 원본 배열에서 인수로 전달된 요소를 검색하여 인덱스를 반환한다.
    • 원본 배열에 인수로 전달한 요소와 중복되는 요소가 여러 개 있다면 첫 번째로 검색된 요소의 인덱스를 반환한다.
    • 원본 배열에 인수로 전달한 요소가 존재하지 않으면 -1을 반환한다.
  • indexOf 대신 Array.prototype.includes 메서드를 사용하면 가독성이 더 좋다.

27.8.3 Array.prototype.push

  • 인수로 전달받은 모든 값을 원본 배열의 마지막 요소로 추가하고 변경된 length 프로퍼티 값을 반환한다. (원본 배열을 직접 변경한다.)
  • 성능 면에서 좋지 않다. 추가할 요소가 하나 뿐이라면 push 메서드를 사용하지 않고 요소를 직접 추가하면 더 빠르다.
  • push 메서드는 원본 배열을 직접 변경하는 부수 효과가 있다. 따라서 push 메서드보단 스프레드 문법을 사용하는 편이 좋다.
const arr = [1, 2]

// ES6 스프레드 문법;
const newArr = [...arr, 3]
console.log(newArr)

27.8.4 Array.prototype.pop

  • 원본 배열에서 마지막 요소를 제거하고 제거한 요소를 반환한다.
  • 빈 배열이면 undefined를 반환한다.
  • 원본 배열을 직접 변경한다.
  • pop과 push 메서드를 사용하면 스택을 쉽게 구현할 수 있다.
const Stack = (function () {
  function Stack(array = []) {
    if (!Array.isArray(array)) {
      /// 47장 에러 처리 참고
      throw new TypeError(`${array} is not an array.`)
    }
    this.array = array
  }

  Stack.prototype = {
    constructor: Stack,

    push(value) {
      return this.array.push(value)
    },
    // 스택의 가장 마지막 데이터, 가장 나중에 밀어넣은 최신 데이터를 꺼낸다.
    pop() {
      return this.array.pop()
    },
    // 스택의 복사본 배열을 반환한다.
    entries() {
      return [...this.array]
    },
  }

  return Stack
})()

const stack = new Stack([1, 2])
console.log(stack.entries())

stack.push(3)
console.log(stack.entries())

stack.pop()
console.log(stack.entries())
class Stack {
  #array // private class member

  constructor(array = []) {
    if (!Array.isArray(array)) {
      throw new Error(`${array} is not an array`)
    }
    this.#array = array
  }

  // 스택의 가장 마지막에 있는 데이터를 밀어 넣는다.

  push(value) {
    return this.#array.push(value)
  }

  pop() {
    return this.#array.pop()
  }

  entries() {
    return [...this.$array]
  }
}

const stack = new Stack([1, 2])
console.log(stack.entries())

stack.push(3)
console.log(stack.entries())

stack.pop()
console.log(stack.entries())

27.8.5 Array.prototype.unshift

  • 인수로 전달받은 모든 값을 원본 배열의 선두에 요소로 추가하고 변경된 length 프로퍼티값을 반환한다.
  • 원본 배열을 직접 변경한다.

27.8.6 Array.prototype.shift

  • 원본 배열에서 첫 번째 요소를 제거하고 제거한 요소를 반환한다.

  • 원본 배열이 빈 배열이면 undefined를 반환한다.

  • 원본 배열을 직접 변경한다.

  • shift와 push 메서드를 사용하면 큐를 쉽게 구현할 수 있다.

const Queue = (function () {
  function Queue(array = []) {
    if (!Array.isArray(array)) {
      throw new TypeError(`${array} is not an array`)
    }
    this.array = array
  }

  Queue.prototype = {
    constructor: Queue,

    enqueue(value) {
      return this.array.push(value)
    },

    dequeue() {
      return this.array.shift()
    },

    entries() {
      return [...this.array]
    },
  }

  return Queue
})()

const queue = new Queue([1, 2])
console.log(queue.entries())

queue.enqueue(3)
console.log(queue.entries())

queue.dequeue()
console.log(queue.entries())
class Queue {
  #array

  constructor(array = []) {
    if (!Array.isArray(array)) {
      throw new TypeError(`${array} is not an array.`)
    }
    this.#array = array
  }

  enqueue(value) {
    return this.#array.push(value)
  }

  dequeue() {
    return this.#array.shift()
  }

  entires() {
    return [...this.#array]
  }
}

const queue = new Queue([1, 2])
console.log(queue.entries())

queue.enqueue(3)
console.log(queue.entries())

queue.dequeue()
console.log(queue.entries())

27.8.7 Array.prototype.concat

  • 인수로 전달된 값들을 원본 배열의 마지막 요소로 추가한 새로운 반환한다.
  • 인수로 전달한 값이 배열인 경우 배열을 해체하여 새로운 배열의 요소로 추가한다.
  • 원본 배열은 변경되지 않는다.
  • push와 unshift 메서드는 concat 메서드로 대체할 수 있다. (단 미묘한 동작 방식 차이가 있다.)
    • push와 unsfhit는 원본 배열을 직접 변경하지만 concat 메서드는 원본 배열을 변경하지 않고 새로운 배열을 반환한다.
    • 즉, push와 unshift는 반드시 원본 배열을 변수에 저장해 두어야하며, concat 메서드를 사용할 경우 반환 값을 변수에 할당 받아야 한다.
    • 인수로 전달받은 값이 배열인 경우, push와 unsfhit는 배열을 그대로 원본 배열의 마지막/첫 번째 요소로 추가하지만 concat 메서드는 인수로 전달받은 배열을 해체하여 새로운 배열의 마지막 요소로 추가한다.
  • concat 메서드는 ES6의 스프레드 문법으로 대체할 수 있다.

27.8.8 Array.prototype.splice

  • unsfhit, shift, push, pop 처럼 원본 배열을 변경한다.
  • 원본 배열의 중간에 요소를 추가하거나 중간에 있는 요소를 제거하는 경우 사횽한다.
  • splice 에는 3개의 매개변수가 있다.
    • start: 원본 배열의 요소를 제거하기 시작할 인덱스다. start만 지정하면 start부터 모든 요소를 제거하며, 음수인 경우 배열의 끝에서의 인덱스를 나타낸다. (-1이면 마지막 요소, -n이면 마지막에서 n번째 요소를 가리킨다.)
    • deleteCount : 원본 배열을 제거하기 시작할 인덱스인 start부터 제거할 요소의 개수다. (0은 아무것도 제거하지 않는다.)
    • items: 제거한 위치에 삽입할 요소들이다. 생략할 경우 원본 배열에서 요소들을 제거하기만 한다.
// 배열의 특정 요소 제거하기
const arr = [1, 2, 3, 1, 2]

// 배열 array에서 item 요소를 제거한다. item 요소가 여러 개 존재하면 첫 번째 요소만 제거한다.

function remove(array, item) {
  // 제거할 item 요소의 인덱스를 취득한다.
  const index = array.indexOf(item)

  // 제거할 item 요소가 있다면 제거한다.
  if (index !== -1) array.splice(index, 1)

  return array
}

console.log(remove(arr, 2))
console.log(remove(arr, 10))
  • filter 메서드를 사용해서 특정 요소를 제거할 수도 있지만 특정 요소가 중복되면 모두 제거된다.

27.8.9 Array.prototype.slice

  • slice 메서드는 인수로 전달된 범위의 요소들을 복사하여 배열로 반환한다.
  • 원본 배열은 변경되지 않는다.
  • slice 메서드는 두 개의 매개변수를 갖는다.
    • start : 복사를 시작할 인덱스다. 음수인 경우 배열의 끝에서 인덱스를 나타낸다.
      • slice(-2)는 배열의 마지막 두 개의 욕소를 복사하여 배열로 반환한다.
    • end : 복사를 종료할 인덱스다. 이 인덱스에 해당하는 요소는 복사되지 않는다.
      • 생략 가능하며, 생략 시 기본 값은 length 프로퍼티 값이다.
const arr = [1, 2, 3]

// arr[0]부터 arr[1] 이전(arr[1] 미포함)까지 복사하여 반환한다.
arr.slice(0, 1) // -> [1]

arr.slice(1, 2) // -> [2]

// 원본은 변경되지 않는다.
console.log(arr)
const arr = [1, 2, 3]

// 배열의 끝에서부터 요소를 한 개 복사하여 반환한다.
arr.slice(-1)

// 배열의 끝에서부터 요소를 두 개 복사하여 반환한다.

arr.slice(-2)
  • slice 메서드의 인수를 모두 생략하면 원본 배열의 복사본을 생성하여 반환한다.

    • 이때 생성된 복사본은 얕은 복사를 통해 생성된다.
  • slice 메서드가 복사본을 생성하는 것을 이용하여 arguments, HTMLCollection, NodeList 같은 유사 객체를 배열로 변환할 수 있다.

function sum() {
  // 유사 배열 객체를 배열로 변환(ES5);
  var arr = Array.prototype.slice.call(arguments)
  console.log(arr)

  return arr.reduce(function (pre, cur) {
    return pre + cur
  }, 0)
}

console.log(sum(1, 2, 3))
  • Array.from 을 사용하면 더욱 간단하게 유사 배열 객체를 배열로 변환할 수 있다.

27.8.10 Array.prototype.join

  • join 메서드는 원본 배열의 모든 요소를 문자열로 변환한 후, 인수로 전달받은 문자열, 즉 구분자(separator)로 연결한 문자열을 반환한다.
  • 구분자는 생략가능하며 기본 구분자는 콤마(’,‘)다.

27.8.11 Array.prototype.reverse

  • reverse 메서드는 원본 배열의 순서를 반대로 뒤집는다.
  • 원본 배열이 변경되며, 반환 값은 변경된 배열이다.

27.8.12 Array.prototype.fill

  • ES6에서 도입된 fill 메서드는 인수로 전달받은 값을 배열의 처음부터 끝까지 요소로 채운다.

  • 원본 배열이 변경된다.

  • 두 번째 인수로 요소 채우기를 시작할 인덱스를 전달할 수 있다.

  • 세 번째 인수로 요소 채우기를 멈출 인덱스를 전달할 수 있다.

  • fill 메서드를 사용하면 배열을 생성하면서 특정 값으로 요소를 채울 수 있다.

  • fill 메서드로 요소를 채울 경우 모든 요소를 하나의 값만으로 채울 수밖에 없는 단점이 있다.

    • Array.from 메서드를 사용하면 두 번째 인수로 전달한 콜백 함수를 통해 요소값을 만들면서 배열을 채울 수 있다.
    • Array.from 메서드는 두 번째 인수로 전달한 콜백 함수에 첫 번째 인수에 의해 생성된 배열의 요소 값과 인덱스를 순차적으로 전달하면서 호출하고, 콜백 함수의 반환값으로 구성된 배열을 반환한다.
    const sequences = (length = 0) => Array.from({ length }, (_, i) => i)
    
    console.log(sequences(3))

27.8.13 Array.prototype.includes

  • ES7 에서 도입된 includes 메서드는 배열 내에 특정 요소가 포함되어 있는지 확인하여 true 또는 false를 반환한다.
  • 첫 번째 인수로 검색할 대상을 지정한다.
  • 두 번째 인수로 검색을 시작할 인덱스를 전달할 수 있다. (기본값 0)
    • 만약 음수를 전달하면 length 프로퍼티 값과 음수 인덱스를 합산하여 검색 시작 인덱스를 설정한다.
  • indexOf 메서드를 사용해도 배열 내에 특정 요소가 포함되어 있는지 확인할 수 있지만, 반환값이 -1인지 확인해야 하고, NaN이 포함되어 있는지 확인할 수 없다는 문제가 있다.

27.8.14 Array.prototype.flat

  • ES10에서 도입된 flat 메서드는 인수로 전달한 깊이만큼 재귀적으로 배열을 평탄화한다.
;[1, [2, 3, 4, 5]].flat() // [1,2,3,4,5]
  • 중첩 배열을 평탄화할 깊이를 인수로 전달할 수 있다. (생략 시 기본값 1)
  • 인수로 Infinity를 전달하면 중첩 배열 모두를 평탄화한다.
;[1, [2, [3, [4]]]].flat() // -> [1,2,[3,[4]]]
;[1, [2, [3, [4]]]].flat(1) // -> [1,2,[3,[4]]]
;[1, [2, [3, [4]]]].flat(2) // [1,2,3,[4]]
;[1, [2, [3, [4]]]].flat().flat() // [1,2,3,[4]]
;[1, [2, [3, [4]]]].flat() // [1,2,3,4]

27.9 배열 고차 함수

  • 고차 함수(HOF, Higher-Order Function)은 함수를 인수로 전달받거나 함수를 반환하는 함수를 말한다.
  • 고차 함수는 외부 상태의 변경이나 가변 데이터를 피하고 **불변성(immutability)**을 지향하는 함수형 프로그래밍에 기반을 두고 있다.
  • 함수형 프로그래밍은 순수 함수와 보조 함수의 조합을 통해 로직 내에 존재하는 조건문과 반복문을 제거하여 복잡성을 해결하고 변수의 사용을 억제하여 상태 변경을 피하려는 프로그래밍 패러다임이다.

27.9.1 Array.prototype.sort

  • sort 메서드는 배열의 요소를 정렬한다.
  • 원본 배열을 직접 변경하며 정렬된 배열을 반환한다.
  • sort 메서드는 기본적으로 오름차순으로 요소를 정렬한다.
  • 내림차순으로 요소를 정렬하려면 sort 메서드를 사용하여 오름차순으로 정렬한 후 reverse 메서드를 사용하여 요소의 순서를 뒤집는다.
const fruits = ["Banana", "Orange", "Apple"]

// 오름차순(ascending) 정렬
fruits.sort()

// sort 메서드는 원본 배열을 직접 변경한다.

console.log(fruits) // ['Apple', 'Banana', 'Orange']

// 내림차순(descending) 정렬
fruits.reverse()

// reverse 메서드도 원본 배열을 직접 변경한다.
console.log(fruits)
  • 문자열 요소로 이루어진 배열의 정렬은 문제가 없지만, 숫자로 이루어진 배열을 정렬할 때는 주의가 필요하다.
const points = [40, 100, 1, 5, 2, 25, 10]

points.sort()

// 숫자 요소들로 이루어진 배열은 의도한 대로 정렬되지 않는다.
console.log(points) // [1, 10, 100, 2, 25, 40, 5]
  • sort 메서드의 기본 정렬 순서는 유니코드 코드 포인트의 순서를 따른다.
  • 배열의 요소가 숫자 타입이라 할지라도 배열의 요소를 일시적으로 문자열로 변환한 후 유니코드 코드 포인트의 순서를 기준으로 정렬한다.
  • 따라서 숫자 요소를 정렬할 때는 sort 메서드에 정렬 순서를 정의하는 비교 함수를 인수로 전달해야한다.
    • 비교 함수는 양수, 음수 또는 0을 반환해야 한다.
    • 비교 함수의 반환 값이 0보다 작으면 함수 첫 번째 인수를 우선하여 정렬, 0일 땐 정렬하지 않으며, 0보다 크면 두 번째 인수를 우선하여 정렬한다.
const points = [40, 100, 1, 5, 2, 25, 10]

// 숫자 배열의 오름차순 정렬, 비교 함수의 반환값이 0보다 작으면 a를 우선하여 정렬한다.
points.sort((a, b) => a - b)
console.log(points) // [1,2,5,10,25,40,100]

// 숫자 배열에서 최소/최대값 획득
console.log(points[0], points[points.length - 1])

// 숫자 배열의 내림차순 정렬, 비교 함수의 반환값이 0보다 작으면 b를 우선하여 정렬한다.
points.sort((a, b) => b - a)
console.log(points) // [100,40, 25, 10, 5, 2, 1];

// 숫자 배열에서 최소/최대값 취득
console.log(points[(points.length - 1, points[0])])
  • 객체를 요소로 갖는 배열을 정렬하는 예제는 다음과 같다.
const todos = [
  { id: 4, content: "JavaScript" },
  { id: 1, content: "HTML" },
  { id: 2, content: "CSS" },
]

// 비교 함수, 매개변수 key는 프로퍼티 키다.

function compare(key) {
  // 프로퍼티 값이 문자열인 경우 - 산술 연산으로 비교하면 NaN이 나오므로 비교 연산을 사용한다.
  // 비교 함수는 양수/음수/0을 반환하면 되므로 - 산술 연산 대신 비교 연산을 사용할 수 있다.
  return (a, b) => (a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0)
}

// id를 기준으로 오름차순 정렬
todos.sort(compare("id"))
console.log(todos)

// content를 기준으로 오름차순 정렬
todos.sort(compare("content"))
console.log(todos)

27.9.2 Array.prototype.forEach

  • forEach는 for 문을 대체할 수 있는 고차 함수다. forEach 메서드는 자신의 내부에서 반복문을 실행한다.
  • 즉 forEach 메서드는 반복문을 추상화한 고차 함수로서 내부에서 반복문을 통해 자신을 호출한 배열을 순회하면서 수행해야 할 처리를 콜백 함수로 전달받아 반복 호출한다.
const numbers = [1, 2, 3]
const pows = []

// forEach 메서드는 numbers 배열의 모든 요소를 순회하면서 콜백 함수를 반복 호출한다.
numbers.forEach(item => pows.push(item ** 2))
console.log(pows)
  • forEach 메서드의 콜백 함수는 forEach 메서드를 호출한 배열의 요소값과 인덱스, forEach 메서드를 호출한 배열 자체, 즉 this를 순차적으로 전달받을 수 있다.
;[1, 2, 3].forEach((item, index, arr) => {
  console.log(`요소값: ${item}, 인덱스: ${index}, this: ${JSON.stringify(arr)}`)
})

/* 요소값: 1, 인덱스: 0, this: [1,2,3]
요소값: 2, 인덱스: 1, this: [1,2,3]
요소값: 3, 인덱스: 2, this: [1,2,3]
*/

JSON.stringify 메서드

  • JSON.stringify 메서드는 객체를 JSON 포맷의 문자열로 변환한다.

  • forEach 메서드는 원본 배열을 변경하지 않는다. 하지만 콜백 함수를 통해 원본 배열을 변경할 수는 있다.

const numbers = [1, 2, 3]

// forEach 메서드는 원본 배열을 변경하지 않지만 콜백 함수를 통해 원본 배열을 변경할 수는 있다.
// 콜백 함수의 세 번째 매개변수 arr은 원본 배열 numbers를 가리킨다.
// 따라서 콜백 함수의 세 번째 매개변수 arr을 직접 변경하면 원본 배열 numbers가 변경된다.

numbers.forEach((item, index, arr) => {
  arr[index] = item ** 2
})

console.log(numbers)
  • forEach 메서드 반환값은 undefined다

  • forEach 메서드의 두 번째 인수로 forEach 메서드의 콜백 함수 내부에서 this로 사용할 객체를 전달할 수 있다.

  • forEach 메서드의 콜백 함수 내부의 this와 multiply 메서드 내부의 this를 일치시키려면 forEach 메서드의 두 번째 인수로 forEach 메서드의 콜백 함수 내부에서 this로 사용할 객체를 전달한다.

class Numbers {
  numberArray = []

  multiply(arr) {
    arr.forEach(function (item) {
      this.numberArray.push(item * item)
    }, this)
  }
}

const numbers = new Numbers()
numbers.multiply([1, 2, 3])
console.log(numbers.numberArray)
  • 더 나은 방법은 ES6의 화살표 함수를 사용하는 것이다. 화살표 함수는 함수 자체의 this 바인딩을 갖지 않는다. 따라서 화살표 함수 내부에서 this를 참조하면 상위 스코프, multiply 메서드 내부의 this를 그대로 참조한다.
class Numbers {
  numberArray = []

  multiply(arr) {
    // 화살표 함수 내부에서 this를 참조하면 상위 스코프의 this를 그대로 참조한다.
    arr.forEach(item => this.numberArray.push(item * item))
  }
}

const numbers = new Numbers()
numbers.multiply([1, 2, 3])
console.log(numbers.numberArray)
  • forEach 메서드도 내부에서는 반복문을 통해 배열을 순회할 수 밖에 없다.

    • 단, 반복문을 메서드 내부로 은닉하여 로직의 흐름을 이해하기 쉽게 하고 복잡성을 해결한다.
  • forEach 메서드는 for문과는 달리 break, continue 문을 사용할 수 없다. 다시 말해, 배열의 모든 요소를 빠짐없이 모두 순회하며 중간에 순회를 중단할 수 없다.

  • 희소 배열의 경우 존재하지 않는 요소는 순회 대상에서 제외된다.

  • forEach 메서드는 성능이 좋지는 않지만 가독성은 더 좋다. 따라서 높은 성능이 필요한 경우가 아니라면 for문 대신 forEach 메서드를 사용할 것을 권장한다.

27.9.3 Array.prototype.map

  • map 메서드는 자신을 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출한다.
    • 콜백 함수의 반환값들로 구성된 새로운 배열을 반환한다.
    • 원본 배열은 변경되지 않는다.
  • map 메서드는 콜백 함수의 반환값들로 구성된 새로운 배열을 반환한다.
    • 요소값을 다른 값으로 매핑한 새로운 배열을 생성하기 위한 고차 함수다.
  • map 메서드가 생성하여 반환하는 새로운 배열의 length 프로퍼티 값은 map 메서드를 호출한 배열의 length 프로퍼티 값과 반드시 일치한다.
    • map 메서드를 호출한 배열과 map 메서드가 생성하여 반환한 배열은 1:1 매핑한다.

27.9.4 Array.prototype.filter

  • filter 메서드는 자신을 호출한 배열의 모든 요소를 순회하면서 인수로 전달받은 콜백 함수를 반복 호출한다.
    • 콜백 함수의 반환값이 true인 요소로만 구성된 새로운 배열을 반환한다.
  • filter 메서드가 생성하여 반환한 새로운 배열의 length 프로퍼티 값은 filter 메서드를 호출한 배열의 length 프로퍼티 값과 작거나 같다.
  • filter 메서드는 자신을 호출한 배열에서 특정 요소를 제거하기 위해 사용할 수도 있다.
class Users {
  constructor() {
    this.users = [
      { id: 1, name: "Seo" },
      { id: 2, name: "Kim" },
    ]
  }

  // 요소 추출
  findById(id) {
    return this.users.filter(user => user.id === id)
  }

  remove(id) {
    return this.users.filter(user => user.id !== id)
  }
}

const users = new Users()

let user = users.findById(1)
console.log(user)

// id가 1인 사용자를 제거한다.
users.remove(1)

user = users.findById(1)
console.log(user)
  • filter 메서드를 사용해 특정 요소를 제거할 경우 특정 요소가 중복되어 있다면 중복된 요소가 모두 제거된다.
  • 특정 요소 하나만 제거하려면 indexOf 메서드를 통해 특정 요소의 인덱스를 취급한 다음 splice 메서드를 사용한다.

27.9.5 Array.prototype.reduce

  • 자신을 호출한 배열을 모든 요소를 순회하며 인수로 전달받은 콜백 함수를 반복 호출한다.
  • 콜백 함수의 반환값을 다음 순회 시에 콜백 함수의 첫 번째 인수로 전달하면서 콜백 함수를 호출하여 하나의 결과값을 만들어 반환한다.
  • 첫 번째 인수로 콜백 함수, 두 번째 인수로 초기값을 전달받는다.
  • reduce 메서드의 콜백 함수에는 4개의 인수를 받는다.
    • 초기값 또는 콜백 함수의 이전 반환값,
    • reduce 메서드를 호출한 배열의 요소값
    • 인덱스
    • reduce 메서드를 호출한 배열 자체, this
// 1부터 4까지 누적을 구한다.

const sum = [1, 2, 3, 4].reduce(
  (accumulator, currentValue, index, array) => accumulator + currentValue,
  0,
)

console.log(sum)
  • 위 코드에서 reducer 메서드의 콜백 함수는 4개의 인술르 전달받아 배열의 length 만큼 총 4회 호출된다.
  • 콜백함수의 반환값과 두 번째 요소값을 콜백 함수의 인수로 전달하면서 호출한다.
    • reduce 메서드는 하나의 결과값을 반환한다.
  • reduce 메서드는 자신을 호출한 배열의 모든 요소를 순회하며 하나의 결과값을 구해야 하는 경우 사용한다.

평균 구하기

const values = [1, 2, 3, 4, 5, 6]

const average = values.reduce((acc, cur, i, { length }) => {
  // 마지막 순회가 아니면 누적값을 반환하고 마지막 순회면 누적값으로 평균을 구해 반환한다.
  return i === length - 1 ? (acc + cur) / length : acc + cur
}, 0)

console.log(average)

최대값 구하기

const values = [1, 2, 3, 4, 5]

const max = values.reduce((acc, cur) => (acc > cur ? acc : cur), 0)
console.log(max)

요소의 중복 횟수 구하기

const fruits = ["banana", "apple", "orange", "orange", "apple"]

const count = fruits.reduce((acc, cur) => {
  // 첫 번째 순회 시 acc는 초기값인 {}dlrh cur은 첫 번째 요소인 'banana'다
  // 초기값으로 전달받은 빈 객체 요소값인 cur을 프로퍼티 키로, 요소의 개수를 프로퍼티 값으로 할당한다.
  // 만약 프로퍼티 값이 undefined임녀 프로퍼티 값을 1로 초기화한다.

  acc[cur] = (acc[cur] || 0) + 1
  return acc
}, {})

console.log(count)

배열 평탄화 하기

const values = [1, [2, 3], 4, [5, 6]]

const flatten = values.reduce((acc, cur) => acc.concat(cur), [])

console.log(flatten)
  • flat 메서드를 활용하는 것이 더 직관적이다.

중복 요소 제거

const values = [1, 2, 1, 3, 5, 4, 5, 3, 4, 4]

const result = values.reduce((acc, cur, i, arr) => {
  // 순회 중인 요소의 인덱스가 자신의 인덱스라면, 처음 순회하는 요소다.
  // 이 요소만 초기값으로 전달받은 배열에 담아 반환한다.
  // 순회 중인 요소의 인덱스가 자신의 인덱스가 아니라면, 중복된 요소다.

  if (arr.indexOf(cur) === i) acc.push(cur)
  return acc
}, [])

console.log(result)
  • filter를 사용하는 것이 직관적이다.
  • 중복 요소 제거시 Set을 사용할 수도 있다.
  • 모든 배열의 고차 함수는 reduce 메서드로 구현할 수 있다.
  • 두 번째 인수인 초기값이 옵션이므로 생략할 수 있지만, 언제나 초기값을 전달하는 것이 안전하다.

27.9.6 Array.prototype.some

  • some 메서드는 콜백 함수의 반환값이 단 한번이라도 참이면 true, 모두 거짓이면 false를 반환한다.
  • 배열의 요소 중에 콜백 함수를 통해 정의한 조건을 만족하는 요소가 1개 이상 존재하는지 확인하여 그 결과를 불리언 타입으로 반환한다.
  • 단, some 메서드를 호출한 배열이 빈 배열인 경우 언제나 false를 반환한다.

27.9.7 Array.prototype.every

  • 콜백 함수의 반환값이 모두 참이면 true, 단 한번이라도 거짓이면 false를 반환한다.
  • 즉, 배열의 모든 요소가 콜백 함수를 통해 정의한 조건을 모두 만족하는지 확인하여 그 결과를 불리언 타입으로 반환한다.
    • 단, 빈 배열인 경우 언제나 true를 반환한다.

27.9.8 Array.prototype.find

  • 인수로 전달된 콜백 함수를 호출하여 반환값이 true인 첫 번째 요소를 반환한다.
  • 반환값이 true인 요소가 존재하지 않는다면 undefined를 반환한다.
  • find의 결과값은 배열이 아닌 해당 요소값이다.

27.9.9 Array.prototype.findIndex

  • 인수로 전달된 콜백함수를 호출하여 반환값이 true인 첫 번째 요소의 인덱스를 반환한다.
  • 콜백 함수의 반환값이 true인 요소가 존재하지 않으면 -1을 반환한다.

27.9.10 Array.prototype.flatMap

  • map 메서를 통해 생성된 새로운 배열을 평탄화한다.
  • 즉, map과 flat 메서드를 순차적으로 실행하는 효과가 있다.
  • flatMap 메서드는 깊이를 지정할 수 없이 1단계만 평탄화한다.

© 2022 Developer Abel, Powered By Gatsby.