이번 글에서는 내가 진행한 프로젝트를 소개하는 "프로젝트 섹션"의 카드에 hover 미리보기 영상과 클릭 시 전체 영상 재생 모달 기능을 어떻게 구현했는지 기록한다. 이번 기능은 기존의 정적인 카드 구성에서 한 단계 나아가, 사용자와의 인터랙션이 강화된 동적인 UI를 구성하는 데 집중했다.
CHOP!은 온라인 게임 아이템 현금 거래를 중개하는 서비스다. 기존의 복잡한 거래 방식을 개선하고, 사용자 간 안전한 직거래를 돕기 위해 기획했다. 해당 프로젝트는 PWA(Progressive Web App) 로 개발되어 모바일에서도 앱처럼 동작할 수 있게 구성했다.
카드는 다음과 같은 정보들로 구성했다:
썸네일 이미지 (CHOP! 로고)
프로젝트명, 설명, 기술 스택 리스트
GitHub 링크
hover 시 썸네일이 영상 미리보기로 전환되며 자동 재생됨
카드 클릭 시 zustand 기반 모달로 전체 영상이 재생됨
영상 미리보기는 video
태그에 다음과 같은 속성 및 이벤트로 구현했다:
<video
src={videoSrc}
muted
playsInline
className="absolute opacity-0 group-hover:opacity-100 ..."
onMouseEnter={(e) => {
e.currentTarget.currentTime = 0;
e.currentTarget.play();
}}
onMouseLeave={(e) => {
e.currentTarget.pause();
e.currentTarget.currentTime = 0;
}}
onTimeUpdate={(e) => {
if (e.currentTarget.currentTime > 5) {
e.currentTarget.pause();
}
}}
/>
muted와 playsInline은 브라우저 자동 재생 정책에 필수
group-hover와 opacity 전환을 통해 부드럽게 썸네일과 교체됨
영상은 항상 0초부터 재생되며 5초까지만 보여주고 자동 멈춤
hover는 일시적 미리보기용이지만, 프로젝트를 실제로 보여줄 땐 모달로 전체 영상을 보여준다. 이를 위해 zustand를 도입했다.
modalStore.ts
import { create } from 'zustand';
export const useModalStore = create((set) => ({
isOpen: false,
videoSrc: '',
openModal: (src) => set({ isOpen: true, videoSrc: src }),
closeModal: () => set({ isOpen: false, videoSrc: '' }),
}));
ProjectCard.tsx 클릭 시 실행
onClick={() => videoSrc && openModal(videoSrc)}
ProjectModal.tsx
const { isOpen, videoSrc, closeModal } = useModalStore();
{isOpen && (
<video src={videoSrc} controls autoPlay muted ... />
)}
zustand를 사용하면 프로젝트 카드가 몇 개가 되든 매우 유연하게 확장할 수 있다. 구조는 아래처럼 유지하면 된다:
const projects = [
{
title: 'CHOP!',
videoSrc: '/videos/chop.mp4',
...
},
{
title: 'Next Project!',
videoSrc: '/videos/next.mp4',
...
}
];
map()을 돌려서 <ProjectCard {...project}
/> 형태로 넘기면 각각의 카드에서 개별 영상 미리보기 및 모달 재생이 가능하다.
그동안 간단한 상태 관리는 대부분 useState를 통해 해결해왔다. 예를 들어 모달을 여닫는 상태나 영상 경로 같은 것도 상위 컴포넌트에서 useState로 관리하고 하위 컴포넌트로 props로 넘겨주는 방식이었다.
하지만 프로젝트 카드가 많아지고, 특정 카드에서만 영상을 재생하거나 모달을 띄워야 할 때 props 전달이 점점 복잡해졌다. 이때 zustand를 도입하니까 다음과 같은 장점이 있었다:
어디서든 바로 상태를 읽고 쓸 수 있다: 하위 컴포넌트에서 바로 openModal()을 호출할 수 있어 코드가 훨씬 깔끔하다.
props drilling이 사라졌다: 상위 → 하위로 props를 계속 전달하지 않아도 된다.
유지보수가 쉬워졌다: 모달 관련 로직이 하나의 store 파일에 모여 있어 관리가 용이했다.
zustand는 처음 써봤지만, 구조가 간단해서 Redux보다 진입장벽도 낮고 직관적이었다. 특히 모달, 필터, 탭 같은 UI 상태에 최적화돼 있다는 점에서 앞으로도 계속 활용하게 될 것 같다.
hover로 영상 미리보기 기능 구현 (썸네일 교체 + 자동재생)
zustand로 전역 상태를 관리하며 모달을 열고 닫음
모달에선 autoPlay, muted, controls를 사용해 전체 영상 재생
프로젝트가 많아져도 확장 가능하게 props 중심 설계
useState와 비교해 zustand의 유연함과 깔끔한 코드 구조를 직접 체감함
이번 프로젝트 섹션에서는 인터랙션을 강조한 사용자 경험에 집중했다. 단순히 텍스트 중심의 카드가 아니라, hover 시 미리보기가 제공되고, 클릭 시 자연스럽게 전체 영상이 재생되는 방식으로 실제 서비스 느낌을 살리고자 했다.
zustand를 도입한 이후에는 상태를 더 직관적이고 간결하게 관리할 수 있었고, useState 기반의 props 전달 방식보다 훨씬 깔끔한 코드 구조를 유지할 수 있었다. 앞으로 다른 모달, 탭, 필터 같은 UI에도 zustand를 활용할 계획이다.
다음은 프로젝트 상세 페이지를 개별 라우팅하거나, 프로젝트별 상세 설명을 별도 구성하는 작업으로 이어갈 예정이다.