[JavaScript] 클린코드 자바스크립트 : 배열
이 글은 유데미의 "클린코드 자바스크립트" 강의 내용(섹션 6: 배열 다루기)을 정리한 것이다.
1. 배열은 객체이다.
배열은 인덱스로 값에 접근한다. 하지만 [ ]에 키를 넣을 수도 있다. obj라는 키에 빈 객체를 할당해 보겠다.
const arr = [1, 2, 3]
arr["obj"] = {}
console.log(arr) // [ 1, 2, 3, obj: {} ]
console.log(arr[3]) // undefined
console.log(arr["obj"]) // {}
arr의 요소에 obj요소가 있는 것을 확인할 수 있다. 시각적으로 볼 때 인덱스 3번에 위치해 있는 것처럼 보이지만 arr[3]으로 접근하면 요소가 없는 것을 확인할 수 있다. 하지만 "obj"라는 키로 접근하면 {}라는 값에 접근이 가능하다. 물론 함수도 할당 가능하다.
arr["func"] = function () { return "array? object?" }
console.log(arr) // [ 1, 2, 3, obj: {}, func: [Function (anonymous)] ]
console.log(arr.func()) // 'array? object?'
2. Array.length는 인덱스가 있는 요소의 개수만 알려준다!
배열 요소의 개수를 확인할 때 Array.length를 사용한다. 나 역시 딱 그 정도로만 활용했었다. 그런데 재미난 기능을 숨기고 있었다.
const arr = [1, 2, 3]
arr.length = 10
// 빈 요소 7개가 생겨난 것을 확인할 수 있다.
console.log(arr) // [1, 2, 3, <7 empty items>]
위의 코드와 같이 arr.length에 정수 값을 할당하면 배열의 길이를 직접 지정할 수 있고 해당 길이만큼 비어있는 요소가 생성되는 것을 알 수 있다. 이번에는 키와 값을 가진 요소도 포함된 상태에서 진행해 보겠다.
// 아래와 같은 요소를 가지고 있는 배열 arr이 있다.
console.log(arr) // [ 1, 2, 3, obj: {}, func: [Function (anonymous)] ]
// 배열의 길이를 확인해 보면 3인 것을 확인할 수 있다.
console.log(arr.length) // 3
arr.length = 0
// 인덱스를 가진 값은 모두 사라졌다.
console.log(arr) // [ obj: {}, func: [Function (anonymous)] ]
// 키와 값을 가진 요소 외에 비어있는 요소 10개가 생겼다.
arr.length = 10
console.log(arr) // [ <10 empty items>, obj: {}, func: [Function (anonymous)] ]
이상의 코드를 보면 키와 값으로 되어있는 요소는 Array.length와 무관한 것을 알 수 있다. 정리하자면 Array.length에는 정수 값을 할당할 수 있고 할당된 정수 값에 따라 배열의 길이가 결정되기 때문에 인덱스를 가진 요소를 제거할 수도 있고 요소의 공간을 확보할 수 도 있다.
3. 배열 요소(element)에 접근할 때 [ ] 쓰지 않기
[ ]에 인덱스를 넣어 해당 요소에 접근할 수 있는데 이 같은 방식은 명시적이지 못하여 가독성을 떨어트릴 수 있다. 따라서 다음과 같은 방식을 사용한다면 코드를 읽는 이가 더 잘 이해할 수 있도록 도울 수 있다.
3.1. 구조분해 할당 활용
const phoneNumber = "010-1111-1111";
/**
* before : 인덱스로 접근
* 가독성이 좋지 못하다!
*/
const numArr = phoneNumber.split('-');
console.log(`이동 통신 번호 : ${numArr[0]}, 국번 : ${numArr[1]}, 개별 번호 : ${numArr[2]}`);
/**
* after : 구조 분해 할당
*/
const [firstNum, middleNum, lastNum] = phoneNumber.split('-');
console.log(`이동 통신 번호 : ${firstNum}, 국번 : ${middleNum}, 개별 번호 : ${lastNum}`);
3.2. 사용자 정의 함수 활용
아래의 코드는 배열의 첫 번째 요소만 사용하는 경우에 대한 예시이다.
// 첫 번째 요소만 사용하는 경우
/**
* before : 인덱스로 할당
* 배열 요소가 없을 경우 undefined 반환
*/
const firstNum = phoneNumber.split('-')[0];
/**
* after : 사용자 정의 함수 활용
* 배열 요소가 없을 경우 원하는 값 반환 가능
*/
const head = (arr) => arr[0] ?? '';
const firstNum = head(phoneNumber.split('-'));
4. 유사 배열 객체
4.1. 배열을 흉내낸 객체
객체를 배열 처럼 만들 수 있다. 하지만 배열이 아닌 객체이기 때문에 배열로 만들기 위해서는 Array.from()을 이용해 변환해야 한다.
const likeArr = {
0: 1,
1: 2,
2: 3,
length: 3,
};
console.log(likeArr[0]) // 1
console.log(likeArr.length) // 3
console.log(Array.isArray(likeArr)) // false
const Arr = Array.from(likeArr))
console.log(Array.isArray(Arr)) // true
4.2. 대표적인 유사 배열 객체 arguments
대표적인 유사 배열 객체로 arguments가 있다. arguments는 배열처럼 보이지만 배열이 아니다. 따라서 고차함수를 사용할 수 없다.
function func() {
return arguments
}
const args = func(1, 2, 3)
console.log(args[0]) // 1
console.log(args.length) // 3
console.log(Array.isArray(args)) // false
const argsArr = Array.from(args)
console.log(Array.isArray(argsArr)) // true
5. 불변성
5.1. 불변성이 유지되지 않는 경우
다음 코드는 불변성이 유지되지 않는 경우이다. 원본 배열의 값이 변경되면 복사본 배열의 값도 변경이된다.
const arr1 = [1, 2, 3]
const arr2 = arr1
console.log(arr2) // [1, 2, 3]
const arr1.push(4)
console.log(arr2) // [1, 2, 3, 4]
5.2. 불변성 유지 방법
- 배열을 복사한다(concat() 메서드, 전개 연산자 활용).
- 새로운 배열을 반환하는 메서드(고차 함수)를 활용한다.
6. for문 배열 고차 함수로 리팩터링
아래 코드는 for문을 map 메서드를 사용해 리팩터링한 예제이다.
const arrNums = [1, 3, 2];
// for문
function func1(arr) {
let temp = [];
for (let i = 0; i < arr.length; i++) {
temp.push(`${arr[i]}명`);
}
return temp;
}
const forResult = func1(arrNums);
console.log(forResult); // [ '1명', '3명', '2명' ]
// 고차 함수
function func2(arr) {
return arr.map((num) => `${num}명`);
}
const mapResult = func2(arrNums);
console.log(mapResult); // [ '1명', '3명', '2명' ]
7. 배열 메서드 체이닝 활용하기
배열 고차함수를 파이프라인 처럼 이어서 사용할 수 있다. 이 경우 for문을 이용할 때 보다 명시적이다.
const arrNums = [1, 3, 2];
const isOverFucn = (num) => num > 1;
const ascFunc = (a, b) => a - b;
const strFunc = (num) => `${num}명`;
function func(arr) {
// 1보다 큰 값을 필터, 오름차순 정렬, "명"을 붙여 문자열로 변환
return arr.filter(isOverFucn).sort(ascFunc).map(strFunc);
}
const rst = func(arrNums);
console.log(rst); // [ '2명', '3명' ]
8. map vs forEach
map과 forEach는 다음과 같은 차이가 있다.
- Array.prototype.map() : 매 요소마다 함수를 실행, 결과 값 반환, 새로운 배열 생성
- Array.prototype.forEach(): 매 요소마다 함수를 실행
아래 코드는 두 배열 메서드의 차이를 보여주는 간단한 예제이다. 코드 아래에 주석 처리된 부분은 실행 결과이다. 언뜻 보면 map과 forEach가 별 차이없게 보이지만 반환값에서 큰 차이를 보인다. map은 콜백 함수의 결과 값으로 새로운 배열을 반환하고, forEach는 콜백 함수의 리턴 값을 반환한다. 위 예제의 경우 콜백 함수는 리턴 값이 없으므로 undefined이다.
const arr = [1, 2, 3]
console.log(arr.map((el) => console.log(el)));
console.log(arr.forEach((el) => console.log(el)));
// 1
// 2
// 3
// [ undefined, undefined, undefined ]
// 1
// 2
// 3
// undefined
9. Continue & Break
for문에서는 if문과 함께 사용할 수 있는 제어문이다. 하지만 forEach 메서드에서는 사용할 수 없다. 배열 메서드에서 이 제어문을 사용할 수 있는 방법이 있는데 some 메서드를 이용하면 된다.
some 메서드는 순회하는 요소의 값이 하나라도 true이면 순회를 멈추고 true를 반환한다. 반대로 true가 나오지 않는 다면 모든 요소를 순회하고 결국 false를 반환한다. 이 점을 이용해 break하고 싶은 조건에서 true를 반환하면 순회를 중도에 멈출 수 있다. 아래는 예제 코드인데, 예제 1은 for of 문을 이용한 경우이고 예제 2는 some을 이용한 경우이다.
const arr = [1, 3, 4, 5];
// 예제 1
for (n of arr) {
if (n % 2 !== 0) {
continue;
}
console.log(n);
break;
}
// 예제 2
arr.some((n) => {
if (n % 2 === 0) {
console.log(n);
return true;
}
return false;
});
// 4
// 4
forEach에서는 Continue, Break를 문법적으로 지원하지 않는다. 이와 같은 효과를 구현하기 위해서는 다음의 방법을 사용할 수 있다.
- try catch를 이용하고 Continue, Break 대신 throw로 에러를 던지는 방법
- every(), some(), find(), findIndex()와 같은 메서드로 조기에 반복을 종료
😉 every와 some은 불리언 값을 반환한다. every는 &&연산자와 비슷해서 모든 요소가 true이면 true를 리턴하고, some은 ||연산자와 비슷해서 요소 중 하나라도 true이면 true를 반환한다. find와 findIndex는 특정 인덱스 요소를 찾으면 종료한다.