JavaScript로 데이터를 다룰 때, 깊게 중첩된 JSON 구조 안의 특정 객체들에만 일괄적으로 식별자를 부여해야 하는 경우가 종종 있습니다. 이번 글에서는 재귀(Recursion)와 클로저(Closure)를 활용해 "title" 속성을 가진 모든 객체에 고유한 "id"를 추가하는 방법을 알아보겠습니다.
문제 상황
다음과 같이 여러 계층으로 중첩된 배열이 있다고 가정해 보겠습니다.
const arr = [
{
"Arts": [
{
"Performing arts": [
{
"Music": [
{ "title": "Accompanying" },
{ "title": "Chamber music" },
{ "title": "Church music" },
{ "Conducting": [
{ "title": "Choral conducting" },
{ "title": "Orchestral conducting" },
{ "title": "Wind ensemble conducting" }
] },
{ "title": "Early music" },
{ "title": "Jazz studies" },
{ "title": "Musical composition" },
{ "title": "Music education" },
{ "title": "Music history" },
{ "Musicology": [
{ "title": "Historical musicology" },
{ "title": "Systematic musicology" }
] },
{ "title": "Ethnomusicology" },
{ "title": "Music theory" },
{ "title": "Orchestral studies" },
{ "Organology": [
{ "title": "Organ and historical keyboards" },
{ "title": "Piano" },
{ "title": "Strings, harp, oud, and guitar" },
{ "title": "Singing" },
{ "title": "Strings, harp, oud, and guitar" }
] },
{ "title": "Recording" }
]
},
{ "Dance": [
{ "title": "Choreography" },
{ "title": "Dance notation" },
{ "title": "Ethnochoreology" },
{ "title": "History of dance" }
] },
{ "Television": [
{ "title": "Television studies" }
] },
{ "Theatre": [
{ "title": "Acting" },
{ "title": "Directing" },
{ "title": "Dramaturgy" },
{ "title": "History" },
{ "title": "Musical theatre" },
{ "title": "Playwrighting" },
{ "title": "Puppetry" }
] }
]
}]
}];요구 사항 정리
우리가 작성해야 할 함수의 조건은 다음과 같습니다.
- 위와 같은 형태의 배열을 인수로 받습니다.
- 배열 전체를 탐색하면서 "title" 필드를 가진 모든 객체에 "id" 필드를 추가합니다.
- "id" 값 자체는 그다지 중요하지 않으며, 단지 고유하기만 하면 됩니다.
- 핵심은 "title" 속성을 지닌 모든 객체가 반드시 "id" 속성을 갖도록 만드는 것입니다.
- 또한 원본 배열의 복사본을 생성하지 않고 원본 데이터를 직접 수정(in-place)해야 합니다.
해결 방법
이 문제는 클로저로 카운터를 관리하면서, 객체 구조를 재귀적으로 순회하는 방식으로 해결할 수 있습니다. 코드는 다음과 같습니다.
const arr = [
{ "Arts": [
{ "Performing arts": [
{ "Music": [
{ "title": "Accompanying" },
{ "title": "Chamber music" },
{ "title": "Church music" },
{ "Conducting": [
{ "title": "Choral conducting" },
{ "title": "Orchestral conducting" },
{ "title": "Wind ensemble conducting" }
] },
{ "title": "Early music" },
{ "title": "Jazz studies" },
{ "title": "Musical composition" },
{ "title": "Music education" },
{ "title": "Music history" },
{ "Musicology": [
{ "title": "Historical musicology" },
{ "title": "Systematic musicology" }
] },
{ "title": "Ethnomusicology" },
{ "title": "Music theory" },
{ "title": "Orchestral studies" },
{ "Organology": [
{ "title": "Organ and historical keyboards" },
{ "title": "Piano" },
{ "title": "Strings, harp, oud, and guitar" },
{ "title": "Singing" },
{ "title": "Strings, harp, oud, and guitar" }
] },
{ "title": "Recording" }
] },
{ "Dance": [
{ "title": "Choreography" },
{ "title": "Dance notation" },
{ "title": "Ethnochoreology" },
{ "title": "History of dance" }
] },
{ "Television": [
{ "title": "Television studies" }
] },
{ "Theatre": [
{ "title": "Acting" },
{ "title": "Directing" },
{ "title": "Dramaturgy" },
{ "title": "History" },
{ "title": "Musical theatre" },
{ "title": "Playwrighting" },
{ "title": "Puppetry" }
] }
]
}]
}];
const addId = (id = 1) => {
return function recur(obj) {
// 'title' 키가 존재하면 고유 id를 부여
if ('title' in obj) {
obj.id = id++;
};
// 값이 배열인 프로퍼티를 찾아 재귀적으로 탐색
Object.keys(obj).forEach(el => {
Array.isArray(obj[el]) && obj[el].forEach(recur);
});
};
}
const mapId = arr => {
arr.forEach(addId);
}
mapId(arr);
console.log(JSON.stringify(arr, undefined, 4));코드 동작 원리
핵심 로직을 단계별로 살펴보면 다음과 같습니다.
- addId(id = 1): 외부 함수가 시작값을 받아 내부 함수
recur를 반환합니다. 이 덕분에 카운터 변수id가 클로저에 의해 유지되어, 호출될 때마다 1씩 증가하는 고유 번호를 부여할 수 있습니다. - 'title' in obj: 현재 객체에 "title" 키가 있는지 확인하고, 존재한다면 해당 객체에
obj.id = id++로 고유 id를 할당합니다. - Object.keys(obj).forEach(...): 객체의 모든 키를 순회하면서 값이 배열(
Array.isArray)인 경우, 그 배열의 각 요소에 대해recur를 다시 호출합니다. 이 과정이 임의의 깊이까지 반복되며 전체 트리 구조를 완전히 탐색합니다. - 원본 직접 수정: 새로운 객체를 반환하거나 복사본을 만드는 대신 기존 객체에 직접 속성을 추가하므로, 메모리 낭비 없이 원본 배열이 그대로 업데이트됩니다.
출력 결과
콘솔에 출력된 결과에서 "title"을 가진 모든 객체에 순차적인 "id"가 부여된 것을 확인할 수 있습니다.
[
{
"Arts": [
{
"Performing arts": [
{
"Music": [
{
"title": "Accompanying",
"id": 1
},
{
"title": "Chamber music",
"id": 2
},
{
"title": "Church music",
"id": 3
},
{
"Conducting": [
{
"title": "Choral conducting",
"id": 4
},
{
"title": "Orchestral conducting",
"id": 5
},
{
"title": "Wind ensemble conducting",
"id": 6
}
],
"id": 7
},
{
"title": "Early music",
"id": 8
},
{
"title": "Jazz studies",
"id": 9
},
{
"title": "Musical composition",
"id": 10
},
{
"title": "Music education",
"id": 11
},
{
"title": "Music history",
"id": 12
},
{
"Musicology": [
{
"title": "Historical musicology",
"id": 13
},
{
"title": "Systematic musicology",
"id": 14
}
],
"id": 15
},
{
"title": "Ethnomusicology",
"id": 16
},
{
"title": "Music theory",
"id": 17
},
{
"title": "Orchestral studies",
"id": 18
},
{
"Organology": [
{
"title": "Organ and historical keyboards",
"id": 19
},
{
"title": "Piano",
"id": 20
},
{
"title": "Strings, harp, oud, and guitar",
"id": 21
},
{
"title": "Singing",
"id": 22
},
{
"title": "Strings, harp, oud, and guitar",
"id": 23
}
],
"id": 24
},
{
"title": "Recording",
"id": 25
}
]
},
{
"Dance": [
{
"title": "Choreography",
"id": 26
},
{
"title": "Dance notation",
"id": 27
},
{
"title": "Ethnochoreology",
"id": 28
},
{
"title": "History of dance",
"id": 29
}
],
"id": 30
},
{
"Television": [
{
"title": "Television studies",
"id": 31
}
],
"id": 32
},
{
"Theatre": [
{
"title": "Acting",
"id": 33
},
{
"title": "Directing",
"id": 34
},
{
"title": "Dramaturgy",
"id": 35
},
{
"title": "History",
"id": 36
},
{
"title": "Musical theatre",
"id": 37
},
{
"title": "Playwrighting",
"id": 38
},
{
"title": "Puppetry",
"id": 39
}
],
"id": 40
}
],
"id": 41
},
"id": 42
],
"id": 43
}
]마무리
이처럼 클로저와 재귀 함수를 조합하면, 깊이나 구조가 얼마나 복잡하든 중첩된 JSON 데이터를 안정적으로 순회하며 필요한 속성을 일괄 추가할 수 있습니다. 실무에서는 이 패턴을 확장해 UUID 생성, 노드 경로 추적, 트리 컴포넌트용 데이터 변환 등 다양한 용도로 응용할 수 있으니 꼭 기억해 두시기 바랍니다.