목록 데이터를 특정 기준으로 묶어서 보여줄 일이 자주 생긴다. 주문 내역을 상태별로, 할 일을 날짜별로, 상품을 카테고리별로. 매번 reduce로 짜자니 손이 아프고, 그렇다고 이거 하나 때문에 lodash를 깔기도 애매했다.
예전엔 reduce로 짰다
reduce로 그룹핑하는 코드는 이제 손에 익었지만, 매번 초기값 {}를 넣고 누산기를 만지는 게 은근히 번거롭다.
const grouped = orders.reduce((acc, order) => {
(acc[order.status] ??= []).push(order);
return acc;
}, {});
동작은 하는데, 처음 보는 사람은 ??=랑 push가 한 줄에 섞여 있어서 한 번 더 읽게 된다.
Object.groupBy()
Object.groupBy(배열, 콜백) 한 줄이면 끝이다. 콜백이 반환한 값을 키로 삼아 알아서 배열로 묶어준다.
const orders = [
{ id: 1, status: 'paid', amount: 12000 },
{ id: 2, status: 'pending', amount: 8000 },
{ id: 3, status: 'paid', amount: 30000 },
];
const byStatus = Object.groupBy(orders, (order) => order.status);
// {
// paid: [{ id: 1, ... }, { id: 3, ... }],
// pending: [{ id: 2, ... }],
// }
콜백에서 조건을 계산해 그룹 이름을 직접 만들어도 된다.
const byPrice = Object.groupBy(orders, (o) =>
o.amount >= 20000 ? 'high' : 'low'
);
반환값은 프로토타입이 없는 순수 객체(null 프로토타입)라, byStatus.hasOwnProperty 같은 게 없다. 대신 paid, constructor 같은 이름을 키로 써도 기존 메서드랑 안 부딪힌다는 장점이 있다.
키 순서가 중요하면 Map.groupBy()
객체 키가 숫자 문자열이면 자바스크립트가 오름차순으로 정렬해버린다. 넣은 순서를 지켜야 한다면 Map.groupBy()를 쓰면 된다. 사용법은 똑같고 결과가 Map으로 나온다.
const byMonth = Map.groupBy(orders, (o) => getMonth(o.date));
for (const [month, list] of byMonth) {
console.log(month, list.length);
}
객체 키로 그룹핑하고 싶을 때도 Map이라 문제없다. 예를 들어 사용자 객체 자체를 키로 쓸 수 있다.
지원 범위
두 메서드 모두 2024년에 Baseline에 들어왔다. 최신 크롬·사파리·파이어폭스에서 다 돌아가고, Node도 21부터 지원한다. 오래된 환경까지 챙겨야 한다면 core-js 폴리필로 메꾸면 된다.
reduce 그룹핑 스니펫을 복붙하던 습관을 이걸로 바꿨다. 코드가 짧아지는 것보다, groupBy라는 이름이 코드에 바로 보이니까 나중에 읽을 때 의도가 한눈에 들어오는 게 더 좋았다. 정렬된 키가 필요하면 Object, 순서나 객체 키가 필요하면 Map — 이 정도만 기억하면 된다.