node로 실행한 서버 스크립트를 데몬(damon)으로 실행하기

nohup과 &의 조합으로 실행해도 해당 터미널을 종료하면 실행한 서버 스크립트가 같이 종료되 버리는 문제가 있습니다. 이 경우 forever란 패지키를 npm으로 설치해서 이용하면 됩니다.

npm i -g forever

그리고 index.js를 실행하고 싶다면 다음처럼 입력합니다.

forever start index.js

잘 실행되고 있는지 확인하기 위해서는 다음 명령을 입력합니다.

forever list

forever로 실행한 것을 종료하기 위해서는 다음처럼 명령을 입력합니다.

forever stop index.js

forever로 실행한 모든 프로그램을 종료하기 위해서는 다음과 같습니다.

forever stopall

Vite로 개발된 웹앱 배포 시 주의점

Vite로 구성된 웹 프로젝트를 배포할 때 npm run build로 배포본을 생성하는데, 이때 경로(path)가 틀려 필요한 리소스를 불러오지 못하는 에러가 발생합니다. vite.config.js 파일에 경로를 명시적으로 지정해주면 됩니다.

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react()],
  base: "./",
})

위의 코드 중 7번째 줄을 추가했는데요. 이처럼 base를 지정하지 않으면 상대 경로가 / 로 잡힙니다.

이 부분 이외에도 문자열로 구성된 데이터 파일에 대한 URL에 대해서 주의를 해야 하는데요. 예를들어 glb 모델 파일을 로드할때 /가 아닌 ./로 지정해야 배포시에 데이터를 로드할 수 있습니다.

// 개발에서는 문제가 없지만 배포에서는 문제가 발생함
useGLTF.preload('/city_2.glb')

// 개발에서도, 배포에서도 문제가 없음
useGLTF.preload('./city_2.glb')

Unknown property ‘?’ found 대처

React에서 R3F를 이용해 JavaScript로 개발할 때 아래의 그림처럼 프로퍼티에 빨간펜 선생님의 등장을 경험할 수 있습니다.

좋은 방법은 아니지만 Lint 옵션 중 react/no-unknown-property 속성을 비활성화 하면 해결 됩니다.

import js from '@eslint/js'
...

export default [
  { ignores: ['dist'] },
  {
    files: ['**/*.{js,jsx}'],
    ...
    rules: {
      ...js.configs.recommended.rules,
      ...
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true },
      ],
      'react/no-unknown-property': 'off'
    },
  },
]

위의 코드는 eslint.config.js 파일의 내용인데, 16번째 코드를 새롭게 추가했습니다.

three.js, 3D 모델에 대한 라벨 시각화

3D 모델에 대한 효과적은 라벨 시각화는 그 목표에 따라 매우 주관적이고 다양하게 접근할 수 있습니다. 이 글에서 제공되는 라벨 시각화 역시 많은 다양한 방법 중에 하나인데요. 목표는 3D 모델을 최대한 가리지 않아야 하며 라벨 사이의 충돌을 최소화 해야 합니다. 이런 목표에 따라 만든 기능은 아래와 같습니다.

위의 기능을 프로젝트에 빠르고 쉽게 적용하기 위해 SmartLabel이라는 이름으로 컴포넌트로 만들었는데요. 이 컴포넌트를 적용할 때 고려해야할 코드를 정리하면 다음과 같습니다.

먼저 SmartLabel 컴포넌트와 라벨 관련 데이터를 생성하고 설정해야 합니다.

  _setupLabels() {
    const smartLabel = new SmartLabel(this._scene, this._camera);
    this._smartLabel = smartLabel;

    const labels = {
      "Object": {
        label: "미할당영역",
        textColor: "gray",
      },
      "Part1": {
        label: "정문 계단",
        textColor: "white",
      },
      "Part2": {
        label: "정문",
        textColor: "white",
      },
      "Part3": {
        label: "파손영역A",
        textColor: "red",
      },
      "Part4": {
        label: "파손영역B",
        textColor: "red",
      },
      
      ...
    }

    this._smartLabel.setLabelData(labels);
  }

labels 객체의 key는 Mesh의 이름이고 Value는 표시될 라벨과 텍스트 색상입니다. 라벨이 표시될 Mesh의 지정은 다음과 같습니다.

_setupModel() {
  const loader = new GLTFLoader();
  loader.load("./model.glb", gltf => {
    const object = gltf.scene;
    this._scene.add(object);
    const meshes = [];

    object.traverse(obj => {
      if (obj.isMesh) meshes.push(obj);
    });

    this._smartLabel.setTargetMeshs(meshes);
    this._smartLabel.buildLabels();

    ...
  });
}

매 프레임마다 다음과 같은 코드 호출이 필요합니다.

update() {
  ...

  this._smartLabel.updateOnFrame();
}

렌더링 시 다음과 같은 코드 호출이 필요하구요.

render() {
  this.update();

  ...

  this._smartLabel.render();

  requestAnimationFrame(this.render.bind(this));
}

렌더링 되는 영역의 크기가 변경되면 다음 코드 호출이 필요합니다.

resize() {
  const width = this._divContainer.clientWidth;
  const height = this._divContainer.clientHeight;

  ...  

  this._smartLabel.updateSize(width, height);
}

라벨은 DOM 요소입니다. 이에 대한 스타일이 필요할 수 있는데, 위의 예시의 경우 다음과 같습니다.

.label {
  font-size: 1em;
  border: 2px solid yellow;
  padding: 0.3em 0.8em;
  border-radius: 0.9em;

  background-color: black;
  transition: transform 0.2s ease-out, opacity 1s ease;
  box-shadow: 0 0 5px rgba(0,0,0,0.5);
}