diff --git a/README.md b/README.md index 9d7f906..f3c1704 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ _스피커를 이동하거나 최적의 위치를 찾는 것은 시간과 노력이 많이 소요됩니다._
_이러한 문제를 해결하기 위해 3D 가상 공간에서 스피커를 자유롭게 배치할 수 있는 환경을 구현하였습니다._ +[서버 레포지토리](https://github.com/soundrag/soundrag-server) +
@@ -33,71 +35,67 @@ _이러한 문제를 해결하기 위해 3D 가상 공간에서 스피커를 자 - [1. 움직이는 스피커로 소리를 바꾸기](#1-%EC%9B%80%EC%A7%81%EC%9D%B4%EB%8A%94-%EC%8A%A4%ED%94%BC%EC%BB%A4%EB%A1%9C-%EC%86%8C%EB%A6%AC%EB%A5%BC-%EB%B0%94%EA%BE%B8%EA%B8%B0) - [1-1. 마우스로 3D 스피커 모델 조작하기](#1-1-%EB%A7%88%EC%9A%B0%EC%8A%A4%EB%A1%9C-3d-%EC%8A%A4%ED%94%BC%EC%BB%A4-%EB%AA%A8%EB%8D%B8-%EC%A1%B0%EC%9E%91%ED%95%98%EA%B8%B0) - [(1) 다양한 해상도에서 매끄럽게 움직이려면?](#1-%EB%8B%A4%EC%96%91%ED%95%9C-%ED%95%B4%EC%83%81%EB%8F%84%EC%97%90%EC%84%9C-%EB%A7%A4%EB%81%84%EB%9F%BD%EA%B2%8C-%EC%9B%80%EC%A7%81%EC%9D%B4%EB%A0%A4%EB%A9%B4) - - [(2) 2D 화면에서 3D 모델의 좌표 구하기](#2-2d-%ED%99%94%EB%A9%B4%EC%97%90%EC%84%9C-3d-%EB%AA%A8%EB%8D%B8%EC%9D%98-%EC%A2%8C%ED%91%9C-%EA%B5%AC%ED%95%98%EA%B8%B0) - - [(3) 마우스와 3D 모델의 실시간 상호작용](#3-%EB%A7%88%EC%9A%B0%EC%8A%A4%EC%99%80-3d-%EB%AA%A8%EB%8D%B8%EC%9D%98-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9) + - [(2) 2D 화면에서 3D 모델 좌표 구하기](#2-2d-%ED%99%94%EB%A9%B4%EC%97%90%EC%84%9C-3d-%EB%AA%A8%EB%8D%B8-%EC%A2%8C%ED%91%9C-%EA%B5%AC%ED%95%98%EA%B8%B0) + - [(3) 3D 모델과 마우스 움직임의 실시간 상호작용](#3-3d-%EB%AA%A8%EB%8D%B8%EA%B3%BC-%EB%A7%88%EC%9A%B0%EC%8A%A4-%EC%9B%80%EC%A7%81%EC%9E%84%EC%9D%98-%EC%8B%A4%EC%8B%9C%EA%B0%84-%EC%83%81%ED%98%B8%EC%9E%91%EC%9A%A9) - [1-2. 스피커의 위치로 소리를 바꾸는 방식](#1-2-%EC%8A%A4%ED%94%BC%EC%BB%A4%EC%9D%98-%EC%9C%84%EC%B9%98%EB%A1%9C-%EC%86%8C%EB%A6%AC%EB%A5%BC-%EB%B0%94%EA%BE%B8%EB%8A%94-%EB%B0%A9%EC%8B%9D) - [(1) 멀리 갈수록 약해지는 소리](#1-%EB%A9%80%EB%A6%AC-%EA%B0%88%EC%88%98%EB%A1%9D-%EC%95%BD%ED%95%B4%EC%A7%80%EB%8A%94-%EC%86%8C%EB%A6%AC) - [(2) 좌우 위치에 따라 달라지는 소리](#2-%EC%A2%8C%EC%9A%B0-%EC%9C%84%EC%B9%98%EC%97%90-%EB%94%B0%EB%9D%BC-%EB%8B%AC%EB%9D%BC%EC%A7%80%EB%8A%94-%EC%86%8C%EB%A6%AC) - [(3) 천장과 바닥의 소리 차이: 주파수](#3-%EC%B2%9C%EC%9E%A5%EA%B3%BC-%EB%B0%94%EB%8B%A5%EC%9D%98-%EC%86%8C%EB%A6%AC-%EC%B0%A8%EC%9D%B4-%EC%A3%BC%ED%8C%8C%EC%88%98) - [2. 사용자 편의성을 위한 도전들](#2-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%8E%B8%EC%9D%98%EC%84%B1%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%8F%84%EC%A0%84%EB%93%A4) - [2-1. 잊어버려도 괜찮아요, 자동 저장](#2-1-%EC%9E%8A%EC%96%B4%EB%B2%84%EB%A0%A4%EB%8F%84-%EA%B4%9C%EC%B0%AE%EC%95%84%EC%9A%94-%EC%9E%90%EB%8F%99-%EC%A0%80%EC%9E%A5) - - [(1) 저장 버튼만 존재하는 불편한 세상](#1-%EC%A0%80%EC%9E%A5-%EB%B2%84%ED%8A%BC%EB%A7%8C-%EC%A1%B4%EC%9E%AC%ED%95%98%EB%8A%94-%EB%B6%88%ED%8E%B8%ED%95%9C-%EC%84%B8%EC%83%81) - - [(2) 자동 저장 구현하기](#2-%EC%9E%90%EB%8F%99-%EC%A0%80%EC%9E%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0) - - [(3) 자동 저장이 가져다주는 편리함](#3-%EC%9E%90%EB%8F%99-%EC%A0%80%EC%9E%A5%EC%9D%B4-%EA%B0%80%EC%A0%B8%EB%8B%A4%EC%A3%BC%EB%8A%94-%ED%8E%B8%EB%A6%AC%ED%95%A8) + - [(1) [고민] 저장 버튼만 존재하는 불편한 세상](#1-%EA%B3%A0%EB%AF%BC-%EC%A0%80%EC%9E%A5-%EB%B2%84%ED%8A%BC%EB%A7%8C-%EC%A1%B4%EC%9E%AC%ED%95%98%EB%8A%94-%EB%B6%88%ED%8E%B8%ED%95%9C-%EC%84%B8%EC%83%81) + - [(2) [구현] 자동 저장이 실행될 조건과 저장할 위치](#2-%EA%B5%AC%ED%98%84-%EC%9E%90%EB%8F%99-%EC%A0%80%EC%9E%A5%EC%9D%B4-%EC%8B%A4%ED%96%89%EB%90%A0-%EC%A1%B0%EA%B1%B4%EA%B3%BC-%EC%A0%80%EC%9E%A5%ED%95%A0-%EC%9C%84%EC%B9%98) + - [(3) [결과] 자동 저장이 가져다주는 편리한 세상](#3-%EA%B2%B0%EA%B3%BC-%EC%9E%90%EB%8F%99-%EC%A0%80%EC%9E%A5%EC%9D%B4-%EA%B0%80%EC%A0%B8%EB%8B%A4%EC%A3%BC%EB%8A%94-%ED%8E%B8%EB%A6%AC%ED%95%9C-%EC%84%B8%EC%83%81) - [2-2. 빠른 속도의 비밀, 프리로드](#2-2-%EB%B9%A0%EB%A5%B8-%EC%86%8D%EB%8F%84%EC%9D%98-%EB%B9%84%EB%B0%80-%ED%94%84%EB%A6%AC%EB%A1%9C%EB%93%9C) - - [(1) 느린 네트워크를 극복하기 위한 고민](#1-%EB%8A%90%EB%A6%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%EB%A5%BC-%EA%B7%B9%EB%B3%B5%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EA%B3%A0%EB%AF%BC) - - [(2) 프리로드 구현하기](#2-%ED%94%84%EB%A6%AC%EB%A1%9C%EB%93%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0) - - [(3) 프리로드가 가져다주는 편리함](#3-%ED%94%84%EB%A6%AC%EB%A1%9C%EB%93%9C%EA%B0%80-%EA%B0%80%EC%A0%B8%EB%8B%A4%EC%A3%BC%EB%8A%94-%ED%8E%B8%EB%A6%AC%ED%95%A8) -- [3. 구현하며 배운 점들](#3-%EA%B5%AC%ED%98%84%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B4-%EC%A0%90%EB%93%A4) + - [(1) [고민] 느린 네트워크를 극복하기 위한 고민](#1-%EA%B3%A0%EB%AF%BC-%EB%8A%90%EB%A6%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC%EB%A5%BC-%EA%B7%B9%EB%B3%B5%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EA%B3%A0%EB%AF%BC) + - [(2) [구현] 미리 불러오고 저장까지 해준다고?](#2-%EA%B5%AC%ED%98%84-%EB%AF%B8%EB%A6%AC-%EB%B6%88%EB%9F%AC%EC%98%A4%EA%B3%A0-%EC%A0%80%EC%9E%A5%EA%B9%8C%EC%A7%80-%ED%95%B4%EC%A4%80%EB%8B%A4%EA%B3%A0) + - [(3) [결과] 프리로드로 느린 네트워크 환경을 극복하기](#3-%EA%B2%B0%EA%B3%BC-%ED%94%84%EB%A6%AC%EB%A1%9C%EB%93%9C%EB%A1%9C-%EB%8A%90%EB%A6%B0-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%81%AC-%ED%99%98%EA%B2%BD%EC%9D%84-%EA%B7%B9%EB%B3%B5%ED%95%98%EA%B8%B0) + - [2-3. 잊지 않고 기억해요, 사용자 인증](#2-3-%EC%9E%8A%EC%A7%80-%EC%95%8A%EA%B3%A0-%EA%B8%B0%EC%96%B5%ED%95%B4%EC%9A%94-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9D%B8%EC%A6%9D) + - [(1) [고민] 새로고침할 때마다 번거로운 인증하기](#1-%EA%B3%A0%EB%AF%BC-%EC%83%88%EB%A1%9C%EA%B3%A0%EC%B9%A8%ED%95%A0-%EB%95%8C%EB%A7%88%EB%8B%A4-%EB%B2%88%EA%B1%B0%EB%A1%9C%EC%9A%B4-%EC%9D%B8%EC%A6%9D%ED%95%98%EA%B8%B0) + - [(2) [구현] 사용자는 몰라도 되는 인증 기억 방법](#2-%EA%B5%AC%ED%98%84-%EC%82%AC%EC%9A%A9%EC%9E%90%EB%8A%94-%EB%AA%B0%EB%9D%BC%EB%8F%84-%EB%90%98%EB%8A%94-%EC%9D%B8%EC%A6%9D-%EA%B8%B0%EC%96%B5-%EB%B0%A9%EB%B2%95) + - [(3) [결과] 사용자 인증 경험을 부드럽게 이어갑니다.](#3-%EA%B2%B0%EA%B3%BC-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%9D%B8%EC%A6%9D-%EA%B2%BD%ED%97%98%EC%9D%84-%EB%B6%80%EB%93%9C%EB%9F%BD%EA%B2%8C-%EC%9D%B4%EC%96%B4%EA%B0%91%EB%8B%88%EB%8B%A4) + - [2-4. 잘 될거야, 낙관적 업데이트](#2-4-%EC%9E%98-%EB%90%A0%EA%B1%B0%EC%95%BC-%EB%82%99%EA%B4%80%EC%A0%81-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8) + - [(1) [고민] 서버의 응답을 기다려야만 하는 상황](#1-%EA%B3%A0%EB%AF%BC-%EC%84%9C%EB%B2%84%EC%9D%98-%EC%9D%91%EB%8B%B5%EC%9D%84-%EA%B8%B0%EB%8B%A4%EB%A0%A4%EC%95%BC%EB%A7%8C-%ED%95%98%EB%8A%94-%EC%83%81%ED%99%A9) + - [(2) [구현] 화면은 바꿨는데 만약 서버 요청에 실패한다면?](#2-%EA%B5%AC%ED%98%84-%ED%99%94%EB%A9%B4%EC%9D%80-%EB%B0%94%EA%BF%A8%EB%8A%94%EB%8D%B0-%EB%A7%8C%EC%95%BD-%EC%84%9C%EB%B2%84-%EC%9A%94%EC%B2%AD%EC%97%90-%EC%8B%A4%ED%8C%A8%ED%95%9C%EB%8B%A4%EB%A9%B4) + - [(3) [결과] 서버의 응답을 기다리지 않아도 되는 합리적인 이유](#3-%EA%B2%B0%EA%B3%BC-%EC%84%9C%EB%B2%84%EC%9D%98-%EC%9D%91%EB%8B%B5%EC%9D%84-%EA%B8%B0%EB%8B%A4%EB%A6%AC%EC%A7%80-%EC%95%8A%EC%95%84%EB%8F%84-%EB%90%98%EB%8A%94-%ED%95%A9%EB%A6%AC%EC%A0%81%EC%9D%B8-%EC%9D%B4%EC%9C%A0) +- [3. 프로젝트 관련 정보](#3-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B4%80%EB%A0%A8-%EC%A0%95%EB%B3%B4) + - [3-1. 기술 스택](#3-1-%EA%B8%B0%EC%88%A0-%EC%8A%A4%ED%83%9D) + - [3-2. 프로젝트 구조](#3-2-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B5%AC%EC%A1%B0) + - [(1) 다양한 컴포넌트에서 공유되는 전역 상태](#1-%EB%8B%A4%EC%96%91%ED%95%9C-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8%EC%97%90%EC%84%9C-%EA%B3%B5%EC%9C%A0%EB%90%98%EB%8A%94-%EC%A0%84%EC%97%AD-%EC%83%81%ED%83%9C) + - [(2) 중앙 집중화된 상수 데이터 파일](#2-%EC%A4%91%EC%95%99-%EC%A7%91%EC%A4%91%ED%99%94%EB%90%9C-%EC%83%81%EC%88%98-%EB%8D%B0%EC%9D%B4%ED%84%B0-%ED%8C%8C%EC%9D%BC) + - [(3) 코드의 중복을 줄이는 공통 컴포넌트](#3-%EC%BD%94%EB%93%9C%EC%9D%98-%EC%A4%91%EB%B3%B5%EC%9D%84-%EC%A4%84%EC%9D%B4%EB%8A%94-%EA%B3%B5%ED%86%B5-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8) + - [(4) 관심사 분리를 적용한 커스텀 훅](#4-%EA%B4%80%EC%8B%AC%EC%82%AC-%EB%B6%84%EB%A6%AC%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%9C-%EC%BB%A4%EC%8A%A4%ED%85%80-%ED%9B%85) +- [4. 구현하며 배운 점들](#4-%EA%B5%AC%ED%98%84%ED%95%98%EB%A9%B0-%EB%B0%B0%EC%9A%B4-%EC%A0%90%EB%93%A4) + - [(1) 3D 구현 도전](#1-3d-%EA%B5%AC%ED%98%84-%EB%8F%84%EC%A0%84) + - [(2) 소리 변화 구현](#2-%EC%86%8C%EB%A6%AC-%EB%B3%80%ED%99%94-%EA%B5%AC%ED%98%84) + - [(3) 사용자 중심 설계](#3-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A4%91%EC%8B%AC-%EC%84%A4%EA%B3%84)

-## 기술 스택 - -### Client - -![React](https://img.shields.io/badge/react-%23404d59.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) -![Vite](https://img.shields.io/badge/vite-%23404d59.svg?style=for-the-badge&logo=vite&logoColor=w) -![Axios](https://img.shields.io/badge/axios-%23404d59.svg?style=for-the-badge&logo=axios&logoColor=w) -![ThreeJS](https://img.shields.io/badge/Three.js-404d59?style=for-the-badge&logo=Three.js&logoColor=w) -![Zustand](https://img.shields.io/badge/zustand-%23404d59.svg?style=for-the-badge&logo=react&logoColor=black) -![Styled-components](https://img.shields.io/badge/styled_component-404d59.svg?style=for-the-badge&logo=styledcomponents&logoColor=DB7093) - -### Server - -![NodeJS](https://img.shields.io/badge/node.js-404d59?style=for-the-badge&logo=node.js&logoColor=6DA55F) -![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) -![MongoDB & Mongoose](https://img.shields.io/badge/MongoDB%20&%20Mongoose-%23404d59.svg?style=for-the-badge&logo=mongodb&logoColor=w) - -### Deploy - -![Firebase](https://img.shields.io/badge/firebase-%23404d59.svg?style=for-the-badge&logo=firebase&logoColor=red) -![Amazon Web Service](https://img.shields.io/badge/amazon%20web%20service-%23404d59.svg?style=for-the-badge&logo=amazon&logoColor=b) - -### Test - -![Vitest](https://img.shields.io/badge/vitest-%23404d59.svg?style=for-the-badge&logo=vitest&logoColor=sd) -![Playwright](https://img.shields.io/badge/playwright-%23404d59.svg?style=for-the-badge&logo=playwright&logoColor=sd) - -
-
- ## 1. 움직이는 스피커로 소리를 바꾸기 사용자가 3D 공간에서 스피커를 움직이며 소리의 변화를 체험할 수 있도록, 다음과 같은 주요 기능을 구현하였습니다. -- **2D 화면에서의 3D 모델 움직임**: 사용자가 마우스를 통해 3D 모델을 드래그할 수 있도록, 2D 화면과 3D 공간 간의 **좌표 변환** 및 **드래그 이벤트 처리**를 구현하였습니다. +- **2D 화면에서의 3D 모델 움직임**
+ 사용자가 마우스를 이용해 3D 모델을 드래그할 수 있도록, **2D 화면 좌표**를 **3D 공간 좌표**로 변환하고 해당 좌표를 **마우스 드래그 이벤트**와 연결하는 방식으로 움직임을 구현하였습니다. -- **3D 공간에서 위치에 따른 소리 변화**: 스피커의 배치에 따라 달라지는 소리의 특성을 제공하기 위해, **세 가지의 위치 기반 음향 변화**를 구현하였습니다. +- **3D 공간에서 위치에 따른 소리 변화**
+ 스피커의 위치에 따라 소리가 어떻게 변화하는지 체험할 수 있도록 세 가지 주요 음향 특성을 적용하였습니다. -
+ - **거리 기반 소리 변화**: 리스너와 스피커 사이의 거리에 따라 소리 **감쇠효과**를 반영 + - **좌우 배치에 따른 변화**: 스피커가 좌우로 이동함에 따라 **스테레오 효과**를 조정 + - **천장 및 바닥 배치에 따른 변화**: 스피커가 천장에 가까울수록 **고주파** 강조, 바닥에 가까울수록 **저주파** 강조 + + 세 가지 특성을 적용한 이유는 사용자가 스피커를 이동하면서 소리의 변화를 **직관적**으로 느끼기 위함입니다. + +
| 2D 화면에서 3D 모델 움직임 | 3D 공간에서 위치에 따른 소리 변화 | | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| | | +| | |
@@ -108,16 +106,16 @@ _이러한 문제를 해결하기 위해 3D 가상 공간에서 스피커를 자
- 2D 화면에서 3D 모델 움직이기 + 스크린샷 2024-12-22 오후 1 29 31

-이 프로젝트에서는 사용자가 3D 모델의 스피커를 배치하여 소리의 변화를 경험할 수 있습니다. 스피커를 배치하기 위해서는 사용자가 **2D 화면에서 3D 스피커 모델의 움직임**을 직접 조작해야 합니다. +사용자가 3D 모델의 스피커의 위치를 조정하여 소리의 변화를 경험할 수 있습니다. 스피커를 움직이기 위해서는 사용자가 **2D 화면에서 3D 스피커 모델의 움직임**을 직접 조작해야 하는 것이 주요했습니다. #### (1) 다양한 해상도에서 매끄럽게 움직이려면? -> 다양한 2D 화면에 대응하기 위해서는 **NDC(Normalized Device Coordinates)** 좌표로 변환해야 합니다. +> 다양한 해상도에 대응하기 위해서는 **NDC(Normalized Device Coordinates)** 좌표로 변환해야 합니다. 웹에서 사용 가능한 이 프로젝트는 사용자들이 다양한 해상도와 화면 비율을 갖고 있는 모니터로 접근합니다. 이러한 다양성 때문에 고정된 화면 크기를 기준으로 좌표를 계산하면 다른 해상도나 비율의 화면에서는 3D 공간에서의 위치 계산이 부정확해질 수 있습니다. 예를 들어, 개발 당시 설정된 화면의 해상도와 비율이 사용자의 모니터와 다를 경우, 사용자가 마우스로 모델을 조작하려 할 때 모델이 의도하지 않은 방향으로 이동하는 문제가 발생할 수 있습니다. 이는 화면 좌표와 3D 공간 좌표의 매핑 오류로 인해 사용자 경험에 혼란을 줄 수 있습니다. @@ -126,7 +124,7 @@ _이러한 문제를 해결하기 위해 3D 가상 공간에서 스피커를 자
- 스크린샷 2024-10-24 오전 7 11 43 + 스크린샷 2024-10-24 오전 7 11 43 _프로젝트 화면 NDC 좌표 변환_ @@ -134,9 +132,9 @@ _프로젝트 화면 NDC 좌표 변환_
-#### (2) 2D 화면에서 3D 모델의 좌표 구하기 +#### (2) 2D 화면에서 3D 모델 좌표 구하기 -> 2D 화면에서 대응하는 3D 모델의 좌표를 구하기 위해 **광선(Ray)** 을 생성합니다. +> 2D 화면에서 보이는 3D 공간의 모델 좌표를 구하기 위해 **광선(Ray)** 을 생성합니다. 2D 화면에서 3D 좌표를 직접 구할 수 없는 이유는 **깊이** 정보가 없기 때문입니다. 이를 해결하기 위해 2D 화면과 3D 공간을 연결하는 **광선** 투사 방식을 사용합니다. 화면상의 마우스 위치에서 3D 공간으로 **광선**을 발사하여, 광선이 3D 모델과 만나는 지점의 거리를 측정합니다. 이를 통해 마우스 위치에 대응하는 3D 공간의 `[x, y, z]` 좌표를 얻을 수 있습니다. @@ -151,16 +149,16 @@ _광선 생성을 통한 z축 좌표 계산_
-#### (3) 마우스와 3D 모델의 실시간 상호작용 +#### (3) 3D 모델과 마우스 움직임의 실시간 상호작용 -> 3D 스피커 모델과 마우스의 움직임이 서로 상호작용하기 위해 사용자가 움직인 3D 모델의 `[x, y, z]` 좌표를 **마우스 이벤트와 연결**합니다. +> 3D 모델과 사용자의 마우스 움직임이 서로 상호작용하기 위해 3D 모델의 `[x, y, z]` 좌표를 **마우스 이벤트와 연결**합니다. 사용자가 마우스로 스피커를 배치할 수 있도록 마우스 **드래그** 이벤트를 활용합니다. **드래그를 시작**할 때 마우스 위치를 기준으로 스피커의 초기 좌표를 저장하고, **드래그 중**에는 마우스 움직임에 따라 스피커의 `[x, y, z]` 좌표를 실시간으로 업데이트하여 화면상의 위치 변화를 반영합니다.
- + _마우스 이벤트와 3D 모델 좌표 실시간 연동_ @@ -199,7 +197,7 @@ _마우스 이벤트와 3D 모델 좌표 실시간 연동_ | 거리가 가까운 경우 | 거리가 먼 경우 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 스크린샷 2024-10-24 오후 4 41 50 | 스크린샷 2024-10-24 오후 4 42 05 | +| 스크린샷 2024-10-24 오후 4 41 50 | 스크린샷 2024-10-24 오후 4 42 05 |
@@ -217,7 +215,7 @@ _cos_ 함수 활용 방식은 아래와 같습니다. 1. 리스너를 중심으로 스피커와의 x축 ,z축 거리를 입력합니다. 2. 입력된 거리를 바탕으로 스피커와 리스너 사이의 각도를 계산합니다. -3. 계산된 각도를 _cos_ 함수에 대입하여 결과 값을 통해 스피커의 위치를 결정합니다. +3. 계산된 각도를 _cos_ 함수에 대입하여 결과 값을 통해 스피커의 좌우 위치를 파악합니다. | _cos_ (각도) | 값 | 위치 | | ------------ | --- | ------ | @@ -225,17 +223,17 @@ _cos_ 함수 활용 방식은 아래와 같습니다. | _cos_ (90°) | 0 | 중앙 | | _cos_ (0°) | 1 | 오른쪽 | -이 방식을 통해 _cos_ 값이 -1에 가까우면 스피커가 왼쪽에, 1에 가까우면 오른쪽에 배치된 것으로 분류합니다. +이 방식을 통해 _cos_ 값이 -1에 가까우면 스피커가 리스너를 기준으로 왼쪽에, 1에 가까우면 오른쪽에 배치된 것으로 분류합니다.
| 왼쪽 배치 | 오른쪽 배치 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 스크린샷 2024-10-24 오후 5 46 37 | 스크린샷 2024-10-24 오후 5 46 49 | +| 스크린샷 2024-10-24 오후 5 46 37 | 스크린샷 2024-10-24 오후 5 46 49 |
-사용자는 스피커를 왼쪽에 배치할 때 왼쪽 소리가 강조되고 오른쪽에 배치할 때 오른쪽 소리가 강조되는 경험을 할 수 있습니다. 리스너로부터 스피커의 x, z축 거리를 활용하여 스피커 **좌우** 배치에 따른 **소리의 세기 차이**를 보여줍니다. +이와 같은 계산을 통해 사용자는 스피커를 왼쪽에 배치할 때 왼쪽 소리가 강조되고 오른쪽에 배치할 때 오른쪽 소리가 강조되는 경험을 할 수 있습니다. 즉, 리스너로부터 스피커의 x, z축 거리를 활용하여 스피커 **좌우** 배치에 따른 **스테레오 효과**를 제공합니다. #### (3) 천장과 바닥의 소리 차이: 주파수 @@ -254,11 +252,11 @@ _cos_ 함수 활용 방식은 아래와 같습니다. | 바닥 배치 | 천장 배치 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 스크린샷 2024-10-24 오후 7 30 55 | 스크린샷 2024-10-24 오후 7 31 18 | +| 스크린샷 2024-10-24 오후 7 30 55 | 스크린샷 2024-10-24 오후 7 31 18 |
-**바닥** 가까이에 설치된 스피커는 고주파 음역대가 흡수되어 약하게 전달되어 상대적으로 저주파 음역대를 강조한 풍부하고 깊은 베이스를 경험할 수 있도록 설계하였습니다. 반면 **천장** 가까이에 설치된 스피커는 고주파 음역대가 흡수되지 않고 그대로 반사되어 고주파 음역대를 강조한 섬세하고 선명한 소리를 제공하도록 구현하였습니다. +**바닥** 가까이에 설치된 스피커는 고주파 음역대가 흡수되어 약하게 전달되어 상대적으로 **저주파 음역대를 강조**한 풍부하고 깊은 베이스를 경험할 수 있도록 설계하였습니다. 반면 **천장** 가까이에 설치된 스피커는 고주파 음역대가 흡수되지 않고 그대로 반사되어 **고주파 음역대를 강조**한 섬세하고 선명한 소리를 제공하도록 구현하였습니다.
@@ -271,60 +269,101 @@ _cos_ 함수 활용 방식은 아래와 같습니다. ## 2. 사용자 편의성을 위한 도전들 -이 프로젝트에서의 사용자 경험을 증진하기 위해, 다음과 같은 기능을 추가하였습니다. +이 프로젝트에서 사용자 편의성을 증진시키기 위해, 다음과 같은 기능을 추가하였습니다. + +- **자동 저장**: 시스템 오류, 저장 버튼 누락 등 예상치 못한 상황 속에서 사용자의 스피커 배치 데이터를 안전하게 저장하기 위해 **자동 저장** 방식을 도입하였습니다. 자동 저장은 사용자가 스피커의 위치를 이동하고 일정 시간동안 더 이상 움직이지 않을 때, 실행됩니다. 또한 애플리케이션의 접근성을 높이기 위해 로그인을 한 사용자의 경우 스피커의 위치 정보와 관련된 데이터는 서버로 저장하고 로그인을 하지 않은 사용자의 경우 위치 정보를 로컬 스토리지에 저장하도록 구현하였습니다. + +- **프리로드**: 느린 네트워크 환경에서 발생할 수 있는 3D 애플리케이션의 불편함을 최소화하기 위해 필요한 3D 리소스를 사전에 로드하는 **프리로드** 기능을 도입하였습니다. 이 기능을 구현하기 위해 프리 로드로 불러온 3D 모델과 텍스처의 경로를 캐싱하는 React Three Drei의 `useGLTF` 훅을 사용하였습니다. 경로를 캐싱하는 `useGLTF`의 동작원리를 활용하여 같은 모델이 여러 개인 경우 재사용할 수 있도록 하였고 불필요한 리소스 네트워크 요청을 줄일 수 있었습니다. -- **자동 저장 기능**: 시스템 오류, 저장 버튼 누락 등 예상치 못한 상황 속에서 사용자의 스피커 배치 **데이터를 안전하게 저장**하기 위해 자동 저장 방식을 도입하였습니다. 또한, **데이터 변경 감지**와 **중복 방지 로직**을 활용하여 효율적으로 작업을 진행하도록 구현하였습니다. +- **사용자 인증 상태 유지**: 사용자가 새로고침하거나 인증 만료와 같은 상황이 발생더라도 끊김 없는 인증 상태를 유지하기 위해 **Axios** **인터셉터** 로직을 구현하였습니다. 인터셉터는 요청 전에 Firebase 인증 토큰을 발급받아 모든 요청에 추가하도록 하였습니다. 만약 인증이 만료가 된 경우, 응답 후에 토큰을 갱신하거나 요청을 재시도하는 로직을 구현하여 사용자 경험에 연속성을 보장하도록 구현하였습니다. 이를 통해 사용자가 로그인 절차를 반복하는 번거로움을 제거하였습니다. -- **프리로드 기능**: 느린 네트워크 환경에서 발생할 수 있는 **3D 애플리케이션의 불편함을 최소화**하기 위해 필요한 3D 리소스를 사전에 로드하는 **프리로딩** 기능을 도입하였습니다. 또한, 불러온 3D 모델과 텍스처를 **캐싱**하고 **재사용**하여 불필요한 리소스 요청을 줄이도록 구현하였습니다. +- **낙관적 업데이트**: 사용자가 로그인 후 서버와 데이터를 동기화하는 과정에서 데이터를 UI에 즉시 반영하여 부드러운 사용자 경험을 제공하는 **낙관적 업데이트** 방식을 도입하였습니다. 만약 동기화에 실패할 경우, 이전 데이터로 **롤백**하여 데이터 무결성을 유지하고, 사용자에게 적절한 안내를 통해 작업 진행 상황을 명확하게 알려주었습니다. 이를 통해 서버 요청 작업이 오래 걸리더라도 일관된 데이터 처리를 보장하며 화면 업데이트가 지연되지 않도록 구현하였습니다.
### 2-1. 잊어버려도 괜찮아요, 자동 저장 -#### (1) 저장 버튼만 존재하는 불편한 세상 +#### (1) [고민] 저장 버튼만 존재하는 불편한 세상 -만약 사용자가 스피커 배치 정보를 저장하기 위해 버튼을 눌러야만 하는 경우, 발생할 수 있는 문제점들을 아래와 같이 고민하였습니다. +만약 저장 버튼으로만 사용자가 스피커 위치 정보를 저장할 수 있다면 어떤 일들이 발생할까요? -1. **데이터 손실 발생**: - 사용자가 스피커를 배치하던 중 시스템 오류, 네트워크 문제 또는 의도치 않은 브라우저 종료 등으로 인해 **작업 중인 데이터를 잃어버릴 위험**이 존재합니다. 이러한 경우, 사용자는 모든 작업을 처음부터 다시 진행해야 하는 불편함을 겪게 될 수 있습니다. +- **데이터 손실 위험**
+ 사용자가 스피커를 배치하다가 시스템 오류, 네트워크 문제, 또는 저장 버튼 클릭을 잊고 브라우저를 닫는 경우, 작업 중인 데이터를 잃을 가능성이 큽니다. 이로 인해 사용자는 작업을 **처음부터 다시 시작해야 하는 불편함**을 겪을 수 있습니다. -2. **저장 버튼의 불편함**: - 저장 버튼을 수동으로 클릭해야 하는 방식은 사용자 경험을 저해할 수 있습니다. 특히, 사용자가 스피커 배치 작업에 집중하다 보면 **저장 버튼 클릭을 잊거나 번거롭게 느낄 가능성**이 높습니다. 자동 저장 기능이 없는 경우 사용자는 “저장”이라는 작업을 지속적으로 의식해야 하고, 이는 프로젝트의 목표인 직관적이고 원활한 사용자 경험을 방해한다고 생각했습니다. +
-이러한 문제들을 해결하기 위해, 사용자가 별도의 작업 없이 데이터가 주기적으로 저장되는 기능이 필요하다고 판단하였습니다. 이를 통해 사용자가 걱정없이, 저장 버튼 클릭과 같은 반복적인 작업 없이도 스피커 배치에만 집중할 수 있는 환경을 제공하고자 **자동 저장 기능**을 도입하기로 결정하였습니다. +
+ 로그 아웃 저장 버튼 -#### (2) 자동 저장 구현하기 + _로그아웃 상태에서 이용할 수 없는 저장 버튼_ -**자동 저장** 기능을 단순히 주기적으로 실행할 경우, 수시로 저장이 이루어지거나 중복 데이터가 저장되어 불필요한 작업이 발생하는 문제점을 해결하기 위해 두 가지 조건을 설정하여 모두 만족할 때만 자동 저장이 실행되도록 설계하였습니다. +

-
- 자동 저장 +또한, 로그인 기능과 연동되어 있는 "현재 공간 저장" 버튼의 경우, 로그인하지 않은 사용자들은 사용할 수 없기 때문에 로그인 여부에 따른 적합한 저장 방식을 별도로 고민하게 되었습니다. 이를 위해, 버튼에 의존하지 않고 데이터 손실의 위험을 줄이며 로그인하지 않은 사용자들도 저장할 수 있는 방식으로 **자동 저장** 기능을 도입하게 되었습니다. -
+
+ +#### (2) [구현] 자동 저장이 실행될 조건과 저장할 위치 + +자동 저장 기능을 설계하고 구현하는데 있어 가장 중요한 부분은 **저장 조건**을 명확히 정의하는 것이었습니다. 이를 통해 중복 저장을 방지하고 불필요한 작업을 줄일 수 있었습니다. + +- **저장 조건**
+ + - 사용자가 스피커의 위치나 회전 값을 변경한 경우, + - 변경된 정보가 기존 정보와 중복되지 않는 경우, + - 5초 동안 사용자가 스피커의 위치나 회전 값을 변경하지 않는 경우, + +이러한 조건을 모두 성립한 경우, 자동 저장을 실행합니다.
-- **5초** 동안 스피커나 리스너 배치가 **바뀌었나요?**
- 자동 저장 기능이 수시로 실행되는 경우 불필요한 연산이 발생하여 **성능 저하**를 일으킬 수 있다고 판단하였습니다. 따라서 **5초**라는 대기 시간과 **변경**이 있을 때만 처리하여 성능 저하없이 정보를 효율적으로 저장하기 위해 해당 조건을 설정하였습니다. +5초 동안 사용자가 스피커의 위치나 회전 값을 변경하지 않는 것을 감지하고 정보를 저장하기 위해 일정 시간 이후에 콜백 함수를 실행하는 비동기 함수 `setTimeout`을 활용하기로 결정하였습니다. 하지만 리액트 환경에서 `setTimeout`만 사용할 경우, 다음과 같은 문제점을 맞닥뜨릴 수 있었습니다. + +- **메모리 누수**
+ 사용자가 스피커의 위치나 회전 정보를 변경할 때마다 새로운 `setTimeout` 함수가 실행될 때, 만약 컴포넌트가 언마운트되었을 때 타이머를 정리하지 않으면 기존 타이머가 계속 남아 **메모리 누수**가 발생할 수 있습니다. + +이러한 문제는 같은 저장 작업이 여러 번 실행되어 **중복 저장**이 발생하거나, 사용자의 의도와 다르게 데이터가 저장되어 **데이터 무결성**을 해치는 원인이 될 수 있습니다. -- 데이터 베이스에 저장된 배치와 **다른가요?**
- 사용자의 스피커 배치 정보는 **데이터 베이스**로 저장됩니다. 데이터 베이스에 저장하기 위해서는 **서버**에 요청을 하는 과정을 거치는데 이 요청은 **네트워크 트래픽**과 관련이 있습니다. 이 요청이 많아진다면 서버에 부하가 생길 수 있습니다. 그리하여 중복된 스피커 배치 정보는 불필요하다고 생각하여 **서버 부하를 줄이기 위해** 해당 조건을 설정하였습니다. +이 문제를 해결하기 위해 `useRef`를 `setTimeout`과 함께 활용하였습니다. + +```jsx +useEffect(() => { + if (!isDuplicate) { + timeoutRef.current = setTimeout(() => { + saveChanges(); + }, delay); + } + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; +}, [positions, rotations]); +``` + +`useRef`를 사용하여 `setTimeout`의 타이머 ID를 저장하고, `useEffect`의 클린업 함수에서 타이머를 정리하는 로직을 작성하였습니다. 이렇게 새로운 타이머가 생성되기 전에 기존 타이머를 처리함으로써 **메모리 누수**를 방지하고, **데이터 무결성**을 보장할 수 있었습니다.
-#### (3) 자동 저장이 가져다주는 편리함 +또한 더 많은 사용자가 서비스를 이용할 수 있도록 로그인을 한 사용자와 로그인을 하지 않은 사용자에게 같은 경험을 제공할 필요성을 느꼈습니다. 그리하여 저장한 정보를 담는 위치를 아래와 같이 구분하였습니다. -이러한 자동 저장 기능을 통해 사용자는 예상치 못한 시스템 오류나 네트워크 문제로 인한 **데이터 손실 위험**을 줄일 수 있었습니다. 또한, 조건부 자동 저장 방식을 적용하여 불필요한 **서버 요청과 연산을 최소화**하는 데 기여할 수 있었습니다. 결과적으로, *Google Docs*와 같은 현대적인 웹 애플리케이션에서 필수적으로 사용되는 자동 저장 기능을 프로젝트에 효과적으로 도입하여 **사용자 경험을 향상**시킬 수 있었습니다. +- **서버**: 로그인한 사용자 +- **로컬 스토리지**: 로그인하지 않은 사용자 + +이렇게 저장 위치를 구분지은 이유는 로그인을 한 사용자의 경우, 인증을 통해 다른 디바이스에서도 저장한 정보를 활용하도록 구현하기 위해 **서버**를 선택하였습니다. 반면, 로그인하지 않은 사용자도 같은 브라우저 환경에서 저장한 정보를 비교적 오래 유지하기 위해 **로컬 스토리지**를 선택하게 되었습니다.
+#### (3) [결과] 자동 저장이 가져다주는 편리한 세상 + +이러한 **자동 저장** 기능을 통해 사용자는 예상치 못한 시스템 오류나 네트워크 문제로 인한 데이터 손실 위험을 크게 줄일 수 있었습니다. 특히, 사용자가 저장 버튼을 누르는 추가적인 작업 없이도 변경 사항이 자동으로 저장되기 때문에 작업에 온전히 집중할 수 있는 환경을 제공할 수 있었습니다. 이러한 접근 방식은 *Google Docs*와 같은 현대적인 웹 애플리케이션에서 필수적으로 사용되는 자동 저장 기술을 효과적으로 도입하여 결과적으로, 사용자 경험을 향상시키는데 기여할 수 있었다고 생각합니다. + | Google Docs | Soundrag | | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | -| | | - -
+| | | ####

목차👆🏼

@@ -332,48 +371,353 @@ _cos_ 함수 활용 방식은 아래와 같습니다. ### 2-2. 빠른 속도의 비밀, 프리로드 -#### (1) 느린 네트워크를 극복하기 위한 고민 +#### (1) [고민] 느린 네트워크를 극복하기 위한 고민 -이 프로젝트에서는 3D 모델과 텍스처 데이터를 반드시 필요로 하며, 이로 인해 **리소스의 로딩 시간**이 사용자 경험에 직접적인 영향을 미친다고 생각합니다. 하지만 네트워크가 느린 경우, 아래와 같이 3D 모델이나 텍스처가 로드되지 않아 사용자에게 빈 화면이 노출되어 애플리케이션이 제대로 작동하지 않는 것처럼 보일 수 있습니다. +이 프로젝트에서 3D 리소스(모델 및 텍스처)는 매우 중요한 요소입니다. 이는 사용자가 3D 모델을 조작해야 하는 핵심 기능과 직결되기 때문입니다. 하지만 느린 네트워크 환경에서는 3D 리소스가 로드되지 않아 **빈 화면**이 사용자에게 노출되는 시간이 길어질 수 있습니다. 이러한 상황이 반복되면 사용자 경험이 심각하게 저하될 가능성이 높다고 생각합니다.
- + + +_느린 네트워크 환경에서 마주한 빈 화면_ +

-이와 같은 문제는 3D 환경에서 **매끄럽지 못한 사용자 경험**을 제공할 수 있는 치명적인 단점을 초래할 위험이 있다고 판단하였습니다. 이를 해결하기 위해 애플리케이션이 시작 단계에서 3D 리소스를 미리 로드하는 **프리로드** 기능을 설계하고 도입하기로 결정하였습니다. +이러한 문제를 해결하기 위해 3D 리소스를 최대한 빠른 시점에 로드해야할 필요가 있다고 판단하였고 **프리로드** 기능을 도입하게 되었습니다. -#### (2) 프리로드 구현하기 +#### (2) [구현] 미리 불러오고 저장까지 해준다고? 프리로드는 애플리케이션 초기 단계에서 사용자가 탐색하게 될 필요한 리소스를 미리 불러오는 과정으로, 빈 화면 노출과 미완성 렌더링과 같은 문제점을 해결하기 위해 아래와 같이 구현하였습니다. -- **진입 페이지 구현**: - 애플리케이션이 실행되는 즉시, **사용자가 인터페이스를 탐색하기 전**에 필요한 모든 리소스를 백그라운드에서 로드하도록 설계하였습니다. 이 때, 초기 로딩 화면으로 **진입 페이지**를 구현하여 해당 페이지에서 백그라운드로 리소스를 미리 로드하도록 구현하였습니다. +- **진입 페이지 구현**
+ 애플리케이션이 실행되면 가장 먼저 **진입 페이지**를 렌더링하도록 설계하였습니다.이 진입 페이지에서 사용자 인터페이스를 탐색하기 전에 필요한 모든 3D 리소스를 백그라운드에서 **프리로드**하도록 구현하였습니다. 이를 통해 사용자가 진입 페이지에서 다른 페이지로 전환하더라도 필요한 3D 모델과 텍스처가 즉시 렌더링될 수 있도록 보장하였습니다. + +
+ + + _진입 페이지에서 프리로드 진행_ +
+ +- **useGLTF**
+ 프리로드 기능은 **React Three Drei**의 `useGLTF` 훅을 활용하여 구현하였습니다. `useGLTF`는 **React Three Fiber**의 `useLoader`를 래핑한 훅으로, `preload` 메서드를 통해 지정된 경로의 GLTF 파일을 미리 로드하고 메모리에 캐싱하는 작업을 수행합니다. 이를 통해 동일한 리소스를 재사용하며 네트워크 요청을 줄이고, 렌더링 성능을 최적화할 수 있었습니다. + + ```jsx + useGLTF.preload("/models/speaker.gltf"); + useGLTF.preload("/models/listener.gltf"); + ``` + +`useGLTF`가 데이터를 캐싱하여 재사용하는 과정은 아래와 같이 진행됩니다. + +1. `useGLTF`는 경로(url)를 기반으로 3D 리소스 데이터를 네트워크 요청으로 가져옵니다. +2. 가져온 데이터는 브라우저의 메모리 Heap에 객체 형태로 저장됩니다. +3. **React Three Fiber**의 내부 Map 객체를 사용하여 파일 경로를 키로 데이터를 저장하고 관리합니다. +4. 동일한 경로의 파일이 요청될 경우, 네트워크 요청을 생략하고 캐싱된 데이터를 반환합니다. + +
+ +#### (3) [결과] 프리로드로 느린 네트워크 환경을 극복하기 + +이러한 프리로드 기능을 도입하고 진입 페이지를 구현함으로써, 3D 모델과 텍스처를 미리 로드된 상태로 다음 페이지로 전환할 수 있어 사용자가 빈 화면을 경험하는 시간을 효과적으로 줄일 수 있었습니다. 또한, `useGLTF`의 캐싱 동작을 통해 중복된 **네트워크 요청을 방지**하고 **렌더링 성능을 최적화**하여 더욱 원활한 사용자 경험을 제공할 수 있었습니다. + +| Before | After | +| -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| | | + +####

목차👆🏼

+ +
+ +### 2-3. 잊지 않고 기억해요, 사용자 인증 + +#### (1) [고민] 새로고침할 때마다 번거로운 인증하기 + +만약 새로고침 시 현재 인증 상태가 초기화되어 사용자가 다시 로그인을 해야 한다면, 새로고침할 때마다 사용자가 로그인을 반복해야 하는 불편함을 느낄 수 있습니다. 이러한 **반복 인증 과정**은 사용자의 피로도를 높이고 결국 서비스 이탈로 이어질 가능성이 높다고 생각하였습니다. + +
+
+ + +_새로고침할 때, 로그인 상태 유지가 안되는 상황_ + +
+
+ +이를 방지하기 위해, 새로고침 시에도 인증 상태를 유지하고, 사용자가 알 수 없는 인증 토큰이 만료된 경우 자동으로 갱신하는 기능 또한 필요하다고 판단하였습니다. + +#### (2) [구현] 사용자는 몰라도 되는 인증 기억 방법 + +사용자의 인증을 기억하기 위해 **Axios** 인터셉터를 활용하여 인증 처리 로직을 구현하였습니다. 물론, 사용자의 인증을 검증하는 방식은 서버 미들웨어를 통해서도 처리할 수 있습니다. 하지만 네트워크 비용 절감과 응답 속도 개선을 통해 사용자 경험을 높이기 위해, 클라이언트에서 **Axios** 인터셉터를 활용한 방식을 선택하였습니다. + +1. **요청 인터셉터**
+ 요청 인터셉터는 API 요청이 서버로 전송되기 전에 실행되며, 현재 로그인한 사용자의 인증 토큰을 확인하고 이를 요청 헤더에 추가합니다. 이를 통해 **모든 요청이 인증된 상태**로 전송되도록 보장하였습니다. + + ```jsx + axiosInstance.interceptors.request.use(async (config) => { + const user = auth.currentUser; + + if (user) { + const idToken = await user.getIdToken(); + config.headers.Authorization = `Bearer ${idToken}`; + } + return config; + }); + ``` + +2. **응답 인터셉터**
+ 응답 인터셉터는 서버로부터 401 (Unauthorized) 에러 응답이 반환되었을 때 실행됩니다. 이 단계에서 인증 토큰이 만료되었음을 감지하고, 새로운 토큰을 요청하여 **갱신된 토큰**으로 실패한 요청을 재실행하도록 구현하였습니다. + + ```jsx + axiosInstance.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; -- **리소스 로딩 최적화**: - 3D 모델과 텍스처 데이터를 불러올 때, 비동기 방식으로 로드하여 리소스가 효율적으로 준비되도록 최적화하였습니다. 추가로 로드된 리소스를 메모리에 **캐싱**함으로써 동일한 리소스에 대한 **중복 네트워크 요청을 방지**하였고, 이후 화면 전환 시에도 빠르고 일관된 렌더링을 제공할 수 있게 되었습니다. + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; -#### (3) 프리로드가 가져다주는 편리함 + const user = auth.currentUser; -이러한 프리로드 기능을 도입함과 동시에 진입 페이지를 구현하면서 3D 모델과 텍스처를 불러온 상태로 다음 페이지 전환되기 때문에 사용자가 빈 화면을 경험할 일이 사라졌습니다. 결과적으로 네트워크 상태와 관계없이 안정적으로 리소스를 불러오면서 사용자가 3D 화면을 탐색하기 전에 모든 리소스가 로드된 상태를 보장하면서 3D 화면으로의 전환 과정에서 발생할 수 있는 불편함을 최소화하여 **사용자 경험을 최적화**하였습니다. + if (user) { + const newIdToken = await user.getIdToken(true); + originalRequest.headers.Authorization = `Bearer ${newIdToken}`; + + return axiosInstance(originalRequest); + } + } + + return Promise.reject(error); + }, + ); + ``` + +#### (3) [결과] 사용자 인증 경험을 부드럽게 이어갑니다. + +**Axios** 인터셉터 로직을 통해 사용자 인증을 유지하면서 새로고침하거나 페이지 이동 후에도 다시 로그인하지 않고 작업을 이어갈 수 있게 되었습니다. 이는 애플리케이션 작업에 대한 **사용자의 집중도**를 높일 수 있었고 반복 작업에 대한 **사용자의 피로도** 또한 낮출 수 있었습니다.
- + + +_새로고침을 하더라도 로그인 상태 유지가 되는 상황_ +
+####

목차👆🏼

+
+### 2-4. 잘 될거야, 낙관적 업데이트 + +#### (1) [고민] 서버의 응답을 기다려야만 하는 상황 + +자동 저장 기능에서 비로그인 사용자가 로그인을 하게 되면, 로컬 스토리지에 저장된 데이터를 서버로 동기화하는 작업이 진행됩니다. 이 과정에서 네트워크 속도가 느리거나 서버 응답 시간이 길어지면, 동기화가 완료될 때까지 **UI 업데이트가 지연**되는 문제가 발생할 수 있습니다. 이러한 상황은 사용자로 하여금 화면이 멈추거나 버벅거리는 것처럼 보이게 하여, 애플리케이션의 신뢰도를 떨어뜨릴 위험이 있습니다. 이를 해결하기 위해, 느린 네트워크 환경에서도 사용자가 불편함을 느끼지 않고 매끄럽게 애플리케이션을 사용할 수 있는 방법을 고민하게 되었습니다. + +
+
+ + +_느린 서버 응답때문에 발생하는 부자연스러운 화면_ + +
+
+ +#### (2) [구현] 화면은 바꿨는데 만약 서버 요청에 실패한다면? + +**낙관적 업데이트**는 사용자의 요청이 성공할 것이라고 가정하고, 서버 응답을 기다리지 않고 UI를 즉시 업데이트하는 방식입니다. 이 방식을 통해 사용자는 네트워크 상태와 관계없이 자신이 조작한 대로 화면이 반응하는 것을 볼 수 있어, 더 빠르고 매끄러운 사용자 경험을 제공하도록 구현하였습니다. + +- **서버 요청이 실패한다면?**
+ 하지만, UI 업데이트 후 서버 요청이 실패하면 화면과 서버 데이터의 **불일치 문제**가 발생할 수 있습니다. 화면상으로는 작업이 완료된 것처럼 보이지만, 실제 서버 데이터는 업데이트되지 않아 사용자 혼란을 초래할 수 있습니다. 이러한 사용자 혼란을 덜어주기 위해 요청 실패했을 때, 아래 두 가지 방식으로 처리하였습니다. + +1. **상태 롤백**: 화면과 서버 데이터 간의 불일치를 방지하기 위해 요청 전에 백업해둔 데이터를 활용하여 화면을 **이전 상태로 복원**하도록 구현하였습니다. +2. **사용자 알림**: 서버 요청 실패 시 적절한 알림 메시지를 표시하여 사용자에게 문제 상황을 명확히 전달합니다. + +
+
+ + +_동기화 진행 중, 서버 요청이 실패한 상황_ + +
+
+ +#### (3) [결과] 서버의 응답을 기다리지 않아도 되는 합리적인 이유 + +낙관적 업데이트를 진행하면서 사용자에게 매끄러운 화면 전환을 제공할 수 있었습니다. 뿐만 아니라, 요청 실패 시 롤백 처리를 구현하면서 화면과 서버 데이터의 불일치한 상황에 대한 걱정없이 낙관적 업데이트를 진행할 수 있었습니다. 이를 통해 서버의 응답을 기다리지 않고 화면을 먼저 업데이트하면서 느린 네트워크에서도 사용자가 스피커 배치 작업을 **부드럽고 안정적**으로 진행하도록 서비스를 제공할 수 있었습니다. + +
+
+ + +_낙관적 업데이트가 적용된 동기화 과정_ + +
+ +####

목차👆🏼

+ +
+
+ +## 3. 프로젝트 관련 정보 + +### 3-1. 기술 스택 + +#### Client + +![React](https://img.shields.io/badge/react-%23404d59.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) +![Vite](https://img.shields.io/badge/vite-%23404d59.svg?style=for-the-badge&logo=vite&logoColor=w) +![Axios](https://img.shields.io/badge/axios-%23404d59.svg?style=for-the-badge&logo=axios&logoColor=w) +![ThreeJS](https://img.shields.io/badge/Three.js-404d59?style=for-the-badge&logo=Three.js&logoColor=w) +![Zustand](https://img.shields.io/badge/zustand-%23404d59.svg?style=for-the-badge&logo=react&logoColor=black) +![Styled-components](https://img.shields.io/badge/styled_component-404d59.svg?style=for-the-badge&logo=styledcomponents&logoColor=DB7093) + +#### Server + +![NodeJS](https://img.shields.io/badge/node.js-404d59?style=for-the-badge&logo=node.js&logoColor=6DA55F) +![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) +![MongoDB & Mongoose](https://img.shields.io/badge/MongoDB%20&%20Mongoose-%23404d59.svg?style=for-the-badge&logo=mongodb&logoColor=w) + +#### Deploy + +![Firebase](https://img.shields.io/badge/firebase-%23404d59.svg?style=for-the-badge&logo=firebase&logoColor=red) +![Amazon Web Service](https://img.shields.io/badge/amazon%20web%20service-%23404d59.svg?style=for-the-badge&logo=amazon&logoColor=b) + +#### Test + +![Vitest](https://img.shields.io/badge/vitest-%23404d59.svg?style=for-the-badge&logo=vitest&logoColor=sd) +![Playwright](https://img.shields.io/badge/playwright-%23404d59.svg?style=for-the-badge&logo=playwright&logoColor=sd) + +
+
+ +### 3-2. 프로젝트 구조 + +#### (1) 다양한 컴포넌트에서 공유되는 전역 상태 + +컴포넌트 간의 데이터 흐름을 간소화하고 추적을 용이하게 하기 위해, **두 개 이상의 컴포넌트**에서 관리되거나 **두 번 이상의 props drilling**이 발생하는 상태를 **Zustand** 라이브러리를 사용해 전역 상태로 관리하였습니다. 특히, 3D 모델의 위치와 회전 정보는 음향 변화 뿐만 아니라 버전 저장, 회전 값 변경, 위치 변경 등 다양한 영향을 미치는 프로젝트의 핵심 상태로, 전역 상태로 관리하는 대표적인 상태입니다. + +
+스크린샷 2024-12-21 오후 5 54 45 + +_3D 모델 위치, 회전 정보 전역 상태 관리 시각화_ + +
+ +전역 상태 관리 라이브러리로 **Zustand**를 사용하게 된 배경은 다음과 같습니다. + +- 경량성
+ **Zustand**는 다른 전역 상태 관리 라이브러리에 비해 매우 **작은** 용량을 차지합니다. 이러한 장점은 프로젝트의 번들 크기를 감소하고 로딩 속도를 빠르게 유지할 수 있습니다. + + | Zustand | Mobx | Redux | Recoil | Jotai | + | ------- | ---- | ------ | ------ | ----- | + | 0.588kb | 16kb | 12.7kb | 23.5kb | 2.5kb | + + _Gzip으로 최소화된 라이브러리 압축 용량_ + +
+ +- 효율성
+ **Zustand**는 다른 라이브러리에 비해 설정이 간단하고 보일러 플레이트 코드가 짧기 때문에 상태 관리 로직의 **가독성**을 향상시킬 수 있습니다. 또한 **발행-구독(Pub-Sub)** 모델을 활용하기 때문에 구독 단위의 **효율적**인 상태 관리가 가능합니다. + +이를 통해, **Zustand**로 복잡한 상태 관리 코드의 비효율성을 줄이고 컴포넌트 간 데이터 흐름을 **단순화**하여 코드의 가독성을 크게 향상 시킬 수 있었습니다. 또한, 상태 변경의 영향을 **명확하게 추적**할 수 있어 기능 추가나 디버깅 과정에서도 작업 효율성을 높일 수 있었습니다. + +
+ +#### (2) 중앙 집중화된 상수 데이터 파일 + +코드의 가독성과 재사용성을 향상시키기 위해, **중앙 집중화**된 단일 파일에서 관리하도록 구현하였습니다. 해당 파일에는 프로젝트에서 사용되는 고정된 값이나, 전역적으로 참조되는 데이터를 관리하도록 설계하였습니다. + +```jsx + 📁 constants.ts + + const AUTO_SAVE_DELAY = 5000; + const SPEAKER_SIZE = 0.5; + const LISTENER_SIZE = 1; + const ROOM_SIZE = 30; +``` + +또한 **직관적인 상수 이름**을 사용하여 코드의 가독성을 높일 수 있었습니다. + +- 하드코딩된 숫자 사용 + + ```jsx + { + position: new Vector3(-(30 / 2), 5 / 2, 0), + rotation: new Euler([0, -Math.PI / 2, 0]), + }, + ``` + +숫자 데이터가 무엇을 의미하는지 코드만으로는 파악하기 어렵습니다. 이러한 경우 유지보수를 진행할 때, 이 값이 다른 곳에서 어떻게 사용되는지 추적하기 힘들고 변경할 때, 모든 코드를 수정해야 할 수 있습니다. + +
+ +- 상수화된 데이터 사용 + + ```jsx + { + position: new Vector3(-(ROOM_SIZE / 2), WALL_HEIGHT / 2, 0), + rotation: new Euler(...ROTATE_Y_90_DEGREES), + }, + ``` + +직관적인 이름을 가진 상수화된 데이터를 통해 값의 의미를 이전보다 **명확하게** 파악할 수 있었습니다. 이를 통해, 상수 값의 **재사용성**과 **일관성**을 강화하고 관리 편의성을 제공하므로써 하드코딩으로 인한 버그 발생 가능성을 줄일 수 있었습니다. + +
+ +#### (3) 코드의 중복을 줄이는 공통 컴포넌트 + +```jsx + 🗂️ common + ㄴ 📁 Button.tsx + ㄴ 📁 Icon.tsx + ㄴ 📁 Modal.tsx + ㄴ 📁 Model.tsx + ㄴ 📁 NavHeader.tsx +``` + +코드 중복을 줄이고 유지보수성을 높이기 위해, UI에서 **공통적으로 사용**하는 컴포넌트를 구현하였습니다. 이러한 공통 컴포넌트는 state와 props를 활용하여 다양한 상황에 맞게 유연하게 동작하도록 설계하였습니다. + +- 예시) 버튼 컴포넌트 + + ```jsx + const Button = ({ text, handleClick }) => { + return ; + }; + ``` + +이를 통해, 클릭할 때 각기 다른 기능을 수행하는 버튼을 여러 컴포넌트로 나눌 필요 없이, **하나의 컴포넌트**에 state와 props를 활용해 **다양한** 버튼을 구현할 수 있었습니다. + +
+ +#### (4) 관심사 분리를 적용한 커스텀 훅 + +코드의 재사용성을 향상시키고 파일명만 보더라도 어떠한 기능을 수행하는 파일인지 명료하게 나타나기 위해 **관심사 분리** 원칙을 적용하여 기능별로 커스텀 훅을 구현하였습니다. + +```jsx + 🗂️ hooks + ㄴ 📁 useAutoSaveVersion.tsx + ㄴ 📁 useDraggableTarget.tsx + ㄴ 📁 useRotatableTarget.tsx + ㄴ 📁 useUpdateData.tsx + . + . + . +``` + +- `useAutoSaveVersion.tsx`: 버전을 자동 저장하는 기능 +- `useDraggableTarget.tsx`: 3D 모델을 드래그하는 기능 +- `useRotatableTarget.tsx`: 3D 모델의 회전을 바꾸는 기능 +- `useUpdateData.tsx`: 데이터를 동기화하는 기능 + ####

목차👆🏼



-## 3. 구현하며 배운 점들 +## 4. 구현하며 배운 점들 #### (1) 3D 구현 도전