
360도 회전하는 이미지를 볼 수 있고, 더 나아가 드래그 이벤트를 통해 그 물체를 조작해 볼 수 있는 기능은 다양한 웹사이트에서 사용되고 있다. 주로 자동차, 신발, 시계 등의 상품을 관찰할 수 있게 서비스하는 경우가 많다. 이 포스트는 360 뷰어를 구현할 수 있는 다양한 방법 중 react-360-view 라이브러리를 통해 구현하는 법과 오류 방지 팁, 한계점에 대해 다룬다.
react-360-view


물체에 회전 효과를 주기 위해선 어떤 방법을 사용해야 할까? 자체적으로 3D 프로그램을 통해 개발을 할 수도 있겠지만, 가벼운 프로젝트나 중,소 규모 웹사이트에 적용하기에는 다소 어려워 보인다.
간단하게 생각해 보면 다른 각도를 지닌 사진 여러 장을 부드럽게 전환하는 것도 좋은 방법이다.




































위처럼 여러 각도를 가진 이미지들이 있고, 이를 부드럽게 전환시킨다면 해당 물체가 360도로 돌아가는 느낌을 줄 수 있다.
라이브러리를 사용하지 않고 드래그 이벤트 처리를 통해 마우스 이동 방향에 따라 이미지 값을 바꾸어 가는 방식으로 개발을 할 수도 있지만, react-360-view
라이브러리를 사용하면 복잡한 코드를 작성하지 않고 간단하게 구현해낼 수 있다. (그만큼 존재하는 단점에 대해서는 포스트 뒷 부분에)

각도 변화에 따른 여러 이미지가 준비되어 있어야 한다. 구현에는 GitHub에 업로드한 차량 이미지를 사용해 보았다.
$ npx create-react-app react360view
$ cd react360view
$ npm start
먼저 React를 설치한다.
$ npm install react-360-view
react-360-view 라이브러리를 설치한다.
// App.js
import ThreeSixty from "react-360-view"
import "./App.css"
function App() {
return (
<div>
<div
style={{
width: "40%",
alignItems: "center",
border: "2px solid black",
margin: "30px auto",
position: "relative",
}}
>
<ThreeSixty
amount={36}
imagePath="https://raw.githubusercontent.com/yhuj79/blog-assets/main/240713/car"
fileName="car-{index}.webp"
spinReverse
/>
</div>
</div>
)
}
export default App
/* App.css */
@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css");
App.js와 App.css의 내용을 위와 같이 수정했다. 여기서 css는 라이브러리의 아이콘이 fontawesome을 사용하기 때문에 적용되었다.
ThreeSixty 속성을 살펴보면 amount가 있는데, 뷰어에 적용될 총 이미지 수를 넣으면 된다. 이미지 수가 많아질수록 미세한 각도까지 표현되면서 더 부드러워질 것이다. imagePath에는 도메인을, fileName에는 이미지 이름과 형식을 적었다. 이 부분이 중요한데, {index}를 통해 이미지 순서에 따라 구현이 된다. 사용한 이미지가 car-1, car-2, car-3 ... 의 패턴이기 때문에 car-{index}.webp 로 작성하였다.

이미지가 부드럽게 돌아간다. 드래그에 따른 반응 또한 문제 없이 잘 이루어진다.
다른 느낌을 보기 위해 Google Earth 에서 캡쳐한 지구로 다시 테스트해 보았다.



































지구가 열심히 돌아간다.
이 이상으로는 GIF 프레임의 한계 때문에 느낌이 전달되지 않기 때문에 react-360-view에 대해 궁금하거나 관련 기능을 필요로 하고 있다면, 이미지를 구해 실제로 테스트를 해보는 것을 추천한다.
스크롤 시 오류 방지
react-360-view 라이브러리는 회전 뿐만 아니라 이미지의 확대, 축소 기능도 제공한다. 위에서 다룬 지구를 예로 들자면, Google Earth만큼 좋은 화질은 아니겠지만 원하는 부분을 확대해볼 수 있다.


하지만 프로젝트에 적용할 때 한 가지 문제가 발생한다. 이미지 위에 마우스가 위치할 때 확대 및 축소가 발생할 텐데, 웹페이지의 다른 부분에서부터 스크롤을 진행하다가 뷰어 위에 마우스가 위치하게 된다면 의도치 않게 확대, 축소가 작동할 가능성이 있다.
확대와 축소는 이미지 아래 툴 박스를 이용하면 충분하다. 그래서 오류 방지를 위해 wheel event 발생을 차단하는 코드를 추가해 보았다.
import { useEffect } from "react"
import ThreeSixty from "react-360-view"
import "./App.css"
function App() {
// 스크롤 시 의도치 않게 특정 요소가 확대, 축소되는 현상 방지
useEffect(() => {
const element = document.getElementById("container-viewer")
if (element) {
const handleWheel = (event) => {
event.stopPropagation()
}
// wheel event가 다른 요소로 전파되기 전에 stopPropagation에 의해 중단
element.addEventListener("wheel", handleWheel)
// 언마운트 될 때, wheel event listener 제거
return () => {
element.removeEventListener("wheel", handleWheel)
}
}
}, [])
return (
<div>
<div
id="container-viewer"
style={{
width: "40%",
alignItems: "center",
border: "2px solid black",
margin: "30px auto",
position: "relative",
}}
>
<ThreeSixty
amount={34}
imagePath="https://raw.githubusercontent.com/yhuj79/blog-assets/main/240713/earth"
fileName="earth-{index}.png"
spinReverse
/>
</div>
</div>
)
}
export default App
div에 container-viewer라는 id를 부여하여 관리한다. 이벤트 리스너를 통해 wheel event를 제어함으로써 해당 컨테이너는 스크롤이 더이상 작동하지 않게 되었다.
한계점
지금까지 다룬 부분에 대해서만 다룰 것이라면, 이 라이브러리는 기능적으로 충분하다.
하지만 좀 더 깊이 다루고자 한다면 막히는 부분이 생긴다. 예시를 몇 개 들어보자면...
- amount가 많고 고화질인 이미지의 로딩 문제
- 이미지 교체가 필요할 때 (ex: 자동차의 타이어, 자동차의 색상, 신발 끈 색상 등)
1번 예시처럼 로딩 문제가 발생할 경우 Pre Loading 과 같은 이미지 처리가 필요할 것이고, 2번의 경우 직접 드래그 이벤트를 작성하면서 뷰어를 처음부터 개발해 나가야 할 것이다. Vanilla Javascript로 코드를 작성한다면 throttle 도달 수치를 조정하여 드래그 감도를 조절하고, 자동차의 색상이나 타이어를 교체할 수 있게 추가적인 커스터마이징도 충분히 가능할 것이다. 대신 그만큼 필요한 이미지의 수 또한 늘어날 것이다.
Reference
360 degrees Product Viewer in React js - Code With Yd