useRef로 요소가 참조될 때 다시 컴포넌트 렌더링하기

React는 선언형 프로그래밍을 지향합니다. 어떤 시각적인 요소를 자식으로 선언해서 웹브라우저에 표시하는데요. 어떤 요소를 참조하기 위해 useRef 훅을 사용합니다. 그런데 처음 컴포넌트가 마운트 될때 useRef로 참조하고자 하는 요소가 아직 참조되지 않을 때가 있습니다. 이럴 때 useRef로 어떤 요소가 제대로 참조될 때 다시 컴포넌트를 렌더링해 줄 필요가 있는데요.

import { OrbitControls } from "@react-three/drei"
import { useRef, useState } from "react";
import * as THREE from "three"

function Outline(props) {
  const material = useMemo(
    () => new THREE.LineBasicMaterial({ color: 0xffff00 }),
    []
  ) 

  return <lineSegments 
    geometry={props.geometry} 
    material={material}
  />
}

function MyElement3D() {
  const refMesh = useRef()
  const [, setRefAllocated ] = useState(false)

  return (
    <>
      <OrbitControls />

      <ambientLight intensity={0.1} />
      <directionalLight position={[2, 1, 3]} intensity={0.5} />

      <mesh 
        ref={self => { refMesh.current = self; setRefAllocated(true) }}
        position={[-1.2, 0, 0]}>
        <boxGeometry args={[1,1,1,10,10,10]} />
        <meshStandardMaterial color="#8e44ad" />
        <Outline geometry={refMesh.current?.geometry} />
      </mesh>
    </>
  )
}

export default MyElement3D

상관없는 코드가 90%고 이 글과 관련된 코드가 10%인데요. 14번에 useRef에 대한 refMesh가 있고, refMesh가 참조하는 것이 25번 코드에 보입니다. 그런데 ref에 대한 지정을 함수로 지정하고 있다는 점입니다. refMesh에 요소가 참조되면 15번 코드에서 만들어 둔 상태 변경 함수를 호출하는데요. React에서 상태 변경은 컴포넌트 함수의 재실행을 의미하므로 refMesh에 어떤 요소가 할당되면 다시 컴포넌트를 렌더링하도록 유도합니다.

R3F에서 Shader를 통한 Material

먼저 다음처럼 drei의 shaderMaterial를 이용해 GLSL로 재질을 만들 수 있습니다.

import { shaderMaterial } from '@react-three/drei'

const WaveShaderMaterial = shaderMaterial(
  {
    uColor: new THREE.Color(1, 0, 0)
  },

  /* glsl */`
    varying vec2 vUv;

    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,

  /* glsl */`
    uniform vec3 uColor;
    varying vec2 vUv;

    void main() {
      gl_FragColor = vec4(vUv.y * uColor, 1.0);
    }
  `
)

리엑트는 선언형 프로그래밍(?) 방식을 권장하므로 위에서 만든 WaveShaderMaterial을 Tag처럼 선언해서 사용할 수 있도록 해야 합니다. 이때 R3F의 extend가 사용됩니다.

import { Canvas, extend } from '@react-three/fiber'

extend({ WaveShaderMaterial })

실제 사용은 다음과 같습니다.

const MyCanvas = () => {
  return (
    <Canvas>
      <pointLight position={[10,10,10]} />
      <mesh>
        <planeGeometry args={[5,5]} />
        <waveShaderMaterial uColor={"white"} />
      </mesh>
    </Canvas>
  )
}

function App() {
  return (
    <MyCanvas />
  )
}

결과는 다음과 같습니다.

1Day React(리엑트)

“하루에 배우는 리엑트 기본” 중 아래 15개의 영상이 리엑트의 기본 내용을 정리한 것입니다. 전체 시간은 100분 정도니 단디 맘 먹고 집중해 학습하면 하루에 다 이해할 수 있는 내용이므로 학습 후 바로 React 실무에 풍덩 빠져 치열하게 싸워 보시기 바랍니다.

















#GWC UI Library : ValueRangesColorMatcher

웹 UI 라이브러리인 GWC에서 제공하는 ValueRangesColorMatcher 컴포넌트에 대한 예제 코드입니다.

먼저 DOM 구성은 다음과 같습니다.

그리고 CSS 구성은 다음과 같구요.

.center {
    display: flex;
    width: 100%;
    height: 100%;
    justify-content: center;
    align-items: center;
    gap: 1em;
}

#matcher {
    width: 30em;
    height: 15em;
}

js 코드는 다음과 같습니다.

window.onload = () => {
    const data = [ 165, 120, 160, 200, 135, 115, 100, 125, 135, 190, 156, 130 ]
    const rangeColors = [
        { ranges: [100, 120], color: '#00ff00'},
        { ranges: [120, 141], color: '#88ff00'},
        { ranges: [141, 160], color: '#ffff00'},
        { ranges: [160, 180], color: '#ff8800'},
        { ranges: [180, 200], color: '#ff0000'}
    ]

    matcher.defineData(data, rangeColors)

    document.querySelector(".btn-get").addEventListener("click", () => {
        alert(JSON.stringify(matcher.valueRangeColors, null, 4))
    })

    GeoServiceWebComponentManager.instance.update();
};

실행 결과는 다음과 같습니다.

소수점을 설정할 수 있으며 속성은 toFixed 입니다.

잡음(Noise) 시각화

무작위 값을 얻기 위한 흔한 방법은 random API 사용인데, 실제로 무작위 값을 얻기 위해 많이 사용하는 것은 Noise라는 개념이고 이 Noise를 얻기 위한 몇가지 알고리즘 중 특허에 대한 저작권 침해에 대한 문제가 없고 또 품질면에서 Simplex Noise가 많이 사용됩니다.

아래는 Simplex Noise에 대한 시각화에 대한 결과물로 누군가의 유튜브 채널에 올라온 영상의 내용입니다.

three.js를 이용해 시각화한 경우로 다음과 같은 라이브러리를 사용하고 있습니다.

"devDependencies": {
  "vite": "^4.3.9"
},
"dependencies": {
  "simplex-noise": "^4.0.1",
  "three": "^0.153.0",
  "three-fatline": "^0.6.1"
}

그리고 주요 코드는 다음과 같습니다.

...

const noise2D = createNoise2D()

function createLines() {
  for(let r=-20; r<20; r++) {
    const wnoise = noise2D(0, r*0.125) * 1.0
    const lineWidth = 0.25 + Math.pow(wnoise * 0.5 + 1, 2)

    const dashed = Math.random() > 0.5
    const dashScale = 1
    const dashSize = Math.pow(Math.random(), 2) * 15 + 4
    const gapSize = dashSize * (0.5 + Math.random() * 1)
    
    const material = new LineMaterial({
      color: "rgb(241, 231, 222)",
      linewidth: lineWidth,
      resolution: new Vector2(res, res),
      dashed,
      dashScale,
      dashSize,
      gapSize
    })

    const vertices = []
    for(let i=0; i<100; i++) {
      let height = 0
  
      height += noise2D(i * 0.0189 * 1, r * 0.125) * 2.0
      height += noise2D(i * 0.0189 * 2, r * 0.125) * 1.0
      height += noise2D(i * 0.0189 * 4, r * 0.125) * 0.5
      height += noise2D(i * 0.0189 * 8, r * 0.125) * 0.25
      height += noise2D(i * 0.0189 * 16, r * 0.125) * 0.125
  
      vertices.push(
        -330 + 660 * (i / 100), 
        height * 20 + r * 16, 
        0
      )
    }  

    const geometry = new LineGeometry()
    geometry.setPositions(vertices)
  
    const line = new Line2(geometry, material)
    line.computeLineDistances()
  
    scene.add(line)
  }
}

...

완전한 코드는 앞서 언급해 드린 유튜브 영상을 보시기 바랍니다.