JSON 평탄화·역변환에서 배열이 사라지는 이유
JSON을 평탄화(flatten)했다가 다시 역변환(unflatten)하면 원본과 완전히 똑같아질 거라 기대하기 쉽습니다. 하지만 배열이 들어있던 JSON이라면 이 기대는 깨집니다. 이 가이드는 JSON 평탄화 도구의 실제 구현 코드를 근거로, 평탄화-역변환 왕복(round-trip)에서 정보가 어디서 어떻게 사라지는지 정확히 짚습니다.
1. 평탄화는 어떻게 동작하는가
평탄화는 중첩 객체를 점(.) 구분자로 이어 붙인 단일 레벨 키로 바꾸는 작업입니다. {"user":{"name":"Alice"}}는 {"user.name":"Alice"}가 됩니다. 배열은 인덱스를 키의 일부로 사용해 처리합니다: {"tags":["a","b"]}는 {"tags.0":"a","tags.1":"b"}가 되고, 배열 속에 객체가 있으면 그 객체도 재귀적으로 더 파고들어 평탄화합니다.
2. 역변환에서 실제로 벌어지는 일: 배열이 객체가 된다
여기가 핵심입니다. 역변환(unflatten) 로직은 점으로 나뉜 각 키 조각을 순서대로 순회하면서 항상 일반 객체({})를 만들어 그 안에 값을 채웁니다. 배열이었는지 아닌지를 판단하는 로직 자체가 없습니다. 즉 {"tags.0":"a","tags.1":"b"}를 역변환하면 {"tags":["a","b"]}가 아니라 {"tags":{"0":"a","1":"b"}}가 나옵니다 — 키가 "0", "1"인 객체이지, 배열이 아닙니다. JSON에서 이 둘은 값 자체는 똑같아 보여도(둘 다 순서대로 "a", "b"를 담고 있음) 타입이 다르므로, 이 결과를 다시 JavaScript 코드에서 .map()이나 .length로 다루려던 사람은 즉시 에러를 만납니다.
원본:
{"tags":["a","b"]}평탄화:
{"tags.0":"a","tags.1":"b"}역변환 결과:
{"tags":{"0":"a","1":"b"}} ← 배열이 객체로 바뀌어 있음
3. 왜 하필 이렇게 만들어졌나 — "배열인지 객체인지"는 원래 답이 없는 질문
이건 버그라기보다는 근본적으로 모호한 질문에 대한 선택입니다. {"list.0":"x","list.1":"y"}라는 평탄화된 키만 보고, 원래 이게 배열이었는지 아니면 우연히 키 이름이 "0", "1"이었던 객체인지 100% 확실하게 구분할 방법이 없습니다. "키가 0부터 시작하는 연속된 정수면 배열로 간주"하는 휴리스틱을 쓸 수도 있지만, 이 도구는 그런 추측을 아예 하지 않고 항상 객체로 통일해서 안전하게(모호함 없이) 처리하는 쪽을 택했습니다. 구현이 단순해지는 대신, 배열을 담은 JSON은 왕복 후 구조가 바뀐다는 트레이드오프를 갖게 됩니다.
4. 2차원 배열: 아예 더 쪼개지지 않는다
배열 안에 또 배열이 있는 경우([[1,2],[3,4]])는 한 단계 더 다릅니다. 평탄화 로직은 배열 요소를 검사할 때 "이 요소가 객체이고 배열이 아닐 때만" 재귀적으로 더 파고들도록 되어 있습니다. 요소 자체가 배열이면 이 조건을 만족하지 않으므로, 그 배열을 쪼개지 않고 원본 그대로 해당 인덱스 키의 값으로 저장합니다.
원본:
{"matrix":[[1,2],[3,4]]}평탄화 결과:
{"matrix.0":[1,2],"matrix.1":[3,4]} — matrix.0.0처럼 더 쪼개지지 않고, 값 자체가 배열인 채로 남습니다.
결과적으로 이 값은 JSON.stringify로 직렬화된 평탄 구조 안에서도 여전히 배열 타입을 유지합니다. 다만 이 값을 다시 역변환에 넣으면, 최상위 값([1,2] 자체)이 원본 배열 타입을 유지한 채 그대로 옮겨지므로 이 부분만큼은 왕복이 깨지지 않습니다 — 깨지는 지점은 어디까지나 "인덱스 키 여러 개로 흩어졌던" 1차원 배열 부분입니다.
5. 그 밖의 정보 손실: 키에 구분자가 포함된 경우
원본 데이터의 필드 이름에 우연히 구분자(기본값 점)가 들어있다면 문제가 생깁니다. 예를 들어 필드 이름이 정말로 "a.b"라면, 평탄화 후에는 이것이 원래 하나의 키였는지 아니면 a 객체 안의 b 필드였는지 구분할 방법이 사라집니다. 이 도구는 별도의 이스케이프 규칙을 두지 않으므로, 이런 데이터를 다룰 때는 UI에 있는 구분자 입력란을 점이 아닌 다른 문자(예: /나 _)로 바꿔서 충돌을 피하는 것이 실질적인 해결책입니다. 반대로 빈 객체 {}와 빈 배열 []은 평탄화 시 아예 통째로 값으로 보존되기 때문에(하위 키를 만들지 않음), 이 두 경우는 역변환해도 원본과 정확히 같은 형태로 돌아옵니다.
자주 묻는 질문
Q. 배열을 평탄화했다가 역변환하면 항상 객체로 바뀌나요?
A. 네. 이 도구의 역변환 로직은 배열 여부를 판단하지 않고 항상 일반 객체를 생성하므로, 1차원 배열이었던 부분은 예외 없이 {"0":..,"1":..} 형태의 객체로 돌아옵니다.
Q. 그럼 배열이 포함된 JSON은 평탄화 기능을 쓰면 안 되나요?
A. 평탄화 자체(단방향)는 문제없이 정확합니다. 문제는 "평탄화 후 다시 역변환"이라는 왕복 시나리오에서만 발생합니다. 평탄화 결과를 스프레드시트나 검색 인덱스처럼 단방향으로만 쓴다면 영향이 없습니다.
Q. 2차원 배열도 완전히 평탄화되나요?
A. 아니요. 배열 안에 배열이 중첩된 경우, 안쪽 배열은 더 쪼개지지 않고 해당 인덱스 키의 값으로 원본 그대로 저장됩니다.
Q. 구분자를 바꾸면 배열 손실 문제도 해결되나요?
A. 아니요. 구분자 변경은 키에 구분자 문자가 포함된 경우의 모호성만 해결하며, 배열이 객체로 바뀌는 문제와는 별개입니다.