three.js의 내장 Inspector 코드 정리

내장 인스펙터 UI의 설치는 다음과 같습니다.

import { Inspector } from 'three/addons/inspector/Inspector.js';

...

renderer.inspector = new Inspector();
this.domApp.appendChild(renderer.domElement);

제어하고자 하는 객체는 다음과 같습니다.

const obj = {
  bVisible: false,

  value: 0.5,

  color: new THREE.Color(0xff0000),
  // color: 0xffff00,
  // color: '#ff00ff',

  item: 'glass'
}

객체에 대한 각 필드를 제어하는 UI의 추가는 다음과 같습니다.

const gui = this.renderer.inspector.createParameters('파라메터 설정1');

gui.add(obj, 'bVisible').name('obj의 bVisible')/*.onChange(v => { console.log(v) });*/
gui.add(obj, 'value', 0, 1, 0.01).name('obj의 value')/*.onChange(v => { console.log(v) });*/
gui.addColor(obj, 'color').name('obj의 color')/*.onChange(v => { console.log(v) });*/
gui.add(obj, 'item', [ 'duck', 'glass' ]).name('obj의 item')/*.onChange(v => { console.log(v) });*/

결과 UI는 다음과 같습니다.

아래와 같은 three.js 최신버전에서의 인스펙터 이슈가 있습니다.

three.js r184의 Inspector UI 적용시

THREE.InterleavedBuffer 설명 및 예제코드

THREE.InterleavedBuffer는 단일 배열에 용도가 다른 데이터를 섞어 저장해 사용할 수 있도록 해주는 클래스로써 메모리와 퍼포먼스 양쪽 측면 모두에 대해 최적화할 수 있습니다.

예를들어 다음처럼 하나의 배열에 정점의 위치와 색상 값을 저장하고 있습니다.

// 1. 하나의 배열에 위치(3)와 색상(3) 데이터를 교차해서 넣습니다.
// 구조: [ X, Y, Z,  R, G, B, ... ]
const arrayBuffer = new Float32Array([
  // 좌하단 정점
  -1.0, -1.0, 0.0, 1.0, 0.0, 0.0, // Pos(XYZ), Color(RGB)
  // 우하단 정점
  1.0, -1.0, 0.0, 0.0, 1.0, 0.0,
  // 우상단 정점
  1.0, 1.0, 0.0, 0.0, 0.0, 1.0,
  // 좌상단 정점
  -1.0, 1.0, 0.0, 1.0, 1.0, 0.0
]);

위의 배열 하나에 대해 정점과 색상에 대한 데이터를 별도의 Attribute로 쪼개 GPU에 전달하기 위해 THREE.InterleavedBuffer 객체를 생성합니다.

// 2. InterleavedBuffer 생성
// 하나의 정점 데이터 크기(Stride)는 6 (XYZ + RGB)입니다.
const interleavedBuffer = new THREE.InterleavedBuffer(arrayBuffer, 6);

이제 지오메트리 객체를 생성해서 앞서 만든 THREE.InterleavedBuffer 객체를 통해 정점과 색상에 대한 Attribute 데이터를 주입합니다.

// 3. BufferGeometry 생성 및 InterleavedBufferAttribute 연결
const geometry = new THREE.BufferGeometry();

// 위치(position) 속성: 데이터 크기는 3(XYZ), 시작 오프셋은 0
geometry.setAttribute('position', new THREE.InterleavedBufferAttribute(interleavedBuffer, 3, 0));

// 색상(color) 속성: 데이터 크기는 3(RGB), 시작 오프셋은 3 (XYZ 다음이므로)
geometry.setAttribute('color', new THREE.InterleavedBufferAttribute(interleavedBuffer, 3, 3));

인덱스 데이터도 주입해야겠죠.

// 4. 인덱스 버퍼 설정 (삼각형 2개로 사각형 구성)
const indices = new Uint16Array([
  0, 1, 2,
  0, 2, 3
]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));

이제 Mesh를 생성합니다.

// 5. 메쉬 생성 (정점 색상을 사용하기 위해 vertexColors: true 설정)
const material = new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geometry, material);

this.scene.add(mesh);

THREE.InterleavedBufferd의 장점은 다음과 같습니다.

  • GPU 캐시 효율성: GPU가 화면에 점을 그릴 때, 특정 정점의 위치를 읽어오면서 바로 옆에 있는 색상 데이터도 함께 캐시에 로드하므로 메모리 접근 속도가 빨라집니다.
  • 단일 버퍼 관리: 여러 개의 버퍼를 바인딩할 필요 없이 하나의 큰 버퍼만 GPU에 넘겨주면 되므로 오버헤드가 줄어듭니다.

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 폴리곤 단순화