Computer >> 컴퓨터 >  >> 프로그래밍 >> JavaScript

JavaScript에서 두 속성을 기준으로 배열 요소 그룹화하는 방법

문제 상황

다음과 같은 객체 배열이 있다고 가정해 보겠습니다.

const arr = [
    { value: 12, gap: 1 },
    { value: 13, gap: 1 },
    { value: 14, gap: 1 },
    { value: 15, gap: 1 },
    { value: 19, gap: 2 },
    { value: 21, gap: 1 },
    { value: 22, gap: 1 },
    { value: 23, gap: 1 },
    { value: 27, gap: 1 },
    { value: 31, gap: 4 },
    { value: 35, gap: 4 },
    { value: 39, gap: 4 },
    { value: 43, gap: 1 },
    { value: 50, gap: 1 },
    { value: 51, gap: 1 },
    { value: 52, gap: 1 },
    { value: 55, gap: 1 },
    { value: 57, gap: 1 },
];

여기서 우리가 작성해야 할 함수는 이 배열을 입력받아 연속된 객체의 value 값이 바로 앞 객체의 value와 gap의 합과 일치하는 경우, 해당 객체들을 하나의 하위 배열로 묶어 새로운 배열을 반환하는 것입니다.

예를 들어, value가 12인 객체는 gap이 1이고 다음 객체의 value가 13이므로 두 객체는 같은 그룹으로 묶입니다. 이어지는 value 14, 15 역시 같은 규칙을 만족하므로 함께 그룹화됩니다.

해결 방법: reduce() 활용

문제를 이해했으니 코드를 작성해 보겠습니다. Array.prototype.reduce() 메서드를 사용하면 누적값(accumulator)을 관리하면서 원하는 형태의 배열을 깔끔하게 만들 수 있습니다.

핵심 아이디어는 다음과 같습니다.

  • 누적 데이터(data)와 마지막으로 그룹에 추가된 하위 배열의 인덱스(currentIndex)를 함께 추적합니다.
  • 현재 객체가 첫 번째 요소이거나, 앞 객체의 value + gap과 일치하지 않으면 새로운 하위 배열을 생성합니다.
  • 규칙을 만족하는 경우에는 현재 currentIndex가 가리키는 하위 배열에 객체를 추가합니다.

예제 코드

const arr = [
    { value: 12, gap: 1 },
    { value: 13, gap: 1 },
    { value: 14, gap: 1 },
    { value: 15, gap: 1 },
    { value: 19, gap: 2 },
    { value: 21, gap: 1 },
    { value: 22, gap: 1 },
    { value: 23, gap: 1 },
    { value: 27, gap: 1 },
    { value: 31, gap: 4 },
    { value: 35, gap: 4 },
    { value: 39, gap: 4 },
    { value: 43, gap: 1 },
    { value: 50, gap: 1 },
    { value: 51, gap: 1 },
    { value: 52, gap: 1 },
    { value: 55, gap: 1 },
    { value: 57, gap: 1 },
];
const groupArray = arr => {
    return arr.reduce((acc, val, ind, array) => {
        // 누적 데이터와 마지막 하위 배열의 인덱스
        const { data, currentIndex } = acc;
        // 현재 객체의 속성
        const { value, gap } = val;
        // 이전 객체의 속성 (옵셔널 체이닝으로 안전하게 접근)
        const v = arr[ind-1]?.value;
        const g = arr[ind-1]?.gap;
        if(ind === 0 || value !== v + g){
            // 새로운 하위 배열을 생성하고 해당 인덱스를 기록
            const index = data.push([val]) - 1;
            return { data, currentIndex: index };
        };
        // 연속 조건을 만족하면 기존 그룹에 추가
        data[currentIndex].push(val);
        return { data, currentIndex };
    }, {
        data: [],
        currentIndex: 0
    }).data;
}
console.log(groupArray(arr));

실행 결과

콘솔에 출력되는 결과는 다음과 같습니다.

[
    [
        { value: 12, gap: 1 },
        { value: 13, gap: 1 },
        { value: 14, gap: 1 },
        { value: 15, gap: 1 }
    ],
    [
        { value: 19, gap: 2 },
        { value: 21, gap: 1 },
        { value: 22, gap: 1 },
        { value: 23, gap: 1 }
    ],
    [ { value: 27, gap: 1 } ],
    [
        { value: 31, gap: 4 },
        { value: 35, gap: 4 },
        { value: 39, gap: 4 },
        { value: 43, gap: 1 }
    ],
    [
        { value: 50, gap: 1 },
        { value: 51, gap: 1 },
        { value: 52, gap: 1 }
    ],
    [ { value: 55, gap: 1 } ],
    [ { value: 57, gap: 1 } ]
]

코드 설명

출력 결과를 보면 예상대로 동작하는 것을 확인할 수 있습니다. 예를 들어 value 19인 객체는 gap이 2이고 다음 객체의 value가 21(19 + 2)이므로 같은 그룹에 포함됩니다. 반면 value 27 다음의 객체는 value 31로 28이 아니기 때문에 새로운 그룹이 시작됩니다.

또한 arr[ind-1]?.value처럼 옵셔널 체이닝(optional chaining)을 사용하면 첫 번째 요소 처리 시 이전 요소가 존재하지 않아도 에러 없이 안전하게 undefined를 반환받을 수 있습니다.

이 방식은 reduce()를 한 번만 순회하므로 시간 복잡도가 O(n)으로 효율적이며, 데이터 양이 많은 경우에도 성능 저하 없이 그룹화를 수행할 수 있습니다.