젠아이모는 Text-to-Motion ai 솔루션을 활용하여 고객들이 프롬프트를 입력하여 애니메이션을 자유롭게 생성 할 수 있습니다. 생성된 애니메이션을 3D뷰에서 실시간으로 확인 해볼 수 있으며 애니메이션을 편집 할 수 있습니다. 또한 편집한 애니메이션을 glb,fbx로 추출할 수 있습니다. 고객들이 자신의 모델을 업로드하여 리타겟팅 할 수 있는 기능이 있으며 리타겟팅 된 모델에 생성된 애니메이션을 적용해 볼 수 있습니다.
말은 이렇게 했지만 피그마보다 어떻게 좋겠습니까? 하지만 피그마가 사용성 적인 측면에서 개선해야할 점이 있습니다.
브라우저는 왼쪽마우스를 클릭시 onClick 이벤트가 있고 누른체로 움직일시 onDrag등 여러 이벤트들이 작동합니다. 그렇기 때문에 노션, 피그마 또는 윈도우 처럼 파란 네모상자로 요소를 선택하는 기능은 기존 이벤트를 제거하고 마우스 이벤트를 구현해야할 것입니다.
이를 쉽게 구현하기 위해서 쓰는 라이브러리가 react-selecto
입니다.
아래 이미지는 간단히 selecto를 사용 하는 방법입니다.
<Selecto
container={selectoContainer}
selectableTargets={[".file-item .file-content"]}
onSelect={(e) => {
우리는 이 selecto를 활용하기 위해서 영역을 지정해 줘야합니다. 위의 코드는 Selecto 컴포넌트에 영역을 selectoContainer 설정해주고 select를 할 타겟을 클래스로 명시 하였습니다. 이렇게 작업하면 지정한 영역에서 드래그시 파란색 상자가 나타나게 됩니다.
만약 영역에 콘텐츠들이 1줄이하라면 어떻게 될까요? 아래 그림은 Figma 드래프트 입니다. 영역에 초록색 패딩영역부터 드래그를 하지않으면 파란색 박스 생성되지 않습니다. 그래서 유저는 다중 선택이 안되나 보다 생각할 수 밖에 없습니다. (실제로 레퍼런스 체크중 누군가는 Figma에서 다중 선택이 있다 없다로 이야기가 나옴)
다시 콘텐츠를 2줄로 만들어 봅시다. 그러면 영역에 여유가 생겨서 편하게 다중 선택이 됩니다.
그렇다면 피그마처럼 유저가 1,2,3개 이렇게 적게 드래프트를 가진다면 타이트하게 끝에 맞춰서 드래그를 해야할까요?
아니죠. css로 selectoContainer
의 영역에 height를 100vh로 꽉차게 하던가 또는 minHeight어느 정도 주면 됩니다. 그렇게 되면 기본 영역이 길어지기 때문에 언제든 편하게 드래그를 할 수 있습니다. 이런 간단한 해결로도 사용성을 증가 시킬 수 있습니다.
(콘텍스트 메뉴, 다중 드래그 드랍 커스텀 ui )
기존 이벤트들을 삭제하고 구현한 기능들입니다. 선택 외의 다중 선택(Shift/Command 클릭, 마우스 드래그), 우클릭 컨텍스트 메뉴(복사/붙여넣기/삭제)등을 구현 하였습니다.
나는 무한 스크롤을 구현할때 올리브영 테크 블로그를 참고하였다. [테크]
서비스에서는 수많은 데이터를 한 번에 모두 불러올 수 없습니다. 특히 젠아이모의 epxlore는 GIF 이미지처럼 용량도 크고 렌더링 비용도 높은 콘텐츠이기 때문에, 페이지에 한꺼번에 다 보여주는 방식은 성능상 매우 비효율적입니다.
과거에는 페이지네이션(1, 2, 3…)을 통해 데이터를 나눠 보여주는 방식이 일반적이었지만, 요즘은 사용자 경험(UX) 측면에서 스크롤에 따라 자연스럽게 다음 데이터를 불러오는 ‘무한 스크롤(Infinite Scroll)’ 방식이 더 선호되고 있습니다.
무한 스크롤을 구현할 때 사용한 라이브러리는 다음과 같습니다.
react-query
react-intersection-observer
react-query를 무한스크롤 구현에서 언급 하려는 이유는 useInfiniteQuery 라는 내장Api를 제공해 주기 때문입니다. (업데이트…중)
persist
미들웨어와 함께 활용해, 유저의 프롬프트 히스토리를 관리 하였습니다.const indexedDBPersist: PersistStorage<any> = {
// item 가져오기
getItem: async (키이름) => {
const db = await openDB("앱에서쓸DB", 1);
const data = await db.get("상태를저장할스토어", 키이름);
return data ? { state: data } : null;
},
// item 저장하기
setItem: async (키이름, value) => {
const db = await openDB("앱에서쓸DB", 1);
if (value.state) {
// 전체 상태를 하나로 저장하거나, 필요한 조각만 분리해서 저장해도 됨
await db.put("상태를저장할스토어", value.state, 키이름);
} else {
console.error("저장할 상태가 없습니다.");
}
},
// item 삭제
removeItem: async (키이름) => {
const db = await openDB("앱에서쓸DB", 1);
await db.delete("상태를저장할스토어", 키이름);
},
};