IT/3D Web

React 3D 웹앱 - 2. Three.js

루벤초이 2021. 3. 11. 21:21

React 3D 웹앱 Basic 시리즈입니다.

Github

 


이전 시간엔 React 개발 환경과 React 앱 구조에 대해 알아봤습니다.

 

이번 시간에는 3D를 그려봅니다. 3D 기술을 단번에 이해하기는 쉽지 않아요. 그렇다고 이론만 깊게 들여다 보면 질려버리죠. 매일 조금씩 꾸준히 공부하다 보면 질리지 않고 어느새 3D에 익숙해진 당신을 보게 될 겁니다. 저의 미흡한 시리즈를 보면서 따라하는 것도 좋은 방법이겠네요!

 

예제 코드(full version)


Three.js

오늘의 재료는 three.js 라는 자바스크립트 라이브러리인데요, 웹에서 3D를 쉽게 구동해주는 유용한 라이브러리입니다.

일단 구경한 번 하고 올까요?

 

Three.js – JavaScript 3D library

 

threejs.org

한때 3D 프로그래밍은 Unity나 Unreal 같은 게임용 소프트웨어로만 할 수 있는 것처럼 여겨졌어요. 그러던 중 GPU의 발전에 힘입어 브라우저에서도 3D를 사용할 수 있는 WebGL과 three.js 같은 오픈 소스 프레임워크를 통해 이제는 웹 브라우저에서도 쉽게 3D 프로그래밍을 할 수 있게 되었죠.

 

특히 three.js는 WebGL을 잘 몰라도 쉽게 3D를 개발할 수 있게 해주는 고마운 자바스크립트 라이브러리에요. React 뿐만 아니라 자바스크립트 기반 개발에서는 three.js가 가장 널리 사용됩니다.


우선 실행부터 시켜봅시다.

 

Three.js 공식 튜토리얼에서는 순수 자바스크립트 코드를 다루는데, 이 순수 자바스크립트 코드를 React에서 그대로 사용할 수는 없어요. 그렇다면 React에서는 어떻게 three.js를 쓸까요?

 

구글에서 'react threejs'를 검색하면 react-three-fiber, react-threejs 등 이것저것 많이 나오는데요, 우리는 three.js 원본을 그대로 npm으로 옮겨온 npm three.js를 사용합니다.

 

설치하기

지난 편에서 만들었던 프로젝트 ruben-app에서 node 쉘(터미널)을 열고 아래와 같이 치면 프로젝트에 추가됩니다.

  • npm install --save three

Tip> 제대로 설치되었다면 package.json 안에 dependencies 안에 "three" : "^0.126.1" 줄이 추가됩니다. 콜론 뒤에는 버전이라 여러분 파일과 다를 수 있어요. 참고로 꺽쇠(^) 표시가 있으면 이 버전과 유사한 버전을 설치하라는 의미입니다.
꺽쇠가 없으면 반드시 그 버전을 설치하라는 의미이고요.

 

컴포넌트 만들기

순수 자바스크립트 코드 예제를 리액트에 적용해봅시다. 우선 아래와 같이 src/viewer/Viewer.js 컴포넌트를 만들어요.

import React from 'react';
import * as THREE from 'three';

class Viewer extends React.Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    const width = window.innerWidth - 1;
    const height = window.innerHeight - 1;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, width / height, 1, 1000);

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(width, height);

    this.element.appendChild(renderer.domElement);

    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    camera.position.z = 5;

    this.scene = scene;
    this.camera = camera;
    this.renderer = renderer;
    this.cube = cube;
    this.animate();
  }

  animate = () => {
    this.renderer.render(this.scene, this.camera);
    this.cube.rotation.x += 0.01;
    this.cube.rotation.y += 0.01;
    requestAnimationFrame(this.animate);
  }

  render() {
    return (
      <div ref={el => this.element = el} style={{ width: '100%', height: '100%', border: '1px solid red' }} />
    );
  }
}

export default Viewer;

이것을 빌드 npm install 후 실행 npm start하면 다음과 같은 화면이 나타날 거에요.

 

실행 화면

 

 

분석

자, 이제 예제를 분석해볼까요?

 

앱이 실행되면 최초 생성자 constructor()가 불리는데 우리 코드에선 딱히 별 내용이 없죠. 그 다음에는 componentDidMount()가 불립니다. 그 안을 살펴볼까요?

    const width = window.innerWidth - 1;
    const height = window.innerHeight - 1;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(75, width / height, 1, 1000);

    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(width, height);

웹 3D는 HTML Canvas에 그려지는데요, 먼저 크기를 정해야겠죠? 예제에서는 편의상  윈도우 크기(window.innerWidth, window.innerHeight)으로 지정합니다. 그 다음에는 camera, scene과 renderer를 생성하는데요, 이것들은 말 그대로 3D 장면을 구성하는

  • camera: 말 그대로 카메라
  • scene: 카메라로 찍으려는 3D 무대
  • renderer: 카메라로 찍은 3D 무대를 디스플레이로 보여주는 플레이어 혹은 영사기

정도로 이해하시면 되겠네요.

this.element.appendChild(renderer.domElement);

Renderer는 앞서 언급한 HTML canvas에 3D 영상을 렌더링하는데요, 이 HTML canvas가 renderer.domElement 입니다. 따라서 이 코드는 이 HTML canvas를 this.element 밑에 붙이겠다는 의미인데, 그럼 this.element가 뭔가요? 그렇죠. 여기서 this.element는 render() 함수 안에 <div>를 가리키고 있어요.

 

Tips> 이해가 조금 어려울 수 있는데, 먼저 this.element는 render() 함수 안에 <div>를 가리켜요. 즉, 지금 이 React 컴포넌트가 가진 건 <div> 뿐인데요, 보통 HTML에서는 document.getElementById() 함수를 써서 <div>를 참조하는데 반해, 리액트에서는 ref 인자를 이용해서 참조합니다.ref = {함수} 구문인데, 원래 들어갈 함수는 아래와 같아요.  
   function (el) {  
          this.element = el  
   }
이 함수를 람다로 표현하면 (el => this.element = el) 처럼 간단히 표시되는 거죠. 있어보이죠? :-) 이렇게 ref에 함수를 지정하면 componentDidMount() 함수가 불리는 시점에서 this.element는 <div>를 가리키게 됩니다. 참고로 React Hook에서는 useRef() 함수와 같아요.

 

다시 말해, 이 컴포넌트가 가진 건 render() 함수 안에 있는 <div> 뿐인데, this.element가 바로 이 <div>를 가리키고 있어요. 자, 이제부터는 3D 박스를 추가하는 코드에요.

    const geometry = new THREE.BoxGeometry(1, 1, 1);
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    camera.position.z = 5;

이어지는 코드는 연두색 박스 하나를 그리고 무대에 추가합니다. 좌표 (0,0,0)에 추가되는데, 카메라도 처음에 (0,0,0)에 추가되니까 이대로 실행하면 카메라와 박스가 겹쳐서 안 보이겠죠. 내가 나를 볼 수 없는 것처럼요. 그래서 카메라를 z축으로 5칸 이동합니다. 마지막으로 animate 함수를 호출하는데요, 

animate = () => {
    this.renderer.render(this.scene, this.camera);
    this.cube.rotation.x += 0.01;
    this.cube.rotation.y += 0.01;
    requestAnimationFrame(this.animate);
  }

첫 줄은 이 장면과 카메라 뷰로 렌더링하겠다는 의미죠. 그 다음 두 줄은 3D 박스를 x, y 축으로 0.01씩 이동하겠다는 의미입니다. 마지막으로 requestAnimationFrame 함수는 this.animate 함수를 계속 호출하라는 의미인데요, 최대 1ms 즉 1초에 60번씩 그리라는 의미입니다. 성능이 낮은 PC나 혹은 CPU/GPU 점유율에 따라 동적으로 변하기도 해요. 즉, 최선을 다해서 그려봐라 같은 명령이죠.

 

컴포넌트 연동하기

이제 방금 만든 3D Viewer 컴포넌트를 연동해봅시다. 진입 포인트인 src/App.js에서 다음과 같이 링크해주면 끝!

import logo from './logo.svg';
import './App.css';

import Viewer from './viewer/Viewer';


function App() {
  return (
    <div className="App">
      <Viewer />
    </div>
  );
}

export default App;

 

마치며

따라해 보셨나요? 생소하거나 어려운 부분이 있다면 일단 넘어가세요. 좀 더 재밌고 다양한 3D 짓을 하다보면 지금 모르는 것도 자연스럽게 이해되실 거에요. 다음 편에서는 3D 화면도 움직여보고 귀여운 캐릭터도 한 번 넣어봅시다. 

 

728x90
반응형