3D 기반 웹 추억 저장 서비스
남겨두고 싶은 순간을 찍은 사진과, 그 순간을 떠올리며 적은 글을 별에 담습니다.
기억을 담은 별들이 모여 나만의 은하가 만들어집니다.
추억으로 가득 채워진 나만 우주를 소중한 사람들에게 공유해보세요 ❤️
우리는 모두 형형색색의 기억들을 가지고 있습니다.
그 기억들을 눈으로 볼 수 있다면 얼마나 좋을까요?
저희 팀은 기억을 시각화할 수 있는 서비스를 만들고 싶었습니다.
또 밋밋하고 정적인 일기 서비스에서 벗어나, 사용자가 서비스 이용에 더 큰 흥미를 느낄 수 있도록 하고 싶었습니다.
그래서 우주 공간을 탐험하는 느낌이 드는 독특한 사용자 경험을 주는 서비스, <별 하나에 글 하나>를 만들게 되었습니다.
wiki에서 더 많은 기능을 살펴볼 수 있습니다.
( gif 로딩이 느릴 수 있습니다🥹 조금만 기다려주세요 )
- 1.5배속된 영상입니다.
- 별에 마우스를 호버하면 제목을 볼 수 있습니다.
- 별을 한 번 누르면 별이 화면의 중앙에 오도록 시점이 변경되고, 한 번 더 누르면 별에 다가가면서 글 조회 모달이 띄워집니다.
- 이미지는 양쪽 화살표 버튼과 아래쪽의 페이지네이션으로 이동할 수 있습니다.
- 좋아요 버튼을 누를 수 있습니다.
- 내 별이면 수정/삭제할 수 있습니다.
- 글은 마크다운 형식으로 작성할 수 있으며, Preview 버튼을 누르면 마크다운이 적용된 글을 미리 볼 수 있습니다.
- 사진은 5장까지 첨부할 수 있습니다.
- 글 작성하고 다음 버튼을 누르면 별을 커스텀할 수 있습니다.
- 별의 양 옆에 있는 화살표 버튼을 통해 별의 모양을 변경할 수 있습니다.
- 색상, 크기, 밝기를 조절할 수 있습니다.
- 색상 추천 버튼을 누르면, CLOVA Sentiment api를 통해 글의 감정을 분석해 색상을 추천해줍니다.
- 글이 생성될 때와 삭제될 때 별에서 애니메이션이 발생합니다.
- 내 은하 나선팔 꼬인 정도, 나선팔 두께, 막대 길이, 은하 높이를 조절할 수 있습니다.
- 오른쪽 위의 되돌리기 버튼을 누르면 수정 이전의 내 은하 스타일로 돌아갑니다.
- 왼쪽 아래의 초기화 버튼을 누르면 기본 은하 스타일로 돌아갑니다.
- 체크박스를 통해 검색 허용 여부를 설정할 수 있습니다.
- 로그인하지 않은 사용자도 공유 링크를 통해 은하에 접근할 수 있습니다.
- 검색 허용된 사용자들의 우주를 닉네임 검색을 통해 구경할 수 있습니다.
- 검색해서 들어간 우주에서 왼쪽 위 뒤로가기 버튼을 누르면 다시 내 우주로 돌아옵니다.
yarn workspace client dev
yarn workspace server start:dev
- 우리 팀이 Zustand를 쓰는 이유
- Emotion 선택 시 고려사항
- Yarn berry로 모노레포 구성하기
- Vite, 왜 쓰는거지?
- 기술스택 선정이유 (NestJS, TypeORM, Docker, GitHub Actions)
- MySQL 선택 이유
프론트엔드의 주요 기술적 도전은 Three.js + React-Three-Fiber(R3F)를 사용한 우주 공간 구현이었습니다. 팀원 모두에게 생소한 기술이었기에 사용한 것 자체도 도전적인 경험이었지만, 그 중에서 특히 사용자 경험 개선 위주의 경험을 작성해보았습니다.
먼저 아래는 Three.js와 R3F에 관련하여 팀원들이 작성한 기술블로그입니다.
- Three.js와의 설레는 첫만남
- JS로 자전과 공전을 구현할 수 있다고?
- R3F Material 간단 정리
- 너와의 추억을 우주의 별로 띄울게
- React로 멋진 3D 은하 만들기(Feat.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을 활용한 유닛 테스트 등을 학습하고 적용해 보았습니다.
- 테스트 코드를 작성해야 하는 이유
- NetsJS + Jest 환경 설정
- NestJS, TDD로 개발하기
- 2주차 멘토링 일지(BE) - TDD 관련
- TDD 기록 1
- TDD 기록 2
- TDD 기록 3
- NestJS e2e 테스트 (jest, supertest)
- NestJS, 유닛 테스트 각종 mocking, e2e 테스트 폼데이터 및 파일첨부
인증/인가에 대해 고민도 많이 하였습니다.
Session vs JWT, Authorization Bearer vs Cookie, RefreshToken
특히 보안과 성능 및 편의성 사이의 트레이드오프에 대해 고민하고 학습을 하였습니다.
TypeORM 쿼리 로그를 통해 하나의 비즈니스 로직에서 복수개의 테이블을 수정하는 경우, 트랜잭션을 직접 제어할 필요가 있었습니다. 저희는 TypeORM의 queryRunner와 transaction 메소드, NestJS의 Interceptor 등을 활용하여 여러 차례 트랜잭션 제어 로직을 개선하였고, 각 구현방식의 장단점에 대해서도 학습할 수 있었습니다.
또한 쿼리 로그와 MySQL 쿼리 플랜 기능을 활용해 기존 TypeORM 메소드의 쿼리를 분석하고, 자주 사용되는 일부 메소드에 대해 이를 개선하여 queryBuilder로 개선된 쿼리를 요청하는 쿼리 최적화 과정도 수행해 보았습니다.
- Transaction(트랜잭션)
- TypeORM 트랜잭션(Transaction) 제어 with Query Runner 1
- TypeORM 트랜잭션(Transaction) 제어 with Query Runner 2
- NestJS Interceptor와 로거 - Transaction Interceptor
- transaction 제어 인터셉터 방식 -> 메소드 내부에서 수행하는 방식으로 변경
- TypeORM 쿼리 로그, MySQL 쿼리 플랜, Query Builder을 이용한 쿼리 최적화 with NestJS
NetsJS 자체에 대한 학습을 위해 NestJS의 Lifecycle과 각 Enhancer들에 대해서도 학습을 해보았습니다.
Interceptor, Exception Filter 등 학습을 하고 백엔드 코드에 적용을 해보았습니다.
클라우드 배포 경험이 많지 않아 이번 프로젝트를 통해 많은 성장을 할 수 있었습니다. AWS 및 NCP에서 제공하는 서버, VPC, NAT Gateway 등 주요 서비스에 대해 학습하여 배포 환경을 구성하고, Nginx, Docker 및 Docker Compose, GitHub Actions 등을 학습하여 main 브랜치에 push되면 자동으로 배포되도록 설정했습니다.
- GitHub Actions을 이용한 자동 배포
- AWS와 NCP의 주요 서비스
- NGINX 설정
- SSH 보안: Key Forwarding, Tunneling, 포트 변경
- Kubernetes 기초(minikube), docker image 최적화(멀티스테이징)
- NCP VPC&인스턴스 구성, MySQL, nginx, docker, docker-compose
- Redis 연결 후 RedisRepository 작성
- NCP Object Storage, HTTPS, GitHub Actions
- NAT Gateway, MongoDB
- 플랫폼 종속성 문제 해결(Sharp), 쿼리 최적화
- docker 이미지 최적화
리액트를 경험해보고 싶어서 Vite + React + TS를 활용해 간단한 admin 페이지를 만들어보았습니다.
admin용 계정 정보를 설정하고, 게시글 관리 및 컴퓨터 자원 사용량, 에러 로그의 차트를 볼 수 있습니다.
J010 김가은 | J016 김동민 | J053 박재하 | J073 송준섭 | J098 이백범 |
- 블로그: https://velog.io/@greencloud
- 깃허브: https://github.com/KimGaeun0806
- <별 하나에 글 하나>에서의 목표: 프로젝트 과정 하나하나 모두 기록으로 남기기. 기술블로그 열심히 써보기 👻
- 블로그: https://velog.io/@minboykim
- 깃허브: https://github.com/MinboyKim
- <별 하나에 글 하나>에서의 목표: 좋은사람들과 좋은시간보내기 ☕️
- 블로그: https://velog.io/@qkrwogk
- 깃허브: https://github.com/qkrwogk
- <별 하나에 글 하나>에서의 목표: 딥 다이브 경험! 🌊
- 블로그: https://velog.io/@songjseop
- 깃허브: https://github.com/SongJSeop
- <별 하나에 글 하나>에서의 목표: 팀원들과 후회 없는 시간 보내기
- 블로그: https://velog.io/@200tiger
- 깃허브: https://github.com/bananaba
- <별 하나에 글 하나>에서의 목표: 재미있는 결과물 만들기!
- J010 김가은