React 메타버스 웹앱 시리즈입니다.
- 메타버스 앱 - 1. 3D 파일 업로드
- 메타버스 앱 - 2. 배경, 건물 그리고 1인칭 시점
- 메타버스 앱 - 3. 캐릭터
요즘 핫한 메타버스, React 웹앱으로 간단히 만들어 봅시다.
이번 편에서는 React 앱을 만들고 3D 파일을 업로드해서 로딩하는 것까지 해 볼 거에요.
React 앱 준비
기초 내용이지만, 간단히 복습해 봅시다.
- Create-react-app으로 앱을 만들어 줍니다. $npx create-react-app metaverse
- UI 라이브러리 Reactstrap을 설치해 줍니다. $npm i -save reactstrap bootstrap
- index.js에 bootstrap.css를 import합니다. import 'bootstrap/dist/css/bootstrap.css';
- 웹 3D 라이브러리인 three와 파일 업로드에 사용할 axios를 설치합니다. $npm i -save three axios
- Visual Studio로 코딩을 시작합시다. $code ./
파일 업로드
시중에 유통되는 3D 파일의 상업용 라이선스는 보통 애니메이션과 같은 2차 저작물에 렌더링해서 사용하는 조건이 대부분입니다. 즉, 게임이나 우리 웹앱처럼 파일 자체가 앱에 포함되어 배포되는 경우에는 라이선스 위배 소지가 있습니다. 남들이 웹앱 안에서 3D 파일 원본을 꺼낼 수 있기 때문이죠. 따라서 본 튜토리얼의 예제 파일에도 외부 3D 파일은 제공되지 않는데요, 3D 파일을 직접 다운 받아서 앱에 넣어 사용할 수 있도록 파일 업로드 기능을 만들어 봅시다.
React 앱은 웹에서 구동되므로 로컬 PC 파일 시스템에 접근할 수 없습니다. 따라서 별도의 Node.js 로컬 서버 앱을 만들어 React 앱에서 업로드한 파일을 받아 원하는 위치, 즉 React 앱의 public/ 폴더에 저장하는 앱을 만들어야 합니다.
파일 업로드 : 서버
React 앱의 최상위 폴더에서 server/ 폴더를 만들고 $npm init 후 server.js 파일을 생성합니다.
코드는 아래와 같은데요, 잘 정리된 Github(링크)를 참고해서 구현했습니다.
const PORT = 4000;
const PUBLIC_DIR = '../public'
//---앱의 최상위 폴더 아래 public 폴더가 파일을 저장할 목적지입니다.
const express = require('express');
const fileUpload = require('express-fileupload');
const cors = require('cors');
const app = express();
const fs = require('fs');
//---유명한 Node express 서버와 fileupload 기능을 불러옵니다. cors는 보안 메커니즘인데 설명하기는 번거롭고 그냥 적용해주세요.
app.use(express.urlencoded({ extended: false }));
app.use(express.static('public'));
app.use(cors());
app.use(fileUpload());
app.use(express.json());
//---이제부터 Restful API 입니다.
//---현재 디렉토리에 있는 파일 리스트를 리턴해주는 API입니다.
app.post('/list', (req, res) => {
const target = PUBLIC_DIR + req.body.dir;
console.log("\n\nPOST/list\n\tdir=", target);
fs.readdir(target, (error, filelist) => {
!fs.existsSync(target) && fs.mkdirSync(target);
if (error) {
console.log("ERROR:", error);
return res.status(500).send({ msg: 'unknown error' })
} else {
console.log("returns:\n\t", filelist);
return res.status(200).send({ fileList: filelist });
}
})
})
//---파일 업로드 요청을 받았을 때 처리하는 API입니다.
//받은 파일을 목적지로 파일을 이동(mv)시킵니다.
app.post('/upload', (req, res) => {
if (!req.files) {
return res.status(500).send({ msg: 'No file' })
}
const file = req.files.file;
const filepath = __dirname + '/' + PUBLIC_DIR + req.body.dir + '/' + file.name;
console.log("\n\nPOST/upload\n\tfile: " + file.name + "\n\tto: " + filepath);
file.mv(filepath, (err) => {
if (err) {
console.log("ERROR:", err);
return res.status(500).send({ msg: 'Error occurred' });
} else {
console.log("SUCCEEDED: ", file);
return res.status(200).send({ name: file.name, path: `/${file.name}` });
}
});
})
//---서버를 시작합니다.
app.listen(PORT, () => {
console.log("server is running at port " + PORT);
})
간단하죠? app.post()로 간단히 Restful API를 만들 수 있습니다. 게다가 fileUpload가 라이브러리로 제공되고 있어서 매우 편하네요. 주의할 점은 express.json()과 urlencoded()를 반드시 써야만 req.body 내용이 제대로 나옵니다. 그렇지 않으면 undefined로 인식되므로 유의하세요.
실행은 server$npm start 하면 됩니다.
파일 업로드 : 클라이언트
위에서 만든 서버에 대응하는 React 앱 클라이언트 컴포넌트를 만들어 봅시다.
import axios from 'axios';
import React, { useEffect, useRef, useState } from 'react';
import { Button, Form, FormGroup, Input, Label } from 'reactstrap';
const SERVER_PORT = 4000;
const BASE_URL = window.location.protocol + '//' + window.location.hostname + ':' + SERVER_PORT;
//---포트는 서버가 듣는 포트와 동일해야 하고 BASE_URL은 현재 브라우저 주소창의 내용을 가져옵니다.
//---window.location 내용은 어떻게 접속하느냐에 따라 달라집니다. e.g. localhost:3000 vs. 127.0.0.1:3000
function FileUpload(props) {
const [targetList, setTargetList] = useState(undefined);
const [uploadFile, setUploadFile] = useState('');
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadResult, setUploadResult] = useState(undefined);
const refInput = useRef(null);
//---위에서 uploadXXXXXX와 refInput은 파일을 직접 입력할 때 사용합니다.
useEffect(() => {
axios.post(BASE_URL + "/list",
JSON.stringify({ dir: props.dir }),
{ headers: { 'Content-Type': 'application/json' } }
)
.then(res => setTargetList(res.data.fileList))
.catch(error => error.toString().includes('Network Error') && setUploadResult("서버가 응답하지 않습니다. server$ npm start를 하셨나요?"))
}, []);
//---최초 로컬 서버에 /list 명령으로 현재 public 폴더 안에 있는 파일 리스트를 가져옵니다.
//---가져온 파일 리스트를 targetList에 넣어주면 선택 바 안에 옵션들로 들어갑니다. 아래 getSelectTarget 함수 참고.
const getSelectTarget = (items) => {
return (
<Form>
<FormGroup>
<Label for={props.title}><h3>{props.title} 선택</h3></Label>
<Input
type="select"
name={props.title}
id={props.title + "Target"}
defaultValue='default'
// onChange={(e) => setTarget({ name: e.target.childNodes[e.target.selectedIndex].innerText, value: e.target.value })}>
onChange={(e) => props.onSelectTarget && props.onSelectTarget(e.target.value)}>
<option disabled value='default'>원하는 {props.title}을 선택하세요.</option>
{items.map((item, idx) => (
<option key={idx} value={item}>
{item}
</option>
))}
</Input>
</FormGroup>
</Form>
)
}
//---선택 바(Select) GUI를 리턴하는 JSX 함수입니다.
const onChooseFile = (e) => {
setUploadProgress(0);
setUploadFile(e.target.files[0]);
setUploadResult(undefined);
}
//---직접 파일을 업로드하기 위해 파일을 선택할 때 state를 업데이트합니다.
//---아직 파일을 보내지는 않습니다. UPLOAD 버튼이 눌릴 때 보냅니다.
const upload = () => {
const form = new FormData();
form.append("file", uploadFile);
form.append("dir", props.dir);
axios.post(BASE_URL + "/upload",
form,
{ onUploadProgress: (evt) => setUploadProgress(Math.round(evt.loaded / evt.total * 100) + '%') })
.then(res => setUploadResult('Upload completed. Please Refresh.' + res))
.catch(err => console.log("ERROR", err));
}
//---UPLOAD 버튼이 눌렸을 때 axios.post 명령을 통해 파일을 보냅니다.
//---파일 첨부는 new FormData()로 간단히 처리되네요!
return (
<div style={{ border: '3px solid #ababab', width: 'fit-content', padding: '1em', margin: '1em' }}>
{targetList && getSelectTarget(targetList)}
<hr />
<p>{props.title}을 직접 업로드하려면, 파일 선택 후 업로드해 주세요.</p>
<input type="file" ref={refInput} onChange={onChooseFile} style={{ margin: 'auto' }} />
{uploadFile && <p><span style={{ color: 'blue' }}>{uploadFile.name}</span> is ready to upload.</p>}
<hr />
{uploadProgress > 0 &&
<div style={{ width: uploadProgress, backgroundColor: 'blue' }}>
{uploadProgress}
</div>
}
{uploadResult && <p style={{ color: 'red' }}>{uploadResult}</p>}
{uploadFile && <Button color="primary" onClick={upload} >Upload</Button>}
</div>
)
}
export default FileUpload;
호출은 다음과 같습니다.
<FileUpload title={'건물'} dir={'/world'} onSelectTarget={setWorld} />
<FileUpload title={'배경'} dir={'/hdri'} onSelectTarget={setHdri} />
실행 결과를 보시죠.
선택 바에서 원하는 아이템을 선택하거나, Choose File 버튼을 눌러 직접 업로드합니다.
Choose File 버튼을 누르면 로컬 파일창이 열리고 파일을 선택하면 UPLOAD 버튼이 생깁니다.
샘플 파일
이제 업로드할 3D 배경 이미지와 건물 파일을 받아 봅시다.
우선, 3D 배경 이미지는 HDRI Haven에서 무료(CC0 라이선스) 이미지를 받습니다. 8K JPEG 파일로 충분합니다.
건물은 CGTrader에서 예쁘게 생긴 무료 FBX를 다운받습니다.
다음 편에서 3D 배경과 건물을 로딩해 봅시다.
'IT > 3D Web' 카테고리의 다른 글
메타버스 앱 - 3. 캐릭터 (0) | 2021.07.10 |
---|---|
메타버스 앱 - 2. 배경, 건물 그리고 1인칭 시점 (3) | 2021.07.01 |
Google TTS (0) | 2021.05.27 |
React 3D 웹앱 - 4. 3D 뷰어 (0) | 2021.04.24 |
React 3D 웹앱 - 3. 3D 모델 (0) | 2021.03.18 |