프론트엔드 개발자라면 3d로 화려하게 만들어진 홈페이지를 보면서 한번쯤은 손이 드릉드릉했을거라고 생각한다.
그리고 언젠가는 나도 저런 앱(웹)을 만들어보리라 목표로 삼았을 것이다.
쓰리디 춘식이가 맵을 돌아다니면서 컨텐츠를 보여주는 웹을 보면서 너무너무 만들어보고싶었던 기억이 난다.
뭐, 나는 그렇다.
계속 생각만 하다가 개발에 대한 재미가 떨어져서(계속 새로운 것을 넣어줘야하는 새럼 나야나 🙌) r3f를 건들여보기로 했다!! 예이 생각만 해도 신난다
r3f는 한글로 된 자료가 없다. 유데미에도 강의가 많지 않음 😭
three.js를 배워보는 방법도 있지만 리액트 프로젝트에 바로 써먹어보고싶엇..!
영어강의를 보면서 진행해보려다가 위 링크의 웹페이지를 따라가는 내용이라는 사실을 깨달아서, 블로그에 한글로 정리하면서 따라가보려 한다.
$ mkdir react-three-fiber-boilerplate
$ cd react-three-fiber-boilerplate
{
"name": "react-three-fiber-boilerplate",
"version": "1.0.0",
"description": "",
"keywords": [],
"main": "src/index.jsx",
"dependencies": {
"@react-three/fiber": "8.13.5",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-scripts": "5.0.1",
"three": "0.154.0"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": {
"production": [">0.2%", "not dead", "not op_mini all"],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { Canvas } from '@react-three/fiber'
import './styles.css'
createRoot(document.getElementById('root')).render(
<StrictMode>
<Canvas camera={{ position: [0, 0, 2] }}>
<mesh>
<boxGeometry />
<meshBasicMaterial color={0x00ff00} wireframe />
</mesh>
</Canvas>
</StrictMode>
)
html,
body,
#root {
height: 100%;
margin: 0;
background: #000000;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<title>React Three Fiber Tutorials by Sean Bradley</title>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
</body>
</html>
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"jsxBracketSameLine": true
}
{
"extends": "react-app",
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"no-unused-vars": [
"warn",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
]
}
}
canvas
와 mesh
를 컴포넌트화한다.
export default function Box() {
return (
<mesh>
<boxGeometry />
<meshBasicMaterial color={0x00ff00} wireframe />
</mesh>
)
}
import { Canvas } from '@react-three/fiber'
import Box from './Box'
export default function App() {
return (
<Canvas camera={{ position: [0, 0, 2] }}>
<Box />
</Canvas>
)
}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './styles.css'
import App from './App'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
)
Box가 2개 이상일 경우, 위치(position)을 부모 컴포넌트로부터 받을 수 있도록 props를 뚫는다.
export default function Box(props) {
return (
<mesh {...props}>
<boxGeometry />
<meshBasicMaterial color={0x00ff00} wireframe />
</mesh>
)
}
import { Canvas } from '@react-three/fiber'
import Box from './Box'
export default function App() {
return (
<Canvas camera={{ position: [0, 0, 2] }}>
<Box position={[-0.75, 0, 0]} name="A" />
<Box position={[0.75, 0, 0]} name="B" />
</Canvas>
)
}
런타임 중 컴포넌트 인스턴스의 속성을 읽거나 수정할 때 ref를 이용할 수 있다.
ref를 사용해 컴포넌트 인스턴스의 내부 프로퍼티 및 메서드와 필수적으로 상호 작용하는 데 사용할 수 있다.
useRef를 사용할 때, 초기값을 설정하지 않으면 jsx가 렌더링 될 때 초기화될 때까지 undefined가 되어버린다.
initValue를 설정해 ref 값이 초기화될 때까지 기본값을 갖고 작동할 수 있도록 한다.
import { useRef } from 'react'
export default function Box(props) {
const ref = useRef()
console.log(ref) // { current : undefined } { current : undefined }
return (
<mesh {...props} ref={ref}>
<boxGeometry />
<meshBasicMaterial color={0x00ff00} wireframe />
</mesh>
)
}
ref.current 값이 있는지 여부를 확인하기 위해 useEffect를 사용한다.
useEffect는 컴포넌트가 생성된 후에 실행된다.
import { useRef, useEffect } from 'react'
export default function Box(props) {
const ref = useRef()
useEffect(() => {
console.log(ref.current)
})
return (
<mesh {...props} ref={ref}>
<boxGeometry />
<meshBasicMaterial color={0x00ff00} wireframe />
</mesh>
)
}
ref.current
Mesh {isObject3D: true, uuid: '2bf1c713-2ce7-4bd5-a019-a0564938b3a6', name: 'B', type: 'Mesh', parent: Scene, …}
useEffect는 비동기적이다. callback이 캔버스에 WebGL 픽셀이 그려진 첫 타임에 호출될 수 있다.
R3F는 웹gl 데이터를 트리거해 캔버스에 그린다. 그래서 mesh의 위치가 변경될 때와 같은 상황에서 딜레이가 발생하지 않는다.
frameloop="demand"
<canvas frameloop="demand"></canvas>
위와 같은 상황에서는 딜레이(깜빡임)가 발생한다. 이때는 useEffect대신 useLayoutEffect를 사용하면 된다. 브라우저가 페인팅을 하기 전에 useLayoutEffect가 동기적으로 호출된다.
import { Canvas } from '@react-three/fiber'
import Box from './Box'
export default function App() {
return (
<Canvas camera={{ position: [0, 0, 2] }} frameloop="demand">
<Box position={[-0.75, 0, 0]} name="A" />
<Box position={[0.75, 0, 0]} name="B" />
</Canvas>
)
}
새로고침하면 위치(y)가 0에서 1로 수정되면서 오른쪽 상자가 깜박인다.
import { useRef, useEffect } from 'react'
export default function Box(props) {
const ref = useRef()
useEffect(() => {
if (ref.current.name === 'B') {
ref.current.position.y = 1
}
})
return (
<mesh {...props} ref={ref}>
<boxGeometry />
<meshBasicMaterial color={0x00ff00} wireframe />
</mesh>
)
}
useLayoutEffect 사용시 깜빡이지 않고 상자의 위치가 바뀐다.
대신 성능저하가 발생할 수도 있으니 될수있으면 useEffect를 쓰는 게 좋다.
import { useRef, useLayoutEffect } from 'react'
export default function Box(props) {
const ref = useRef()
useLayoutEffect(() => {
if (ref.current.name === 'B') {
ref.current.position.y = 1
}
})
return (
<mesh {...props} ref={ref}>
<boxGeometry />
<meshBasicMaterial color={0x00ff00} wireframe />
</mesh>
)
}
state와 delta를 제공한다.
10ms마다 10씩 x좌표를 움직임
ref.current.position.x += 10 * delta
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
export default function Box(props) {
const ref = useRef()
useFrame((_, delta) => {
ref.current.rotation.x += 1 * delta
ref.current.rotation.y += 0.5 * delta
})
return (
<mesh {...props} ref={ref}>
<boxGeometry />
<meshBasicMaterial color={0x00ff00} wireframe />
</mesh>
)
}
위에서 작성했던 'frameloop="demand"'를 삭제해야 box가 돌아간다.
import { Canvas } from '@react-three/fiber'
import Box from './Box'
export default function App() {
return (
<Canvas camera={{ position: [0, 0, 2] }}>
<Box position={[-0.75, 0, 0]} />
<Box position={[0.75, 0, 0]} />
</Canvas>
)
}
이벤트를 캡처할 때 이벤트에 대한 정보들이 콜백으로 전달된다. (useRef 훅을 따로 만들 필요가 없다.)
이벤트에 대한 정보
객체를 클릭하면 해당 객체에 대한 정보,
클릭이 발생한 3D 벡터,
클릭이 발생한 카메라로부터의 거리,
객체의 어느 면을 클릭했는지,
클릭의 UV 좌표 등
포인터 이벤트는 React Three Fiber 캔버스가 인스턴스화될 때 자동으로 생성되는 레이캐스터(raycaster)에 의존한다.
import { useRef } from 'react'
import { useFrame } from '@react-three/fiber'
export default function Box(props) {
const ref = useRef()
useFrame((_, delta) => {
ref.current.rotation.x += 1 * delta
ref.current.rotation.y += 0.5 * delta
})
return (
<mesh
{...props}
ref={ref}
onPointerDown={(e) => console.log('pointer down ' + e.object.name)}
onPointerUp={(e) => console.log('pointer up ' + e.object.name)}
onPointerOver={(e) => console.log('pointer over ' + e.object.name)}
onPointerOut={(e) => console.log('pointer out ' + e.object.name)}
onUpdate={(self) => console.log(self)}
>
<boxGeometry />
<meshBasicMaterial color={0x00ff00} wireframe />
</mesh>
)
}
mesh가 겹칠 경우 마우스 이벤트를 감지할 때 뒤에 있는 mesh까지 캡쳐된다.
이 현상을 방지하기 위해서는 stopPropagation() 메서드를 이벤트 핸들러에서 사용한다.
import { useRef, useState } from 'react'
import { useFrame } from '@react-three/fiber'
export default function Box(props) {
const ref = useRef()
const [hovered, setHover] = useState(false)
const [rotate, setRotate] = useState(false)
useFrame((_, delta) => {
if (rotate) {
ref.current.rotation.x += 1 * delta
ref.current.rotation.y += 0.5 * delta
}
})
return (
<mesh
{...props}
ref={ref}
scale={hovered ? [1.1, 1.1, 1.1] : [1, 1, 1]}
onPointerDown={() => setRotate(!rotate)} //click
onPointerOver={() => setHover(true)} //hover
onPointerOut={() => setHover(false)}
>
<boxGeometry />
<meshBasicMaterial color={hovered ? 0xff0000 : 0x00ff00} wireframe />
</mesh>
)
}
import { useRef, useState } from 'react'
import { useFrame } from '@react-three/fiber'
export default function Box(props) {
const ref = useRef()
const [rotate, setRotate] = useState(false)
useFrame((_, delta) => {
ref.current.rotation.x += delta * rotate
ref.current.rotation.y += 0.5 * delta * rotate
})
return (
<mesh {...props} ref={ref} onPointerDown={() => setRotate(!rotate)}>
<boxGeometry />
<meshBasicMaterial color={'lime'} wireframe />
</mesh>
)
}
geometry의 uuid를 확인해보면 동일한 geometry 객체를 사용하고 있다는 것을 알 수 있다.
import { useRef, useState, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'
export default function Box(props) {
const ref = useRef()
const [rotate, setRotate] = useState(false)
useEffect(() => {
console.log(ref.current.geometry.uuid)
})
useFrame((_, delta) => {
ref.current.rotation.x += delta * rotate
ref.current.rotation.y += 0.5 * delta * rotate
})
return (
<mesh {...props} ref={ref} onPointerDown={() => setRotate(!rotate)}>
<boxGeometry />
<meshBasicMaterial color={'lime'} wireframe />
</mesh>
)
}
boxGeometry를 동적으로 바꿀 수 있다.
<boxGeometry />
를 사용하지 않는다.import { useRef, useState, useEffect } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
export default function Box(props) {
const ref = useRef()
const [rotate, setRotate] = useState(false)
const geometry = new THREE.BoxGeometry()
useEffect(() => {
console.log(ref.current.geometry.uuid)
})
useFrame((_, delta) => {
ref.current.rotation.x += delta * rotate
ref.current.rotation.y += 0.5 * delta * rotate
})
return (
<mesh
{...props}
ref={ref}
onPointerDown={() => setRotate(!rotate)}
geometry={geometry}
>
<meshBasicMaterial color={'lime'} wireframe />
</mesh>
)
}
import { useRef, useState, useEffect, useMemo } from 'react'
import { useFrame } from '@react-three/fiber'
import * as THREE from 'three'
export default function Box(props) {
const ref = useRef()
const [count, setCount] = useState(0)
const geometry = useMemo(
() => [new THREE.BoxGeometry(), new THREE.SphereGeometry(0.785398)],
[]
)
useEffect(() => {
console.log(ref.current.geometry.uuid)
})
useFrame((_, delta) => {
ref.current.rotation.x += delta
ref.current.rotation.y += 0.5 * delta
})
return (
<mesh
{...props}
ref={ref}
onPointerDown={() => setCount((count + 1) % 2)}
geometry={geometry[count]}
>
<meshBasicMaterial color={'lime'} wireframe />
</mesh>
)
}
좋은 글 감사합니다.