본문으로 건너뛰기
김민찬 포트폴리오

인터랙티브 포트폴리오 제작기

Vercel Ship 2024에서 느낀 출입증 인터랙션의 인상을 출발점으로, 경력 탐색과 접근성·로딩 성능을 함께 만족하는 포트폴리오를 설계했습니다.

배경

Vercel Ship 2024에 참여했을 때 화면 속 출입증을 잡아당기고 놓는 인터랙션이 인상적이었습니다. 줄의 장력과 카드의 무게가 느껴지는 효과를 포트폴리오에 적용해 보고 싶었습니다.

그대로 복제하는 것만으로는 포트폴리오의 첫 화면이 될 이유가 부족했습니다. 제가 다닌 회사의 사원증과 개인 프로젝트 카드를 첫 화면의 목차로 만들고, 카드를 당기는 행동이 실제 경력과 작업을 여는 탐색 방식이 되도록 방향을 바꿨습니다.

문제

  • 3D 효과 안에 텍스트를 그리면 WebGL이나 JavaScript를 사용할 수 없는 환경에서 경력 정보가 사라집니다.
  • 카드마다 Canvas와 물리 월드를 만들면 초기 로딩과 렌더링 비용이 카드 수만큼 늘어납니다.
  • 드래그만 진입 경로로 제공하면 키보드 사용자와 정밀한 포인터 조작이 어려운 사용자가 상세를 열 수 없습니다.
  • 장식적인 움직임이 경력과 성과보다 먼저 보이면 포트폴리오의 목적이 흐려질 수 있습니다.

선택

카드의 회사명, 직급, 재직 기간은 HTML에 유지하고 줄의 움직임과 카드의 위치만 React Three Fiber와 Rapier가 계산하도록 경계를 나눴습니다. 세 카드는 하나의 Canvas와 Physics 월드를 공유하되 서로 충돌하지 않는 독립된 줄을 사용합니다.

데스크톱에서는 React 초기화 직후 물리 모듈을 불러옵니다. 처음에는 카드에 포인터나 키보드 포커스가 들어온 뒤 로딩했지만, 조작하려는 순간 대기하게 되어 준비 시점을 앞당겼습니다. 모바일, 운영체제의 모션 감소 설정, WebGL 실패, JavaScript 비활성 환경에서는 같은 HTML 카드와 링크가 탐색을 담당합니다.

하나의 콘텐츠를 물리 경로와 정적 경로가 공유하는 구조
flowchart TB
data["경력 데이터 + 카드 디자인"] --> html["HTML 카드 면"]
data --> scene["공유 Canvas + Physics"]
intent["pointer / focus 의도"] --> scene
scene --> transform["위치 · 회전 transform"]
transform --> html
fallback["mobile / reduced motion / WebGL 실패"] --> html
html --> detail["경력 상세 + Case Study"]

구현

효과와 콘텐츠의 렌더링 경계

Canvas는 줄의 곡선과 카드의 위치·회전만 계산합니다. 카드 앞면은 별도의 HTML 요소라 회사명, 직급, 기간을 그대로 읽을 수 있고 배경색과 줄 색상도 데이터로 주입할 수 있습니다. 아하랩스, 티맥스 클라우드, 개인 프로젝트가 같은 컴포넌트를 사용하면서 각기 다른 사원증 형태를 갖습니다.

세 카드를 하나의 물리 월드에 배치

선반이 카드 열의 중심과 좌우 경계를 측정해 세 rig의 고정점을 정합니다. 각 카드는 선반 전체 폭에서 움직일 수 있지만 충돌 그룹은 사용하지 않아 다른 카드를 밀지 않습니다. 드래그한 카드만 높은 쌓임 순서를 받아 서로 겹치는 동안에도 조작 대상을 유지합니다.

처음에는 활성 카드마다 Canvas를 교체하는 구조를 검토했습니다. 카드 사이를 이동할 때마다 컨텍스트와 물리 상태를 다시 만드는 비용이 생겨, 선반 하나가 Canvas와 Physics 월드 하나를 소유하는 구조로 바꿨습니다.

당김 상태를 상세 진입 계약으로 사용

포인터를 누르면 기간 아래 공간이 밀리며 아래로 당겨서 열기가 나타납니다. 100px을 넘으면 놓아서 상세 보기로 바뀌고, 최대 이동 거리는 112px로 제한합니다. 짧게 당기면 원래 위치로 돌아가며 pointer cancel, Escape, 창 비활성화와 화면 이탈은 상세를 열지 않고 드래그를 해제합니다.

카드 자체는 링크이므로 클릭과 Enter도 같은 상세를 엽니다. 상세 진입 시 제목으로 포커스를 옮기고 경력으로 돌아가기를 누르면 선택했던 카드에 다시 포커스를 둡니다.

실패를 별도 화면으로 만들지 않기

물리 모듈은 별도 청크로 분리하고 지원되는 데스크톱 환경에서 초기화 직후 로딩합니다. 준비 중에는 spinner를 표시하고 드래그를 잠시 막되 클릭과 Enter 진입은 유지합니다. WebGL 초기화나 컨텍스트가 실패하면 공유 Canvas를 제거하고 세 카드 모두 정적 상태로 복구합니다. 정적 카드에도 동일한 당김 임계값과 링크가 있어 실패 안내를 거치지 않고 계속 탐색할 수 있습니다.

검증

  • 카드 크기와 회사별 줄·카드 배색을 데스크톱과 모바일에서 확인합니다.
  • 100px 전후의 안내 문구와 상세 진입, 취소 후 복귀를 브라우저 동작으로 검사합니다.
  • 데스크톱에서 사용자 조작 없이 물리 준비가 시작되고, 이후에도 Canvas가 하나만 유지되는지 확인합니다.
  • WebGL 초기화 실패와 컨텍스트 유실 뒤에도 카드 클릭과 상세 이동이 가능한지 검사합니다.
  • 모션 감소와 JavaScript 비활성 환경에서 같은 콘텐츠와 링크가 남는지 확인합니다.

결과

첫 화면의 세 카드는 경력과 개인 작업을 선택하는 목차가 됐습니다. 시각적인 무게감은 물리 레이어가 담당하고, 경력 정보와 탐색은 HTML이 담당해 어느 한쪽이 실패해도 콘텐츠가 사라지지 않습니다.

공유 Canvas 전환으로 카드를 바꿀 때마다 렌더링 컨텍스트를 다시 만들지 않게 됐습니다. 데스크톱에서는 초기화 직후 3D 코드를 요청하고, 로드한 뒤에는 세 카드가 같은 물리 월드를 재사용합니다.

트레이드오프

정적 카드와 물리 카드의 두 실행 경로가 있어 취소와 복귀 동작을 함께 검증해야 합니다. 대신 모바일과 실패 환경을 별도의 축소판으로 만들지 않고 동일한 콘텐츠 구조를 유지할 수 있습니다.

물리 효과는 첫인상을 만들지만 프로젝트의 결과를 대신하지 않습니다. 홈에서는 카드 선택에 집중하고, 구체적인 문제와 판단은 다른 글과 같은 Case Study 형식으로 분리했습니다.