자바스크립트 - JSON 파싱 구조 개선하기
CGATS 포맷 개발로 자바스크립트를 활용하고 있었는데, 문제 있는 코드가 있어서 수정이 필요하고 복잡해서 글로 남기게 되었다.
함수는 ISO 13655 변환 전에 CGATS 포맷을 가져오는 코드로 XYZ 입력하면 X, Y, Z 변수들로 가져오는 간단한 함수이지만 의외로 LAB, LCH 그리고 SPECTRAL 데이터가 정적 포맷 구조로 사용하기가 어려워서 기초적인 작업하다가 비표준도 허용할 수 있도록 파싱 구조 변경에 필요성을 느껴서 수정하기로 하였다.
먼저 등록된 json 구조는 다음과 같다.
Colorspace.json
{
"SAMPLE": {
"ID": ["SAMPLE_ID"],
"NAME": ["SAMPLE_NAME"]
},
"CMYK": {
"C": ["CMYK_C"],
"M": ["CMYK_M"],
"Y": ["CMYK_Y"],
"K": ["CMYK_K"]
},
"XYZ": {
"X": ["XYZ_X"],
"Y": ["XYZ_Y"],
"Z": ["XYZ_Z"]
},
...
}키 값으로 SAMPLE, XYZ 등이 있고 하위에는 XYZ인 경우 각각 X,Y,Z 구조 분해로 표시하고 있다.
javascript
const proc = await fetch("./Colorspace.json");
const json await proc.json();
const obj = json["XYZ"];
// CGATS 표준이 LCH, LCH 모두 헤더 값으로 LAB_ 시작하므로 구별할 수 없으므로 {{[]}} 포맷으로 맞춰서 작성함
// TODO. format 구조가 불분명하므로 해당 포맷 개선이 필요함
maskHeaders(obj) {
let headers = [];
const verifyType = (obj) => {
let keys = Object.keys(obj)
if (!Array.isArray(obj[keys[0]]) && typeof obj[keys[0]] === "object") return verifyType(obj[keys[0]])
else return obj;
}
const keys = Object.keys(verifyType(obj));
// keys는 중복를 체크함. (ex. Colorspace.json 파일 참고해서 XYZ 키의 X,Y,Z 3개만 있다는 것을 명확히 명시.
// 데이터베이스 distinct 쿼리로 중복 없애는 것과 같은 원리로 중복을 없애므로 n^2 수준으로 성능이 느릴 수 있음. 추후 개선 필요함.
keys.forEach(k =>
obj[k].forEach(v => {
this.cgats.headers.forEach(s => s.match(new RegExp(`^${v}(\\d+)?`)) && headers.push(s))
})
)
return headers;
}문제의 코드.
참고로 this.cgats.headers 멤버 변수는 CGATS 규격의 BEGIN DATA FORMAT, END... 사이에 있는 데이터이다.
이코드는 검증 코드도 재귀로 동작되고 이중 forEach문 사용으로 O^2~3 수준이다. 실제 운용에는 CGATS 키워드 관련이 얼마 없고 헤더 구조만 배열로 가져오는 역할만 하고 지속적으로 추가하는 것이 없어서 큰 문제는 없다. 그러나 정규표현식으로 사용한 ^${v} 에서는 별다른 검증이 없고, 보기 좋지 않은 코드이다.
코드 개선하기
json 구조를 다음으로 변경하였다.
Colorspace.json
{
"STD_SAMPLE": {
"prefix": "SAMPLE",
"channels": ["ID", "NAME"]
},
"STD_RGB": {
"prefix": "RGB",
"channels": ["R", "G", "B"]
},
"STD_XYZ": {
"prefix": "XYZ",
"channels": ["X", "Y", "Z"]
},
"STD_LAB": {
"prefix": "LAB",
"channels": ["LAB_L", "LAB_A", "LAB_B"]
},
"STD_LCH": {
"prefix": "LAB",
"channels": ["LAB_L", "LAB_C", "LAB_H"]
},
"SPD": {
"prefix": "NM",
"numeric": true
}
...
}오브젝트를 하위로 추가한 것인데, 헤더 첫 부분이 어떤 것이고 가져와야 하는 채널이 무엇인지 명확히 구분하였다.
예외로 언더스코어가 없는 nm380, nm390.. 으로 이루어진 비표준 데이터도 파싱해야해서 포함할 수 있도록 했다.
javascript
const proc = await fetch("./Colorspace.json");
const rules = await proc.json();
const type = "XYZ";
classifyHeader(header) {
const m = header.match(
/^(?<prefix>[A-Za-z]+)(?:_(?<channel>[A-Za-z]+))?(?:(?<num>\d+))?$/
);
if (!m) return null;
const { prefix, channel, num } = m.groups;
for(const [type, rule] of Object.entries(this.rules)) {
if ((prefix !== rule.prefix)) continue;
if (rule.channels && !rule.channels.includes(channel)) continue;
if (rule.numeric && !num) continue;
return {
type,
header,
channel,
wavelength: num ? Number(num) : null
}
}
return null;
}
makeHeaders(headers) {
const result = [];
for (const h of headers) {
const info = this.classifyHeader(h);
if (!info) continue;
if (!result[info.type]) {
result[info.type] = [];
}
result[info.type].push(info.header);
}
return result;
}classifyHeader 함수는 정규표현식으로 헤더가 들어오면 (예(ex. LAB_A, LAB_B...)) 구조 분해하여 변수로 사용할 수 있어서 그룹 명명과 비캡처 그룹을 적극적으로 활용하였다. 정규표현식으로 파싱된 오브젝트는 Object.entries(...) 을 사용해 반복문으로 동작시키고 json에서 지정한 CGATS 포맷의 특징에 따라 예외 처리와 어떤 역할과 책임인지 명확하게 처리할 수 있다.
makeHeaders 함수는 입력값으로 CGATS Format Data 전체를 가져와 classifyHeader 하나씩 대입한 후 classifyHeader 생성한 결과를 result 변수에 모두 담아 반환하여 데이터 정제에 사용한다.
처음에는 코드가 복잡하고 어렵지만 이 과정을 거치고 나면 재사용성과 json 주입으로 새로운 비표준 데이터가 들어와도 쉽게 파싱할 수 있고, json에 입력한 특징에 따라서 예외로 들 수 있어서 높은 확장성으로 사용할 수 있다.