API 응답이나 설정 파일을 다루다 보면 car.make, car.revisions.0.miles처럼 점(.) 표기법으로 평탄화된 키를 가진 객체를 만나게 됩니다. 이 글에서는 이렇게 평탄화된 객체를 원래의 중첩 객체와 배열 구조로 되돌리는(unflatten) 방법을 알아보겠습니다.
문제 상황
다음과 같이 모든 키가 점 표기법으로 나열된 객체가 있다고 가정해 보겠습니다.
const obj = {
"firstName": "John",
"lastName": "Green",
"car.make": "Honda",
"car.model": "Civic",
"car.revisions.0.miles": 10150,
"car.revisions.0.code": "REV01",
"car.revisions.0.changes": "",
"car.revisions.1.miles": 20021,
"car.revisions.1.code": "REV02",
"car.revisions.1.changes.0.type": "asthetic",
"car.revisions.1.changes.0.desc": "Left tire cap",
"car.revisions.1.changes.1.type": "mechanic",
"car.revisions.1.changes.1.desc": "Engine pressure regulator",
"visits.0.date": "2015-01-01",
"visits.0.dealer": "DEAL-001",
"visits.1.date": "2015-03-01",
"visits.1.dealer": "DEAL-002"
};
이렇게 평탄화된 구조는 사람이 읽기 어렵고 데이터를 조작하기도 번거롭습니다. 따라서 이러한 객체를 입력받아 원래대로 중첩된 객체와 배열 구조로 되돌려 주는 JavaScript 함수를 작성해야 합니다.
즉, 위 객체를 변환한 최종 결과는 다음과 같은 형태가 되어야 합니다.
const output = {
firstName: 'John',
lastName: 'Green',
car: {
make: 'Honda',
model: 'Civic',
revisions: [
{ miles: 10150, code: 'REV01', changes: '' },
{
miles: 20021,
code: 'REV02',
changes: [
{ type: 'asthetic', desc: 'Left tire cap' },
{ type: 'mechanic', desc: 'Engine pressure regulator' }
]
}
]
},
visits: [
{ date: '2015-01-01', dealer: 'DEAL-001' },
{ date: '2015-03-01', dealer: 'DEAL-002' }
]
};
구현 방법
핵심 아이디어는 각 키를 점(.)을 기준으로 분리한 뒤, 분리된 경로를 따라가면서 결과 객체에 중첩 구조를 하나씩 만들어 주는 것입니다. 이때 다음 세그먼트가 숫자라면 그 자리는 배열로, 그렇지 않다면 객체로 생성하면 됩니다.
const obj = {
"firstName": "John",
"lastName": "Green",
"car.make": "Honda",
"car.model": "Civic",
"car.revisions.0.miles": 10150,
"car.revisions.0.code": "REV01",
"car.revisions.0.changes": "",
"car.revisions.1.miles": 20021,
"car.revisions.1.code": "REV02",
"car.revisions.1.changes.0.type": "asthetic",
"car.revisions.1.changes.0.desc": "Left tire cap",
"car.revisions.1.changes.1.type": "mechanic",
"car.revisions.1.changes.1.desc": "Engine pressure regulator",
"visits.0.date": "2015-01-01",
"visits.0.dealer": "DEAL-001",
"visits.1.date": "2015-03-01",
"visits.1.dealer": "DEAL-002"
};
const unflatten = (obj = {}) => {
const result = {};
let temp, substrings, property, i;
for (property in obj) {
// 키를 점(.) 기준으로 분리
substrings = property.split('.');
temp = result;
// 마지막 세그먼트 직전까지 경로를 따라가며 중첩 구조 생성
for (i = 0; i < substrings.length - 1; i++) {
if (!(substrings[i] in temp)) {
if (isFinite(substrings[i + 1])) {
// 다음 세그먼트가 숫자면 배열 생성
temp[substrings[i]] = [];
} else {
// 그렇지 않으면 객체 생성
temp[substrings[i]] = {};
}
}
temp = temp[substrings[i]];
}
// 마지막 세그먼트에 실제 값 할당
temp[substrings[substrings.length - 1]] = obj[property];
}
return result;
};
console.log(JSON.stringify(unflatten(obj), undefined, 4));
동작 원리
- 키 분리: split('.')을 사용해 "car.revisions.0.miles"와 같은 키를 ["car", "revisions", "0", "miles"] 배열로 분리합니다.
- 경로 탐색 및 생성: 마지막 요소를 제외한 각 세그먼트를 순회하며 해당 경로가 아직 없으면 새로 만듭니다. 이때 바로 다음 세그먼트가 유한한 숫자(isFinite)라면 배열을, 아니면 객체를 생성합니다.
- 값 할당: 마지막 세그먼트 위치에 원본 객체의 값을 대입합니다.
출력 결과
위 코드를 실행하면 콘솔에 다음과 같은 결과가 출력됩니다.
{
"firstName": "John",
"lastName": "Green",
"car": {
"make": "Honda",
"model": "Civic",
"revisions": [
{
"miles": 10150,
"code": "REV01",
"changes": ""
},
{
"miles": 20021,
"code": "REV02",
"changes": [
{
"type": "asthetic",
"desc": "Left tire cap"
},
{
"type": "mechanic",
"desc": "Engine pressure regulator"
}
]
}
]
},
"visits": [
{
"date": "2015-01-01",
"dealer": "DEAL-001"
},
{
"date": "2015-03-01",
"dealer": "DEAL-002"
}
]
}
이처럼 간단한 반복문과 isFinite() 검사만으로도 점 표기법으로 평탄화된 객체를 손쉽게 원래의 중첩 구조로 복원할 수 있습니다. 폼 데이터 처리, 쿼리 스트링 파싱, API 응답 변환 등 다양한 상황에서 유용하게 활용할 수 있습니다.