Frontend/JS.info 정리

자료구조와 자료형 - 배열

Creative_Lee 2022. 1. 24. 20:00

개발을 진행하다 보면 순서가 있는 컬렉션이 필요할 때가 있습니다! 

객체를 사용하면 순서와 관련된 메소드가 없어 편리하지 않습니다.

객체는 태생이 순서를 고려하지 않고 만들어진 자료구조이기 때문에,

새로운 프로퍼티를 생성해 기존 프로퍼티 사이에 끼워넣는 행동도 불가능 합니다.

 

이럴 때! 배열을 사용합니다.

 

 

배열 기초 

배열 요소의 자료형에는 제약이 없습니다!

let friend = ['도현' , { 친구1 : '태훈'}, '재원', '태오']

console.log(friend[1].친구1) // 태훈

Array에 object를 넣을 수도 있다!

 


 

큐( queue )와  스택( stack ) 

 

 

큐는 먼저 집어넣은 요소가 먼저 나오게 되어있는 자료구조 입니다.

이와 같은 구조를 선입선출 ( First-In-First-Out ) 이라고 부릅니다.

 

큐에서 사용하는 주요 연산은 2가지 입니다.

 

push : 맨 끝에 요소를 추가합니다. 

shift : 제일 앞에 요소를 제거하고 남은 요소를 한칸씩 앞으로 당겨줍니다. ( 2번째 요소가 1번째가 됩니다. )

 

 

배열 메소드

배열에는 큐의  2가지 연산을 가능하게 해주는 push, shift 메소드가 있습니다!

 

push

배열의 맨 끝에 해당 요소를 추가합니다.

arr[ arr.length ] = '...' 과 같습니다. 

여러 요소를 한 번에 추가하는 것도 가능합니다.

let friend = ['도현' , '태훈', '재원', '태오']

friend.push('준호')

console.log(friend) //['도현', '태훈', '재원', '태오', '준호']

 

shift

배열의 맨 앞에 요소를 제거하고 해당 요소를 리턴합니다.

let friend = ['도현' , '태훈', '재원', '태오']

console.log(friend.shift()) // 도현

console.log(friend) //['태훈', '재원', '태오']

 

자매품 unshift

배열의 맨 앞에 요소를 추가하고 모든 요소를 한 칸씩 뒤로 미룹니다.

여러 요소를 한 번에 추가하는 것도 가능합니다.

let friend = ['도현' , '태훈', '재원', '태오']

console.log(friend.unshift('준호')) 

console.log(friend) // ['준호', '도현', '태훈', '재원', '태오']

 

 

 

스택

스택은 가장 나중에 집어넣은 요소가 먼저 나오게 되어있는 자료구조 입니다.

이와 같은 구조를 후입선출 ( Last-In-First-Out ) 이라고 부릅니다.

 

스택에서 사용하는 주요 연산은 2가지 입니다.

 

push : 맨 끝에 요소를 추가합니다. 

pop : 맨 끝에 요소를 추출합니다.

 

 

배열 메소드

배열에는 스택의  2가지 연산을 가능하게 해주는 push, pop 메소드가 있습니다!

 

pop

배열의 맨 끝에 요소를 제거하고 해당 요소를 리턴합니다.

let friend = ['도현' , '태훈', '재원', '태오']

console.log(friend.pop()) // 태오

console.log(friend) //['도현', '태훈', '재원']

 

 

자바스크립트 배열을 사용하면 큐와 스택을 만들 수 있습니다.

자료구조들은 배열의 처음이나 끝에 요소를 추가,제거 하는데 사용됩니다.

 

이렇게 처음이나 끝에 요소를 더하거나 빼주는 연산을 제공하는 자료구조를

CS 에선 데큐 (deque, Double Ended Queue)라고 부릅니다.

 

 


배열의 내부 동작 원리

 

배열은 특별한 종류의 객체 입니다.

arr의 요소를 arr[0] 처럼 대괄호로 접근하는 방식도 객체 문법에서 왔습니다.

배열은 키가 숫자라는 점만 다릅니다!

 

배열은 자바스크립트의 7가지 원시 자료형에 속하지 않고, 객체형에 속하기 때문에 객체처럼 동작합니다.

 

배열의 참조 복사

배열은 객체와 마찬가지로 참조를 통한 복사를 합니다.

때문에 복사본에서 수정한 0번 요소가 원본에서도 수정되었습니다.

let friend = ['도현' , '태훈', '재원', '태오']

let clone = friend
clone[0] = '도현이는 죽었다'

console.log(clone) // ['도현이는 죽었다' , '태훈', '재원', '태오']
console.log(friend) // ['도현이는 죽었다' , '태훈', '재원', '태오']

console.log(clone === friend) // true

 

 

배열을 배열로 다루지 않고 객체 처럼 다뤄버린다면?

 

배열은 특수 내부 표현방식과 여러 최적화 기법을 통해
배열의 요소를 인접한 메모리 공간에 차례로 저장해 연산 속도를 높힙니다.

 

그런데 이 같은 배열을 순서가 있는 자료의 컬렉션 처럼 다루지 않고 그저 객체 처럼 다뤄버리면

이런 기법들이 제대로 동작하지 않습니다.

let friend = [];

friend[0] = '도현'
friend[999] = '태훈'
위와 같이 요소를 순서대로 차곡차곡 생성하지 않고 공백을 많이 만들거나,

friend.num2 = '태오'
위와 같이 인덱스 번호가 아닌 다른값을 사용해서 프로퍼티를 생성한다거나,
요소를 역순으로 채워버리는 등 잘못된 방법으로 배열을 사용하면 

console.log(friend) // (1000) ['도현', empty × 998, '태훈', num2: '태오']
최적화 기법이 동작하지 않고 배열의 이점이 사라집니다.
위처럼 임의의 키를 사용해야 한다면 객체를 사용하는 것이 맞습니다.

 

 

성능

본론부터 말해서 push, pop은 빠르지만  shift, unshift는 느립니다.

 

이유는 다음과 같습니다.

 

배열의 앞 부분에 뭔가를 해주는 메소드인 shift와 unshift를 사용할 땐

단순히 인덱스 넘버가 0인 값을 수정하는 것으로 동작이 끝나지 않습니다.

원래 있던 모든 요소들의 인덱스 넘버를 앞으로 당기거나 밀어내야 하는 작업도 수행해야 합니다

이후 length 값을 갱신하는 것 까지요!

 

그에 비해

배열에 뒷 부분에 뭔가르 해주는 메소드인 pop, push를 사용할 땐

아주 단순히 마지막 요소만 추가,삭제 해주면 됩니다.

이후 바로 length 값을 초기화 하면 작업이 끝나죠!

인덱스 넘버를 수정하는 거창한 작업은 거치지 않아도 되죠.

 

이게 바로 두 종류의 메소드 사용시 속도 차이가 나는 이유 입니다.

 


 

for ..of 

배열에서 사용할 순회 문법으로는 for ..of 문이 적합합니다.

for ..of 문을 사용하면 배열의 요소 값으로 반복적인 작업을 할 수 있습니다.

let numbers = [1, 2, 3, 4, 5]

for(let number of numbers) {
  console.log(number)
}

 

 

배열에서의 for ..in ??

 

배열도 객체형이기 때문에 for ..in문을 사용할 수 있지만,,,

let numbers = [1, 2, 3, 4, 5]

for(let index in numbers) {
  console.log(index)
}

2가지 문제점이 생깁니다.

 

1. for ..in 문은 모든 프로퍼티를 대상으로 순회합니다.

 

키가 숫자가 아닌 경우에도 순회 대상에 포함된다는 말입니다.

이러한 특성이 유사 배열 객체와 만나면 문제가 발생합니다.

유사 배열에는 length 프로퍼티와 요소 인덱스 번호도 들어 있습니다.

또한 배열과 달리 키가 숫자가 아닌 프로퍼티나 메소드가 있을 수 있습니다.

이런 경우에는 for ..in문이 필요없는 프로퍼티까지도 순회해 버려서 문제가 발생합니다!

 

 

2. for ..in 문은 객체에서 사용할 때 최적화 되어있습니다.

 

이로인해 배열에서 for ..in 문을 사용할 때 객체에서 사용할 때 대비 10 ~ 100배 정도 느립니다.

 

 

위와 같은 문제점을 파악했다면, 앞으로는

 

배열에선 for ..of 문을 사용하고

객체에선 for ..in 문을 사용합시다.

 


 

length

length 프로퍼티는 배열에 조작이 있을 때 마다 갱신됩니다!

length 는 배열 내 요소의 갯수가 아닌 , 가장 큰 인덱스 값에 1을 더한 값입니다.

 

배열에 요소가 하나 있을 때 그 요소의 인덱스 번호가 매우 크다면  length 프로퍼티도 같이 커지게 되는 것입니다.

하지만 이렇게 배열을 사용하지 않아야 합니다!!!

 

 

length는 쓰기가 가능합니다.

length의 값을 증가시키면 length는 증가합니다.

length의 값을 감소시키면 배열이 length에 맞게 잘려 나갑니다.

짧아진 배열의 length를 다시 증가시킬수 있지만 사라진 요소들은 돌아오지 않습니다.

이러한 특징을 이용하여 arr.length = 0 으로 배열을 비울 수 있습니다!

let numbers = [1, 2, 3, 4, 5]
numbers.length = 3

console.log(numbers) // (3) [1, 2, 3]

numbers.length = 5
console.log(numbers) // (5) [1, 2, 3, empty × 2]
사라진 4,5 ...

 

 


다차원 배열

배열은 배열의 요소가 될 수 있습니다.

이러한 배열을 다차원 배열 이라고 부릅니다.

다차원 배열은 행렬을 저장하는 용도로 쓰입니다.

let blocks = [
  [1,0,0],
  [1,0,0],
  [1,1,1],
]

console.log(blocks[0][0]) // 1

 

toString( )

배열에는 toString 메서드가 구현되어 있습니다.

호출 시 요소를 쉼표로 구분한 문자열이 리턴됩니다.

let idList = [ 1001, 1020, 5510, 2425];

console.log(idList.toString()); // 1001,1020,5510,2425

 

 

배열의 형 변환

배열에는 Symbol.toPrimitive 나 valueOf 메소드가 없기 때문에 다음 예시에서 배열은 문자열로 변환됩니다.

console.log( [] + 1 ); // "1" 
빈 문자열 '' + '1'

console.log( [1] + 1 ); // "11"
'1' + '1'

console.log( [1,2] + 1 ); // "1,21"
'1,2' + '1'

 

 

 

 

기본이 중요하다!