[three.js + React] "deploy했는데 아무것도 안보이는데요?" 대처법 (React Portal과 매우 깊은 연관 있음)

2022. 11. 19. 17:01웹/Three.js

나도 이제 웹으로 포트폴리오 보여줄거야!

 

클라이언트에선 다 그렇지만,

언제나 리소스를 가져오는데에 딜레이는 생기기 마련이다.

 

Three.js에서는 이런 체감이 훨씬 더 심한데, 3D 모델링을 기반으로 하거나, 엄청나게 큰 (화질도 크기도) 사진을 종종 import 해와서 Scene에 넣는 경우가 많기 때문에 그렇다.

 

이건 로컬에서 켰을 때

 

내 포트폴리오 프로젝트가 절반정도 완료되었는데, import 해와야하는 리소스가 무려 36개다.

네트워크 탭에서 확인해보면 캐시된 메모리들도 있고 해서 로컬에서 실행시키면 거진 바로 켜지는 느낌이 들곤 한다.

시간 탭을 보면 거의 1000ms 안쪽으로 전부 리소스가 받아진다. 리소스를 THREE.TextureLoader에서 받아오니까 결론적으로 모든 렌더링까지 1초정도가 걸리는 셈!

 

이건 gh-pages로 배포했을 때

 

반면 이건 gh-pages 라이브러리로 깃허브 페이지에 배포해둔 서비스이다.

물론 이 사이트 서버가 미국에 있어서 느리다고는 하지만, 저 초가 보이는가? 16000ms.. 거진 16초이다.

 

 

보통 Three.js 프로젝트는 Fog를 사용해서 모든 object를 한 번에 보여줄 일이 적기 때문에, 진짜 16초를 기다리진 않고, 첫 번째 assets가 도착하면 바로 그려주지만, 아무튼 이건 문제다. Loading을 시급히 넣어줘야한다.

 

1. ⚙️ DefaultLoadingManager로 구현하는 로딩 컴포넌트

https://threejs.org/docs/?q=loading#api/en/loaders/managers/DefaultLoadingManager 

 

three.js docs

 

threejs.org

 

예제

THREE.DefaultLoadingManager.onStart = (url, item, total) => {
    console.log(url, item, total);
}

THREE.DefaultLoadingManager.onLoad = () => {
    console.log("All loading done!");
}

THREE.onProgress = (url, item, total) => {
    console.log(`loading file info: ${url}, progress rate: ${item}/${total}`);
}

 

 

0%부터 100%까지 늘려가는 spinner를 만들고 싶다면 onProgress를 쓰고, 그냥 뺑글뺑글 도는 spinner를 만들고 싶다면 onLoad를 쓰면 되겠다.

 

z-index를 엄청 위로 해서 하나를 띄워도 좋고, React Portal을 이용해서 띄워줘도 좋다.

useEffect의 의존성에만 조심해서 로딩 컴포넌트를 띄워보자. 나는 React Portal을 사용해보는 걸로!

 

2. 🅿️ Portal 설정 (with TypeScript)

 

/public/index.html

<!--..-->
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <div id="loading"></div>
<!--..-->

 

id: root 아래에 아무거나 용도에 맞게 하나 만들면 된다.

(나는 loading으로 쓸꺼니까 loading으로..)

 

이제 매번마다 진짜 포탈을 열듯이 createPortal이라는 함수로 안에 내용물을 쏴주면 된다.

근데 매번 똑같은 코드를 써야하니 편한 사용을 위해 따로 Portal 컴포넌트로 만들어주자.

 

/pages/Portal.tsx (상위 주소는 아무렇게나 편한데다가 넣어주자!)

import React from "react";
import { createPortal } from "react-dom";

interface PortalProps {
  close: boolean;
  children?: React.ReactNode;
}

export const LoadingPortal: React.FC<PortalProps> = ({ close, children }) => {
  return createPortal(
    <>
      {children}
    </>,
    document.getElementById("loading") as HTMLElement
  )
}

 

Type 관련해서 에러가 몇 개 나는데, 해결하면서 느낀 팁은 document.getElementById() 함수의 return type은 Element | null이고, createPortal의 두 번째 인자는 HTMLElement | FragmentElement를 받기 때문에 타입을 캐스팅 해줘야한다는 점이다.

당황하지 말고 index.tsx를 켜서 as HTMLElement로 그대로 캐스팅 해주면 된다.

(근데 이 논리라면, document.getElementById()에서 null이 return되면, 자동으로 <></>로 내보낸다는 건데.. 맞는지 모르겠다)

 

또, 레이아웃에 관해서 약간 생각을 해보면 알 수 있는 부분인데,

root 태그보다 아래에서 렌더링 되기 때문에 기본적으로 root보다 위...긴 하나

DOM 트리 계층 구조를 따르지 않고, z-index를 따로 조작하는 경우가 다분히 있기 때문에, portal 안의 container 레이아웃의 z-index를 999처럼 완전 위로 선언해주자.

 

아까 위에서 설명을 위해 포탈을 여는 것처럼 쓰는거라고는 했지만, 사실 엄밀히 말하면 원래부터 portal은 열려있고, z-index:999인 컨테이너를 보이게 하냐 마냐의 차이인 것 같다.

 

그리고 만약 이런 식이라면 넘겨주는 state에 따라 scroll을 막아주는 사이드 이펙트를 넣어서 관리해줘야한다.

(모달이든, 로딩창이든 관성적으로 스크롤을 내렸는데 원래 있던 페이지에서 벗어나면 안되니..)

 

/pages/Portal.tsx(스크롤 방지 + 컨테이너 추가)

import React, {CSSProperties, useEffect} from "react";
import { createPortal } from "react-dom";

interface PortalProps {
  className?: string;
  style?: CSSProperties;
  close: boolean;
  children?: React.ReactNode;
}

export const LoadingPortal: React.FC<PortalProps> = (props) => {
  const { className, style, close, children } = props;

  useEffect(() => {
    document.body.style.overflow = `${close ? "unset" : "hidden"}`;
  }, [close]);

  return createPortal(
    <>
      <div className={`${className} ${close ? "hidden" : "flex"} absolute top-0 left-0 w-screen h-screen bg-white z-999`} style={style}>
        {children}
      </div>
    </>,
    document.getElementById("loading") as HTMLElement
  )
}

 

progress는 따로 만들어둔 spinner 컴포넌트로 보내주면 될 것 같다.

그러면 실행이 되는지 확인해보자!

 

 

로딩 전에 Portal이 떠있는 걸 볼 수 있다. 해결 완료!

' > Three.js' 카테고리의 다른 글

[React + three.js] React에서 raycaster 잘 써보기  (0) 2022.12.19