thenullpage.com

배열 메서드는 이름이 워낙 친절해서 문서를 안 봐도 될 것 같다. 거른다, 바꾼다, 찾는다, 합친다. 나도 그렇게 감으로 갖다 쓰다가 목록 화면에서만 사고를 여러 번 쳤다. 문제는 항상 이름이 알려주지 않는 쪽에 있었다. 원본을 건드리는지, 개수를 유지하는지, 중간에 멈출 수 있는지 같은 것들이다.

조회수 순 정렬이 엉망으로 나와서 데이터부터 의심했다. 서버 응답을 열 번쯤 들여다본 뒤에야 범인이 한 줄짜리 정렬 코드인 걸 알았다.


const views = [12, 5, 130, 9];
console.log(views.sort()); // [12, 130, 5, 9], 130이 5보다 앞에 있다


sort는 비교 함수를 안 넘기면 원소를 전부 문자열로 바꿔서 사전순으로 줄을 세운다. "12"와 "130"은 첫 글자가 1이라 "5"보다 앞자리를 차지한다. 숫자로 정렬하려면 두 값을 어떻게 비교할지 직접 알려줘야 한다.


console.log(views.sort((a, b) => b - a)); // [130, 12, 9, 5], 내림차순
console.log(views); // [130, 12, 9, 5], 원본도 같이 바뀌어 있다


여기서 함정이 하나 더 튀어나온다. sort는 새 배열을 주는 게 아니라 원본을 그 자리에서 갈아엎는다. 인기순 목록을 따로 만들었더니 원래 순서로 보여주던 최신순 목록까지 뒤집혀 있었다. 원본을 지키고 싶으면 toSorted를 쓰면 된다. 정렬 결과를 새 배열로 돌려주고 원본은 그대로 둔다. reversesplice도 원본을 부수는 쪽이라 각각 toReversed, toSpliced가 짝으로 있다.


const ranked = views.toSorted((a, b) => a - b);
console.log(ranked); // [5, 9, 12, 130]
console.log(views); // [130, 12, 9, 5], 원본은 안전하다

forEach 안에 break를 넣었더니 에러가 났고, return으로 바꿨더니 조용히 끝까지 다 돌았다. 아이디를 찾으면 바로 멈추게 하려던 코드였다.


const ids = ["a1", "b2", "c3", "d4"];
ids.forEach((id) => {
console.log("검사", id);
if (id === "b2") {
return; // 멈추려고 넣었다
}
});
// 검사 a1
// 검사 b2
// 검사 c3
// 검사 d4, 찾고 나서도 끝까지 돈다


forEach는 원소마다 함수를 한 번씩 불러주는 게 전부라서 중간에 세울 방법이 없다. break는 반복문 문법이지 함수 안에서 쓰는 말이 아니라 SyntaxError가 나고, return은 그 회차 함수 하나만 끝내니 결과적으로 continue처럼 동작한다. 찾는 순간 멈춰야 하는 일이면 애초에 멈출 줄 아는 메서드를 써야 한다.


const target = ids.find((id) => id === "b2");
console.log(target); // b2, 처음 걸린 원소를 주고 순회를 끝낸다
console.log(ids.some((id) => id === "z9")); // false, 하나도 없으면 끝까지 보고 false


find는 조건에 맞는 첫 원소, some은 하나라도 있는지 여부, every는 전부 만족하는지를 준다. 셋 다 답이 정해지는 순간 순회를 멈춘다.

map으로 걸러내려다 배열에 구멍이 뚫렸다. 재고 있는 상품 이름만 뽑으려고 조건을 콜백 안에 넣었다.


const products = [
{ name: "노트북", stock: 3 },
{ name: "마우스", stock: 0 },
{ name: "키보드", stock: 12 },
];
const names = products.map((p) => {
if (p.stock > 0) return p.name;
});
console.log(names); // ["노트북", undefined, "키보드"]
console.log(names.length); // 3, 개수는 줄지 않는다


map은 넣은 개수만큼 그대로 돌려주는 메서드다. 조건에 안 맞아 아무것도 반환하지 않으면 그 자리가 비는 게 아니라 undefined가 박힌다. 화면에는 빈 줄이 하나 생겼고, 목록 개수는 여전히 3으로 찍혔다. 거르는 일은 filter가 하고, 모양을 바꾸는 일은 map이 한다.


const namesFixed = products
.filter((p) => p.stock > 0)
.map((p) => p.name);
console.log(namesFixed); // ["노트북", "키보드"]

reduce는 초기값을 빼먹은 날에만 이빨을 드러낸다. 장바구니 합계를 구하는 코드에서 숫자 대신 이상한 문자열이 나왔다.


const cart = [{ price: 1000 }, { price: 2500 }];
const sum = cart.reduce((acc, item) => acc + item.price);
console.log(sum); // [object Object]2500


초기값을 안 넘기면 첫 원소가 누적값 자리에 그대로 들어가고 순회는 두 번째 원소부터 시작한다. 그래서 acc에는 숫자가 아니라 { price: 1000 } 객체가 앉아 있었고, 객체와 숫자를 더하니 문자열 이어붙이기가 됐다. 빈 배열은 더 사납다. 누적값으로 삼을 첫 원소조차 없어서 TypeError: Reduce of empty array with no initial value로 터진다. 초기값 하나만 넣으면 둘 다 사라진다.


const sumFixed = cart.reduce((acc, item) => acc + item.price, 0);
console.log(sumFixed); // 3500
console.log([].reduce((acc, n) => acc + n, 0)); // 0, 빈 배열도 조용히 넘어간다

includes로 회원을 찾다가 있는 사람을 없다고 답했다. 눈으로 보면 분명 같은 값인데 false가 나왔다.


const members = [{ id: 7 }, { id: 9 }];
console.log(members.includes({ id: 7 })); // false
console.log(members.some((m) => m.id === 7)); // true
console.log(members.findIndex((m) => m.id === 9)); // 1


includesindexOf는 원소를 ===로 비교한다. 객체는 생김새가 아니라 메모리 주소로 비교되니 속이 똑같아도 새로 만든 객체는 남남이다. 숫자나 문자 배열이면 includes가 제일 편하고, 객체가 들어 있는 배열은 무엇을 기준으로 같다고 볼지 내가 직접 적어주는 some이나 find, findIndex 쪽이 맞다.

돌아보면 이 사고들은 배열 메서드를 몰라서 난 게 아니었다. 이름만 보고 나머지를 마음대로 상상해서 났다. 그래서 배열을 만질 때 네 가지를 먼저 본다. 이 메서드가 원본을 건드리는지, 결과 개수가 입력과 같은지, 중간에 멈출 수 있는지, 원소를 무엇으로 비교하는지. 목록 화면 버그는 태반이 이 넷 중 하나였다.