별 하나에 추억을 담아 ✨
3D 기반 웹 추억 저장 서비스
남겨두고 싶은 순간을 찍은 사진과, 그 순간을 떠올리며 적은 글을 별에 담습니다.
기억을 담은 별들이 모여 나만의 은하가 만들어집니다.
추억으로 가득 채워진 나만 우주를 소중한 사람들에게 공유해보세요 ❤️
우리는 모두 형형색색의 기억들을 가지고 있습니다.
그 기억들을 눈으로 볼 수 있다면 얼마나 좋을까요?
저희 팀은 기억을 시각화할 수 있는 서비스를 만들고 싶었습니다.
또 밋밋하고 정적인 일기 서비스에서 벗어나, 사용자가 서비스 이용에 더 큰 흥미를 느낄 수 있도록 하고 싶었습니다.
그래서 우주 공간을 탐험하는 느낌이 드는 독특한 사용자 경험을 주는 서비스, <별 하나에 글 하나>를 만들게 되었습니다.
wiki에서 더 많은 기능을 살펴볼 수 있습니다.
( gif 로딩이 느릴 수 있습니다🥹 조금만 기다려주세요 )
yarn workspace client dev
yarn workspace server start:dev
프론트엔드의 주요 기술적 도전은 Three.js + React-Three-Fiber(R3F)를 사용한 우주 공간 구현이었습니다. 팀원 모두에게 생소한 기술이었기에 사용한 것 자체도 도전적인 경험이었지만, 그 중에서 특히 사용자 경험 개선 위주의 경험을 작성해보았습니다.
먼저 아래는 Three.js와 R3F에 관련하여 팀원들이 작성한 기술블로그입니다.
3D 공간 상에서 카메라는 사용자의 시점입니다.
그렇기 때문에 카메라 움직임은 사용자 경험에 직결됩니다.
저희는 자연스러운 카메라 움직임
을 만들어내 사용자 경험을 향상시키기 위해 여러 과정을 거쳤습니다.
저희 서비스에서 별을 클릭하면 해당 별을 바라보도록 해야 합니다.
처음에는 카메라의 위치는 그대로 둔 채 시야만 회전하도록 하는 회전 운동
의 방식을 사용했습니다.
처음 회전 운동
방식을 적용해본 결과, 별을 바꿀때마다 별과 카메라 사이의 거리를 직접 조정해 줘야 한다는 문제가 있었습니다.
그래서 별과 카메라 사이 거리를 유지한 채 별을 향해 직선 운동
하도록 변경했습니다.
이 방식은 회전 운동
에 비해 사용하기 편했으나, 움직임이 너무 뻣뻣했기에 더 부드러운 모션을 추가하면 좋겠다는 생각을 하게 되었습니다.
많은 고민 끝에 회전 운동처럼 별을 향해 회전하고 직선 운동처럼 별에 다가가도록 하여 '포물선 운동'을 만들어 냈습니다.
포물선 운동
은 회전 운동의 장점인 자연스러운 움직임과 직선 운동의 장점인 직관적인 움직임을 모두 가졌습니다.
이러한 이유로 저희는 포물선 운동
을 적용하게 되었습니다.
직선 운동하는 카메라
포물선 운동하는 카메라
하지만 아직 멀리 있는 별이 너무 작게 보이는 문제가 남아있었습니다. 어찌보면 당연한 이야기일 수 있지만, 서비스 특성상 사용자 입장에서 불편한 요소였고 시각적으로 좋지 않았습니다. 그래서 거리에 비해서 물체가 커 보이게 처리해 멀리 있는 별이 너무 작아보이지 않도록 했습니다.
그랬더니 거리가 먼 별이 겉보기보다 멀리 위치하게 되는 문제가 발생했습니다. 사용자가 그 별로 이동하는데 예상하는 것보다 많은 시간이 소요되었습니다. 이 문제를 해결하기 위해 멀리 이동할 때는 좀 더 빠르게, 가까이 이동할 때는 좀 더 느리게 이동하도록 처리했습니다.
저희는 은하를 만들기 위해 수많은 별 오브젝트들을 화면에 띄워야 했습니다. 하지만 별 개수를 늘릴수록 화면이 더 버벅이기 시작했습니다. 별 개수를 줄이면 시각적으로 좋지 않았기에, 저희는 별 개수를 유지하면서도 화면이 버벅이지 않도록 최적화를 시도하게 되었습니다.
Instancing
저희가 선택한 첫 번째 최적화 방식은 Instancing
이었습니다.
CPU가 GPU에게 무엇을 어떻게 그릴지 지시하는 Draw Call
은 단순해 보이지만 상당히 무거운 작업입니다. 일반적인 컴퓨터 환경에서 Draw Call이 대략 1000회 넘어가면 프레임 드랍이 생긴다고 합니다. 은하를 구성하는 별 오브젝트만 4000개인 저희 프로젝트에서 이러한 Draw Call
을 줄이는 것이 중요햐다고 생각했습니다.
이를 위해 사용한 방식이 Instancing
으로, 동일한 오브젝트를 여러 번 그리는 경우 이를 한번에 처리하도록 하는 방식입니다. 저희는 이를 InstancedMesh
를 사용해 구현했습니다. 이 방식을 통해 은하를 구성하는 별을 종류별로 묶어줌으로써 4000개의 오브젝트를 13개의 인스턴스로 줄일 수 있었습니다. 이렇게 Draw Call
에 의한 CPU 병목 현상을 해결했습니다.
하지만 금요일 프로젝트 현황 공유 시간 때 '처음으로 맥북 팬 소리를 들었어요', '컴터가 안좋아서 그런지 느려요ㅠㅜㅠ' 같은 피드백을 들으면서 추가적인 최적화 작업의 필요성을 느꼈습니다.
Performance Monitoring
피드백을 받은 이후 선택한 것은 Performance Monitoring
입니다. 다양한 최적화 방식이 있었으나 프로젝트에서 사용하는 대부분의 오브젝트가 매우 단순한 형태라 그리 효과적이지 않았습니다. 이에 선택한 방법이 Performance Monitoring
으로, 실시간으로 웹의 퍼포먼스를 모니터링해 이를 반영하는 방식입니다.
react-three/drei 라이브러리의 Performance Monitor
를 통해 웹의 퍼포먼스를 모니터링합니다. 그리고 퍼포먼스가 좋지 않은 경우 Canvas의 Device Pixel Ratio
을 최대 0.5까지 낮춥니다. 은하의 해상도를 낮추어 프레임 드랍을 해결하는 방식입니다. 이렇게 CPU만 고려하던 1번 방식에서 나아가 GPU의 부담까지 덜어주는 방식을 추가함으로써 더 최적화된 서비스를 만들 수 있었습니다.
아래 사진 중 왼쪽은 최고 해상도인 경우이고, 오른쪽은 최저 해상도인 경우입니다.
아래 사진은 메모리 사용량을 비교한 것으로, Performance Monitoring 최적화 전 13.46GB였던 메모리 사용량이 최적화 후 12.50GB까지 감소했습니다.
아래 사진은 퍼포먼스를 비교한 것으로, GPU 전력 사용량이 0.91 에서 0.62로 감소했고 GPU 사용률이 66에서 51로 감소했습니다.
프로젝트를 진행함에 따라 파일들이 점점 많아졌고, 파일 분리와 폴더 구조에 대한 명확한 원칙이 필요해졌습니다. 그래서 팀원들이 함께 여러 폴더 구조와 아키텍쳐들에 대해 조사해보았고, 결과적으로 FSD(Feature-Sliced Design) 아키텍처를 적용하게 되었습니다.
저희 프로젝트는 상대적으로 규모가 작은 편인데, FSD 방식은 폴더를 세세하게 나누는 만큼 규모가 큰 프로젝트에 적합하다는 생각도 했습니다. 하지만 프로젝트를 분할하여 정복하는 해당 방식의 장점이 매력적으로 다가오기도 했고, 이 프로젝트는 학습의 목적이 크기 때문에 팀원들 모두 새로운 폴더구조를 적용해보고 싶어했습니다.
출처: https://feature-sliced.design/
FSD 아키텍처는 app, pages, widgets, features, entities, shared라는 6개의 Layer
로 이루어져있습니다. 그리고 각각의 Layer
는 Slice
들로 이루어져있고, 그 Slice
는 Segment
로 이루어져있습니다. 하위요소들을 조합하여 상위 요소를 구성하는 방식으로, 이 매커니즘이 저희에게 굉장히 매력적으로 다가왔습니다.
이렇게 각자의 역할이 분명한 폴더구조를 적용해봄으로써 모듈을 만들 때 각 모듈의 역할을 명확히 정의하게 되었습니다. 또한 하위 요소들이 모두 개별적으로 기능할 수 있기 때문에 훨씬 유지보수성이 높은 코드를 작성할 수 있게 되었습니다.
아래는 저희 프로젝트의 폴더구조입니다.
📦src
┣ 📂app
┃ ┣ 📜App.tsx
┃ ┣ 📜Router.tsx
┃ ┗ 📜global.css
┣ 📂assets
┃ ┣ 📂fonts
┃ ┣ 📂icons
┃ ┣ 📂logos
┃ ┗ 📂musics
┣ 📂entities
┃ ┣ 📂like
┃ ┣ 📂posts
┃ ┗ 📜index.ts
┣ 📂features
┃ ┣ 📂audio
┃ ┣ 📂backgroundStars
┃ ┣ 📂coachMarker
┃ ┣ 📂controls
┃ ┣ 📂star
┃ ┗ 📜index.ts
┣ 📂pages
┃ ┣ 📂Home
┃ ┣ 📂Landing
┃ ┗ 📜index.ts
┣ 📂shared
┃ ┣ 📂apis
┃ ┣ 📂hooks
┃ ┣ 📂lib
┃ ┃ ┣ 📂constants
┃ ┃ ┣ 📂types
┃ ┃ ┗ 📜index.ts
┃ ┣ 📂routes
┃ ┣ 📂store
┃ ┣ 📂styles
┃ ┣ 📂ui
┃ ┃ ┣ 📂alert
┃ ┃ ┣ 📂alertDialog
┃ ┃ ┣ 📂audioButton
┃ ┃ ┣ 📂buttons
┃ ┃ ┣ 📂inputBar
┃ ┃ ┣ 📂modal
┃ ┃ ┣ 📂search
┃ ┃ ┣ 📂slider
┃ ┃ ┣ 📂textArea
┃ ┃ ┣ 📂toast
┃ ┃ ┗ 📜index.ts
┃ ┗ 📂utils
┣ 📂widgets
┃ ┣ 📂error
┃ ┣ 📂galaxy
┃ ┣ 📂galaxyCustomModal
┃ ┣ 📂landingScreen
┃ ┣ 📂loginModal
┃ ┣ 📂logoAndStart
┃ ┣ 📂nickNameSetModal
┃ ┣ 📂postModal
┃ ┣ 📂screen
┃ ┣ 📂shareModal
┃ ┣ 📂signupModal
┃ ┣ 📂starCustomModal
┃ ┣ 📂underBar
┃ ┣ 📂upperBar
┃ ┣ 📂warpScreen
┃ ┣ 📂writingModal
┗ 📜vite-env.d.ts
테스트와 쿼리 로그 분석을 통한 이유 있는 코드 작성
하나의 API를 구현하기 전에 여러 케이스에 대하여 먼저 테스트코드를 작성하는 TDD(Test Driven Development)를 해보았습니다.
그 과정에서 어색함도 많이 느꼈고, 완벽하게 했다고도 하지 못하지만 TDD의 방법과 장점 등에 대해 알 수 있었습니다.
기능 구현 이후에도, 코드 커버리지를 높이기 위해 e2e 테스트 코드 개선과, mocking을 활용한 유닛 테스트 등을 학습하고 적용해 보았습니다.
인증/인가에 대해 고민도 많이 하였습니다.
Session vs JWT, Authorization Bearer vs Cookie, RefreshToken
특히 보안과 성능 및 편의성 사이의 트레이드오프에 대해 고민하고 학습을 하였습니다.
TypeORM 쿼리 로그를 통해 하나의 비즈니스 로직에서 복수개의 테이블을 수정하는 경우, 트랜잭션을 직접 제어할 필요가 있었습니다. 저희는 TypeORM의 queryRunner와 transaction 메소드, NestJS의 Interceptor 등을 활용하여 여러 차례 트랜잭션 제어 로직을 개선하였고, 각 구현방식의 장단점에 대해서도 학습할 수 있었습니다.
또한 쿼리 로그와 MySQL 쿼리 플랜 기능을 활용해 기존 TypeORM 메소드의 쿼리를 분석하고, 자주 사용되는 일부 메소드에 대해 이를 개선하여 queryBuilder로 개선된 쿼리를 요청하는 쿼리 최적화 과정도 수행해 보았습니다.
NetsJS 자체에 대한 학습을 위해 NestJS의 Lifecycle과 각 Enhancer들에 대해서도 학습을 해보았습니다.
Interceptor, Exception Filter 등 학습을 하고 백엔드 코드에 적용을 해보았습니다.
클라우드 배포 경험이 많지 않아 이번 프로젝트를 통해 많은 성장을 할 수 있었습니다. AWS 및 NCP에서 제공하는 서버, VPC, NAT Gateway 등 주요 서비스에 대해 학습하여 배포 환경을 구성하고, Nginx, Docker 및 Docker Compose, GitHub Actions 등을 학습하여 main 브랜치에 push되면 자동으로 배포되도록 설정했습니다.
리액트를 경험해보고 싶어서 Vite + React + TS를 활용해 간단한 admin 페이지를 만들어보았습니다.
admin용 계정 정보를 설정하고, 게시글 관리 및 컴퓨터 자원 사용량, 에러 로그의 차트를 볼 수 있습니다.
J010 김가은 | J016 김동민 | J053 박재하 | J073 송준섭 | J098 이백범 |