빌드노트
Object.keys에서 10 다음에 넣은 2가 먼저 나오는 이유
객체에 10, 2, 01, label을 차례로 넣어도 Object.keys와 Object.entries는 2를 먼저 내보낸다. 배열 인덱스와 다른 문자열 키의 열거 규칙을 Node 실행 결과로 구분하고, 메뉴의 삽입 순서를 Map이나 배열에 보존할 시점을 설명한다.
메뉴 항목을 객체에 '10', '2' 순서로 추가했는데 Object.keys()로 꺼내면 '2'가 먼저 나온다. 일반 객체는 모든 문자열 키를 삽입 순서대로 열거하지 않는다. 배열 인덱스에 해당하는 키를 숫자 오름차순으로 먼저 내보낸다. 추가한 순서가 메뉴 순서라면 입력할 때부터 Map이나 배열에 그 순서를 남겨야 한다.
다음은 실제 화면 대신 키를 꺼내는 단계만 비교하는 예제다. Node.js가 설치되어 있다면 node --version으로 버전을 확인하고, 아래 코드를 menu-order.cjs에 저장한 뒤 node menu-order.cjs로 실행한다. Windows의 Node.js v22.23.2에서 실행했으며 별도 패키지는 필요 없다. 같은 항목을 같은 순서로 일반 객체와 Map에 넣어 차이를 확인한다.
const items = [
['10', 'Ten'],
['2', 'Two'],
['01', 'Leading zero'],
['label', 'Label'],
];
const menu = {};
const menuMap = new Map();
for (const [id, label] of items) {
menu[id] = label;
menuMap.set(id, label);
}
console.log(JSON.stringify(Object.keys(menu)));
console.log(JSON.stringify(Object.entries(menu)));
console.log(JSON.stringify([...menuMap.keys()]));
실행 결과는 다음과 같다. 첫 두 줄의 객체 키 순서와 마지막 줄의 Map 키 순서를 비교하면 된다.
["2","10","01","label"]
[["2","Two"],["10","Ten"],["01","Leading zero"],["label","Label"]]
["10","2","01","label"]
Object.entries()로 바꿔도 '2'가 먼저 나온다. 값까지 함께 얻는 메서드일 뿐, 삽입 순서를 복구하는 메서드는 아니다. 실제 메뉴에서도 화면에 넘기기 직전 Object.keys(menu)를 출력해 같은 순서인지 확인한다. 키 배열이 이미 원하는 순서라면 그 뒤의 정렬·변환이나 렌더링 단계를 살펴볼 차례다. 위의 새 예제 자체가 다른 결과를 내면 실행한 입력과 node --version부터 대조한다.
'01'은 왜 숫자 1처럼 앞으로 가지 않을까
앞으로 나온 '2', '10'도 문자열이다. 따옴표를 붙였는지가 기준은 아니다. ECMAScript의 OrdinaryOwnPropertyKeys 규칙 (새 탭에서 열림)은 일반 객체의 자체 키를 배열 인덱스, 나머지 문자열, Symbol로 나눠 처리한다. 배열 인덱스는 숫자 오름차순이고, 나머지 문자열은 속성이 만들어진 순서다.
Object.keys (새 탭에서 열림)와 Object.entries (새 탭에서 열림)는 이 중 객체 자신이 가진 열거 가능한 문자열 키를 사용한다. 상속받은 속성, 열거 불가능한 속성과 Symbol 키는 두 메서드의 결과에 들어가지 않는다. 이 필터와 키 순서의 연결은 EnumerableOwnProperties 알고리즘 (새 탭에서 열림)에서 확인할 수 있다.
여기서 말하는 배열 인덱스는 단순히 숫자로 변환할 수 있는 문자열이 아니다. 이 범위의 정수는 표준 숫자 문자열 표현과 일치해야 하며, 값은 0부터 4,294,967,294까지다. '01'을 숫자로 바꾸면 1이지만 다시 문자열로 만들면 '1'이므로 원래의 '01'과 다르다. 그래서 '01'은 나머지 문자열 쪽에 남는다. '4294967295'도 상한을 넘으므로 배열 인덱스가 아니다. 배열 인덱스 정의 (새 탭에서 열림)와 숫자 문자열 판별 규칙 (새 탭에서 열림)에 따른 구분이다.
따라서 위 결과는 모든 키를 숫자로 바꿔 정렬한 결과가 아니다. 별도 실행에서 'label'을 '01'보다 먼저 추가하자 객체 키는 ["2","10","label","01"]로 나왔다. 두 비인덱스 문자열 사이에서는 추가 순서가 반영됐다.
메뉴 순서는 입력할 때부터 보존한다
키로 항목을 찾는 기능과 삽입 순서가 함께 필요하면 첫 예제처럼 Map.set()으로 넣고 menuMap.keys() 또는 menuMap.entries()로 순회한다. Map의 set·keys·반복자 규칙 (새 탭에서 열림)에 따라 새 키가 추가된 순서를 유지한다. 기존 키에 set()을 다시 호출하면 값만 갱신되고 위치는 그대로다. 삭제한 뒤 다시 넣으면 마지막으로 이동하므로, 수정과 재삽입도 구분해야 한다.
메뉴가 순서 있는 표시 목록이면 첫 예제의 items 배열을 그대로 사용할 수 있다. 같은 파일 맨 아래에 다음을 붙이면 객체 키를 다시 열거하지 않고 입력한 항목 순서로 표시 문자열을 만든다.
console.log(JSON.stringify(items.map(([id, label]) => `${id}: ${label}`)));
["10: Ten","2: Two","01: Leading zero","label: Label"]
이미 만든 객체를 Map으로 바꾸는 시점에는 주의해야 한다. 같은 파일에 다음 두 줄을 붙여 변환 전후의 순서를 확인할 수 있다.
console.log(JSON.stringify([...new Map(Object.entries(menu)).keys()]));
console.log(JSON.stringify(Object.keys(Object.fromEntries(menuMap))));
["2","10","01","label"]
["2","10","01","label"]
첫 줄은 Object.entries(menu)가 내놓은 순서로 Map을 만든다. 이 값만으로는 '10'을 먼저 넣었다는 이력을 되찾을 수 없다. 두 번째 줄처럼 Map을 일반 객체로 되돌려 Object.keys()를 호출해도 객체의 열거 규칙이 다시 적용된다. 순서가 요구되면 원래 입력 배열이나 처음부터 만든 Map을 소비 지점까지 유지한다.
이 비교는 합성 데이터로 만든 일반 객체와 Map을 Node에서 실행한 결과다. Proxy의 키 처리, 상속을 포함한 열거, 실제 UI 렌더링과 JSON 서비스가 주고받는 순서 계약은 검증하지 않았다. 서버 응답부터 메뉴 순서를 유지해야 한다면 배열 등 순서를 명시하는 입력 형식까지 별도로 정해야 한다.