취미 겸 공부로 SvelteKit 기반의 웹 앱을 만들던 도중 겪게 된 오류가 또 하나가 있다. 대충 아래와 같은 한숨 나오는 메시지다.
TypeError: Converting circular structure to JSON
circular structure라니 무한 루프에 빠져 허우적거리던 불행한 일을 떠올리게 만드는 문구 같다. circular structure에 대한 대략적인 설명으론 JSON으로 변환하려는 데이터들의 object 중에 하위와 상위 객체 사이에 순환 참조가 걸려 발생하는 문제로 정리하는 것 같은데 실제 구현은 모르니 이 정도로만 정리하자. 순환 참조 문제야 다른 언어에서도 흔히 발생하는 불행한 일이니 말이다.
어쨌든 정확한 오류는 아래와 같은 식이다.
TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Document'
| property 'children' -> object with constructor 'Array'
| index 0 -> object with constructor 'ProcessingInstruction'
--- property 'parent' closes the circle
at JSON.stringify (<anonymous>)
at Module.json (/foo/bar/node_modules/@sveltejs/kit/src/exports/index.js:122:20)
at GET (/foo/bar/src/routes/some/foo/bar/+server.ts:29:12)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Module.render_endpoint (/foo/bar/node_modules/@sveltejs/kit/src/runtime/server/endpoint.js:43:18)
at async resolve (/foo/bar/node_modules/@sveltejs/kit/src/runtime/server/respond.js:451:17)
at async Module.respond (/foo/bar/node_modules/@sveltejs/kit/src/runtime/server/respond.js:327:20)
at async file:///foo/bar/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:525:22
문제가 발생한 코드는 SvelteKit의 기본 세팅인 Typescript 언어로 코딩 중이었다. 대충 아래와 같은 코드다.
const $ = cheerio.load(html);
const anchors = $('a.foobar');
const items = anchors.map((i, element) => {
return {
title: $(element).text().trim(),
url: $(element).attr('href')!.trim(),
}
});
console.log('items = ' + items) // ERROR!
보다시피 Cheerio를 이용해 HTML을 쪼개서 이것저것 추출하려는 의도의 코드인데, 여기서 디버깅을 위해 깔아 둔 console.log() 부분이 말썽이다.
자 그렇다면 왜 오류가 발생한 것일까?
이런 류의 문제에 대해 찾아보면 다양한 글들을 볼 수 있다. 한 가지 확실한 것은 공통된 간단한 해결책은 없다는 점 같다. 다만 map의 결과로 얻은 Array 형식의 데이터를 수동으로 뜯어서 찍어보면 데이터 자체는 문제가 없다고들 하는데 내 경우도 그랬다.
해결하기
삽질을 좀 오래 하다 발견한 게 하나 있다. 약간 특수한 경우겠지만 위 코드에서 map() 함수는 표준 함수가 아니라 Cheerio 모듈의 map() 함수가 호출된다는 점이다. 이런 경우 반환되는 결과가 일반적인 Array 형태가 아닐 가능성이 높다. 그 외에 성능이나 메모리 효율을 위해 실제 evaluation 된 결과가 아닌 그 전의 각 자료구조의 참조 값을 가진 object들이 포함되어 있을 가능성도 있을 것 같았다.
보통 이 경우 해결할 수 있는 수단으로 Lisp에서도 종종 겪던 문제가 있다. 특정 리스트의 경우 메모리 효율이나 원소성을 위해 리스트 내부에 데이터가 아니라 데이터를 추출하는 코드가 바로 삽입되는 경우가 있다. 이 경우 이 리스트를 출력하는 등 아무렇게나 사용하면 오류가 난다. 이럴 때 사용되던 게 일반 list로 바꾸는 방법이 있는데 이 과정에서 원본 리스트의 함수 코드 요소 등이 있다면 실행(evaluation) 결과가 데이터로 치환되어서 리스트로 구성되기 때문에 문제가 해결되는 것이었다. 이런 것과 비슷한 경우로 보기엔 근거는 부족하지만 왠지 느낌이 왔다.
그래서 비슷한 게 있을까 아무렇게 코드를 쳐 보면서 자동완성으로 기능으로 찾아봤는데 운 좋게도 바로 toArray()라는 함수를 발견할 수 있었다. 그래서 위의 코드에서 map 부분을 아래와 같이 수정했다.
const items = anchors.map((i, element) => {
return {
title: $(element).text().trim(),
url: $(element).attr('href')!.trim(),
}
}).toArray();
끝에 추가된 부분이 하나 있다. 앞서 발견한 그걸 써서 map의 결과물을 toArray() 함수로 일반 배열화 하도록 한 부분이다.
이렇게 한 이후 오류도 사라졌고 원하는 대로 코드가 동작하기 시작했다. 이틀 간의 삽질이 이 하나의 메서드 하나로 해결되었다.
사족
물론 이번 경우는 Cheerio의 경우에 한한 것일 수도 있다. 하지만 이처럼 분명 map의 결과로 얻은 Array 안의 내용뮬이 순수(?)하지 않다면 다른 방법으로 한번 더 가공해야 하지 않나를 찾아보는 게 좋을 수도 있다. DB를 다룰 때도 비슷한 경우가 있었는데 쿼리 결과물을 JSON으로 반환할 때는 일반 list로 가공을 한번 더 해야 하는 등 말이다. 데이터가 복잡하지 않다면 수동으로 변환하는 방법을 쓸 수도 있겠지만 말이다.
그전에 불친절한 오류 메시지나 불친절한 스택 트레이스가 좀 더 친절해졌으면 좋겠다는 생각도 들었다. 이번 글의 에러 메시지는 구현 자체를 모르면 무의미한 내용으로 봐도 될 정도니 말이다. 물론 AI가 대신 코딩해 주는 세상이 오고 있으니 이런 불평불만도 무의미한 일이 될지도 모르겠지만 말이다.
'기술적인 이야기 > 웹 개발' 카테고리의 다른 글
SvelteKit에서 호스트 이름 없이 fetch하기 (3) | 2024.10.23 |
---|---|
Electron에서 타이틀 바 숨기기 및 창 이동 문제 (274) | 2022.02.06 |
Electron의 app 정보를 얻기 위한 삽질기 (259) | 2022.01.30 |
Electron에서 iframe 사용하기(?) (264) | 2022.01.23 |
Electron + Svelte로 데스크톱 앱 개발 시작해보기 (382) | 2022.01.16 |
댓글