three.js r184의 Inspector UI 적용시

three.js r184에서 기본 제공하는 Inspector를 생성할때 extensions.json을 파일을 서버로부터 내려받는다. 근데 해당 파일이 웹 서버를 통해 접근할 수 없어 에러가 발생한다.

해결방법은 three.js가 설치된 폴더에서 관련된 파일을 복사해 직접 붙여넣기를 해주면 된다. 아래 그림을 보면 관련된 파일은 node_modules 폴더의 three 폴더에 존재하는데, 이 녀석들을 node_modules 폴더의 .vite 폴더에 복사해 넣는다.

이 부분은 개선될 것인지 아니면 어쩔것인지 모르겠다. r184가 배포된지 한달이 넘어감에도 개선되지 않는걸 보면 ….. 그냥 이렇게 개발자가 직접 복사해서 쓰라는 것인지…

Deep JavaScript

WeakRef와 FinalizationRegistry (메모리 관리)

GC에 의해 수거되는 리소스에 대해 개발자가 처리해야할 때 알아두면 좋은 코드이다. 바로 WeakRef와 FinalizationRegistry인데 관련된 코드는 다음과 같다.

// 무거운 데이터 객체
let bigData = { payload: new Array(1000000) };

// 약한 참조 생성
// WeakRef: 객체를 참조하되, 가비지 컬렉션의 대상이 되는 것을 막지 않는 '약한 참조'를 만듭니다. 
// 객체가 메모리에서 해제되면 참조도 자동으로 끊깁니다.
const ref = new WeakRef(bigData);

// 사용처에서 꺼내 쓰기
const derefData = ref.deref();
if (derefData) {
  console.log("아직 메모리에 남아있음:", derefData.payload);
}

// 특정 객체가 메모리에서 해제될 때 정리 작업을 해주는 레지스트리
// FinalizationRegistry: 객체가 가비지 컬렉터에 의해 메모리에서 완전히 지워지는 시점에 특정 콜백 함수를 실행하도록 등록합니다.
const registry = new FinalizationRegistry((heldValue) => {
  console.log(`${heldValue} 객체가 가비지 컬렉션되었습니다.`);
});
registry.register(bigData, "무거운 데이터");

// 참조를 끊으면 가비지 컬렉터가 수거해감
bigData = null;

코드 실행을 일정 시간 블로킹시키는 함수

Node 환경과 웹브라우저 환경에 따라 다르긴 한데 Node에서는 다음 방법을 추천한다.

function sleep(ms) {
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}

console.log("시작 - 1");
sleep(2000); // 2초 블로킹
console.log("2초 후 - 1");

웹 브라우저에서는 다음 방법을 추천하며 Node에서도 가능함.

function sleepWebBrowser(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

console.log("시작 - 2");
await sleepWebBrowser(2000);
console.log("2초 후 - 2");

Object.groupBy() (데이터 그룹화 공식 지원)

배열의 데이터를 특정 기준에 따라 객체로 묶어주는 기능입니다. 과거에는 reduce를 복잡하게 쓰거나 외부 라이브러리(lodash 등)를 썼어야 했는데, 이제 네이티브 문법으로 지원.

const inventory = [
  { name: "asparagus", type: "vegetables", quantity: 5 },
  { name: "bananas", type: "fruit", quantity: 0 },
  { name: "goat", type: "meat", quantity: 23 },
  { name: "cherries", type: "fruit", quantity: 5 },
];

// 'type' 속성을 기준으로 그룹화
const result = Object.groupBy(inventory, ({ type }) => type);

/*
결과:
{
  vegetables: [ 
    { name: "asparagus", type: "vegetables", ... } 
  ],
  fruit: [ 
    { name: "bananas", ... }, 
    { name: "cherries", ... } 
  ],
  meat: [ 
    { name: "goat", ... } 
  ]
}
*/

Proxy와 Reflect (메타 프로그래밍)

자바스크립트의 기본 동작(객체의 속성 조회, 할당, 열거 등)을 가로채서(Intercept) 사용자 정의 동작을 주입하는 강력한 기능임. Vue 3 같은 모던 프레임워크의 상태 변화 감지(Reactivity) 시스템이 바로 이 Proxy를 기반으로 동작함.

const target = { message: "hello" };

const handler = {
  // 객체의 속성을 읽으려고 할 때(get) 가로챔
  get(target, prop, receiver) {
    console.log(`[로그] ${prop} 속성에 읽기로 접근했습니다.`);
    return Reflect.get(...arguments); // 원래 동작 수행
  },
  // 객체의 속성을 바꿀 때(set) 가로챔
  set(target, prop, value, receiver) {
    console.log(`[로그] ${prop} 속성에 쓰기로 접근했습니다.`);

    if (prop === 'age' && value < 0) {
      throw new Error("나이는 음수가 될 수 없습니다.");
    }
    return Reflect.set(...arguments);
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.message); // [로그] message 속성에 읽기로 접근했습니다. -> "hello"
proxy.message = "안녕하세요." // [로그] message 속성에 쓰기로 접근했습니다. 
proxy.age = 10; // [로그] age 속성에 쓰기로 접근했습니다.
proxy.age = -10; // [로그] age 속성에 쓰기로 접근했습니다. (Error: 나이는 음수가 될 수 없습니다.)

구조적 복제 (Structured Clone)

객체의 깊은 복사(Deep Copy)를 수행하는 내장 API임. 과거에는 JSON.parse(JSON.stringify(obj))라는 편법을 썼지만, 이 방식은 Date, RegExp, Map, Set 같은 특수 객체나 순환 참조가 있으면 깨지는 치명적인 문제가 있음. structuredClone은 이를 완벽하게 복사해 줌.

const original = {
  date: new Date(),
  set: new Set([1, 2, 3]),
  nested: { inner: "value" }
};

// 완벽한 깊은 복사
const clone = structuredClone(original);

console.log(clone.date instanceof Date); // true
console.log(clone.nested === original.nested); // false (참조가 분리됨, 깊은 복사로 객체에 대한 주소값이 다름)

생성기 (Generators)와 이터레이터

함수의 실행을 중간에 멈췄다가(yield), 원하는 시점에 다시 재개(next)할 수 있는 특수 함수. 비동기 스트림을 제어하거나, 메모리를 아끼면서 대용량의 무한한 데이터를 순차적으로 처리할 때 매우 유용하며 필자는 메인 스레드를 얼리지 않고 대량의 데이터를 순차적으로 처리하는 기능에 적용하기도 함.

function* idGenerator() {
  let id = 1;
  while (true) {
    yield id++; // 여기서 실행을 멈추고 값을 반환
  }
}

const gen = idGenerator();
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3 (필요할 때만 값을 무한히 생성)

명시적 자원 관리 (Explicit Resource Management – using)

최근 ECMAScript 스펙에 추가되고 있는 매우 획기적인 기능. 파일 핸들러, 네트워크 소켓, 데이터베이스 커넥션, 혹은 그래픽 컨텍스트처럼 사용 후 반드시 닫아주어야(close/dispose) 하는 자원을 다룰 때 씀. try…finally로 일일이 닫지 않아도, 블록을 벗어나면 자동으로 자원이 해제됨.

// 자원 정의 (Symbol.dispose 메서드를 구현해야 함)
const disposableResource = {
  [Symbol.dispose]() {
    console.log("자원이 자동으로 해제되었습니다.");
  },
  action() {
    console.log("작업 수행 중...");
  }
};

{
  // using 키워드로 선언
  using res = disposableResource;
  res.action();
} // 이 블록을 나가는 순간 Symbol.dispose()가 자동으로 호출됨!

using은 최근에 도입된 아직은 시험 중으로 실행시 다음 옵션이 필요함. node --js-explicit-resource-management

ArrayBuffer.prototype.resize() & SharedArrayBuffer.prototype.grow()

메모리 버퍼의 크기를 가변적으로 조절할 수 있는 기능. WebGL/WebGPU를 다루거나 큰 바이너리 데이터를 청크(Chunk) 단위로 읽어 들일 때, 이전에는 버퍼 크기가 모자라면 더 큰 버퍼를 새로 만들고 기존 데이터를 하나하나 복사(ArrayBuffer.transfer)해야 했음. 이제는 내부 메모리 주소를 유지한 채로 최대 크기(maxByteLength) 범위 내에서 버퍼를 동적으로 늘리고 줄일 수 있음.

// 초기 1MB, 최대 10MB까지 커질 수 있는 버퍼 선언
const buffer = new ArrayBuffer(1024 * 1024, { maxByteLength: 10 * 1024 * 1024 });

console.log(buffer.byteLength); // 1MB

// 복사 작업 없이 인라인으로 즉시 2MB로 확장
buffer.resize(2 * 1024 * 1024);
console.log(buffer.byteLength); // 2MB

지오서비스웹(GEOSERVICE-WEB) 기능 별 메뉴얼

주소를 좌표로 변환하는 지오코딩

(참고) 엑셀을 CSV로 변환할 필요 없이 바로 이용할 수 있습니다. 더 이상 번거롭게 CSV로 변환할 필요가 없습니다. 이는 다른 모든 기능도 동일하게 적용됩니다.

영상보다 이미지와 글이 편한 분들은 아래를 참조해 주세요.

주소 데이터를 지도에 표시하고 싶다구요! (지오코딩)

지오코딩 결과를 QGIS의 배경지도와 중첩 예시

지오코딩 결과를 QGIS의 TMS for Korea 배경지도에 맵핑

좌표를 주소로 변환하는 리버스 지오코딩

SHP 데이터를 CSV로 변환(지오코딩 결과를 텍스트 파일로 저장)

SHP 파일로부터 레이어 구성

지점 간 거리 분석

헥사곤 도형 생성

포인트 카운트 분석

색상단계 구분도 분석

밀도 분석

히트맵 분석

파이차트 맵 분석

배경지도 변경

GEOSERVICE-WEB의 배경지도 변경

폴리라인을 포인트로 변경

라인을 포인트로 변경 (Line To Point)

새로운 공간 데이터 생성

공간/속성 데이터 구축(편집)

공간/속성 데이터 구축(편집)

지오서비스웹 공간/속성 데이터 편집

정밀한 지오코딩 결과 만들기 (편집 기능 활용)

내가 원하는 지역에 대한 최신 행정구역 데이터(SHP 파일) 추출

티센 폴리곤 생성

내가 원하는 지역에 대한 최신 행정구역을 SVG 파일로 생성하기

행정구역도 SVG 생성 및 파워포인트에서 활용하기

SHP 폴리곤 단순화

3D 모델 데이터에 대한 Progressive Loading

three.js에서 모델 데이터를 로딩할때, 기본적으로 해당 모델 데이터가 완전히 로딩된 이후에 장면에 추가될 수 있고 그럼으로써 화면에 레더링된다. 이러한 처리로 인해 모델 데이터의 용량이 클 경우 사용자는 해당 모델 데이터가 완전히 로딩되기까지 기다려야 한다. 아래는 이러한 상황을 보여주는 동영상이다. 네트워크 속도를 느리게 설정해두었고 3개의 모델 데이터(각각의 용량은 32M, 50M, 54M 임)를 렌더링하는 경우이다.

위에서 보는 것처럼 각 모델 데이터가 완전히 로딩되어야 화면에서 볼 수 있고, 화면에 렌더링되기까지 시간도 제법 많이 소요되는것을 알 수 있다. 이러한 문제점을 개선하기 위해 점진적 로딩(Progressive Loading) 기법이 사용된다. 모델 데이터에 대해 여러개의 LOD 데이터를 미리 구축해두고 순차적으로 로딩하는 것인데, 이 LOD 데이터에는 지오메트리 뿐만 아니라 GPU에 최적화된 텍스쳐 이미지 데이터로의 처리가 되어 매우 빠르게 렌더링된다. 그 결과는 다음과 같다.

동일한 네트워크 환경에서 같은 품질의 모델 데이터를 로딩하는 상황인데, 앞서 봤던 것보다 훨씬 더 빠르게 모델이 표시되는 것을 알 수 있다.

다행히도 모델 데이터에 대한 프로그래시브 로딩을 위해 처음부터 개발할 필요는 없다. @needle-tools/gltf-progressive 패지키를 사용하면 매우 쉽게 만들 수 있다. 한번 알아보자.

먼저 모델 데이터를 점진적 로딩이 될 수 있게 변환해줘야 한다. 변환 프로그램은 다음처럼 임시로 설치해 이용할 수 있다.

npx @needle-tools/gltf-build-pipeline@latest

변환하고자 하는 모델 데이터가 MODEL1.glb라면 이를 점진적 로딩을 위한 모델 데이터로 생성하여 PROGRESSIVE_MODEL1 폴더에 저장해 주는 명령은 다음과 같다.

npx @needle-tools/gltf-build-pipeline@latest transform ./public/MODEL1.glb ./PROGRESSIVE_MODEL1

해당 폴더에는 다음처럼 여러 개의 glb 파일이 생성된다. 총 5개의 LOD 단계로 생성된 지오메트리와 텍스쳐에 대한 데이터이다.

이제 이렇게 만들어진 모델 데이터를 three.js에서 렌더링해서 시각화하는 코드를 살펴보자. 먼저 다음과 같은 패키지의 설치가 필요하다.

npm i @needle-tools/gltf-progressive

그리고 코드는 다음과 같다.

import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { useNeedleProgressive } from "@needle-tools/gltf-progressive";

const loader = new GLTFLoader();
useNeedleProgressive(loader, renderer); // 플러그인 등록 (1회)

loader.load('./PROGRESSIVE_MODEL1/MODEL1.glb',
  (gltf) => {
    const model = gltf.scene;
    this._scene.add(model);
  }
);

적용이 매우 쉽다. 하지만 안타깝게도 @needle-tools/gltf-progressive는 WebGL 환경만을 지원한다. WebGPU 환경에 대한 지원도 곧 기대해본다.

`three.js 프로젝트 구성` SKILL

이 스킬은 전역 스킬로 사용되어야 하므로 ~/.gemini/antigravity/skills/<skill-folder>/ 에 입력(threejs-project-setup/SKILL.md)되어야 함.

---
name: threejs-project-setup
description: >
  - 사용자가 three.js에 대한 프로젝트를 구성 또는 생성해 달라는 요청이 있을때 사용한다.
  - 프롬프트에는 프로젝트를 구성할 경로나 WebGL 기반인지 WebGPU 기반인지에 대한 내용이 있을 수 있다. 
---

## 실행순서

이 스킬은 다음 단계로 순차적으로 실행한다.

1. 사용자가 three.js에 대한 프로젝트를 구성 또는 생성해 달라고 요청하면 다음 bash 명령을 실행한다.
    - 사용자가 WebGPU 기반을 언급한 경우 : `git clone https://github.com/GISDEVCODE/threejs-webgpu-with-javascript-starter.git {경로}`
    - 사용자가 WebGL 기반을 언급한 경우 : `git clone https://github.com/GISDEVCODE/threejs-with-javascript-starter.git {경로}`
    - 사용자가 WebGL과 WebGPU에 대해 언급하지 않은 경우 WebGPU를 언급한 것으로 간주한다.
    - 프로젝트를 구성할 {경로}를 사용자가 지정하지 않았을 경우 현재 경로(`.`)로 간주한다.
    - 프로젝트를 구성하는 경로에 어떠한 파일이나 폴더가 존재할 경우 이 스킬의 실행을 중지하고 사용자에게 "프로젝트를 구성하는 폴더에는 어떠한 파일이나 폴더도 존재해서는 안됩니다."라는 메세지를 밝은 빨강색으로 프롬프트 대화창에 출력한다.
1. 구성된 프로젝트에 대한 최신 패키지 업데이트를 위해 다음 bash 명령을 실행한다.
    - `npx npm-check-updates -u`
    - 이 bash 명령은 {경로}에서 실행되어야 함
1. 패키지 설치를 위해 다음 bash 명령을 실행한다.
    - `npm i`
    - 이 bash 명령은 {경로}에서 실행되어야 함
1. 개발 서버 실행을 위해 다음 bash 명령을 실행하지 말고 사용자에게 안내한다. `npm run dev`
1. 위의 실행이 모두 완료 되었다면 사용자의 요구가 없다면 구성된 프로젝트를 절대 분석하지말고 어떠한 변경을 시도하지마. 오직 사용자에게 "Happy three.js Coding!"이라면 메세지를 밝은 초록색으로 프롬프트 대화창에서 출력한다.

아래는 이 스킬을 이용한 프로젝트 구성 예시