실무에서는 숫자만 있는 문자열과 알파벳·숫자가 조합된 문자열이 하나의 배열에 섞여 있는 경우를 종종 마주하게 됩니다. 이런 배열을 단순히 sort() 메서드로 정렬하면 사전순(lexicographic)으로 처리되어 'A10'이 'A3'보다 앞에 오는 등 의도와 다른 결과가 나옵니다. 예를 들어 다음과 같은 배열이 있다고 가정해 보겠습니다.
const arr = ['A1', 'A10', 'A11', 'A12', 'A3A', 'A3B', 'A3', 'A4', 'B10', 'B2', 'F1', '1', '2', 'F3'];
우리는 이 배열을 유일한 인자로 받아 다음 규칙에 따라 제자리(in-place)에서 정렬하는 JavaScript 함수를 작성해야 합니다.
- 숫자만 포함된 문자열은 가장 앞에 위치하며, 오름차순으로 정렬됩니다.
- 알파벳과 숫자가 결합된 문자열은 먼저 알파벳 순서대로, 그다음에는 숫자 크기순(오름차순)으로 정렬됩니다.
따라서 최종 출력 결과는 다음과 같아야 합니다.
const output = ['1', '2', 'A1', 'A3', 'A3A', 'A3B', 'A4', 'A10', 'A11', 'A12', 'B2', 'B10', 'F1', 'F3'];
정렬 함수 구현 예제
아래 코드는 문자열을 숫자 조각과 비숫자 조각으로 분리한 뒤, 조각 단위로 비교하여 위 규칙을 그대로 구현한 것입니다.
const arr = ['A1', 'A10', 'A11', 'A12', 'A3A', 'A3B', 'A3', 'A4', 'B10', 'B2', 'F1', '1', '2', 'F3'];
const alphaNumericSort = (arr = []) => {
const sorter = (a, b) => {
const isNumber = (v) => (+v).toString() === v;
const aPart = a.match(/\d+|\D+/g);
const bPart = b.match(/\d+|\D+/g);
let i = 0;
let len = Math.min(aPart.length, bPart.length);
while (i < len && aPart[i] === bPart[i]) {
i++;
}
if (i === len) {
return aPart.length - bPart.length;
}
if (isNumber(aPart[i]) && isNumber(bPart[i])) {
return aPart[i] - bPart[i];
}
return aPart[i].localeCompare(bPart[i]);
};
arr.sort(sorter);
};
alphaNumericSort(arr);
console.log(arr);
실행 결과
위 코드를 실행하면 콘솔에 다음과 같이 출력됩니다.
[ '1', '2', 'A1', 'A3', 'A3A', 'A3B', 'A4', 'A10', 'A11', 'A12', 'B2', 'B10', 'F1', 'F3' ]
코드 동작 원리
핵심 로직을 단계별로 살펴보면 다음과 같습니다.
- isNumber(v): (+v).toString() === v 비교를 통해 해당 조각이 순수한 숫자 문자열인지 판별합니다.
- match(/\d+|\D+/g): 각 문자열을 연속된 숫자 조각과 비숫자 조각으로 분할합니다. 예를 들어 'A10'은 ['A', '10']으로 나뉩니다.
- 조각별 비교: 두 문자열의 조각을 앞에서부터 하나씩 비교하다가 처음으로 달라지는 지점에서, 양쪽 모두 숫자라면 수치로 비교하고 그렇지 않으면 localeCompare()로 사전순 비교를 수행합니다.
- 길이 비교: 한쪽이 다른 쪽의 접두어처럼 모든 조각이 일치하는 경우, 조각 개수의 차이를 반환해 더 짧은 문자열을 앞에 배치합니다.
이 방식을 활용하면 일반적인 사전순 정렬에서 'A10'이 'A9'보다 앞에 오는 문제를 피할 수 있으며, 사람이 직관적으로 기대하는 자연스러운(natural) 순서대로 배열을 정렬할 수 있습니다.