SLOHERO®

바이브코딩으로 인터랙티브 웹 만들기 — 나노바나나 이미지 20장으로 끝낸 3D 캐러셀

SLOHERO · 2026년 8월 19일 · 제작 노트
↑ 이 글에서 만드는 결과물입니다. 드래그해서 돌려보세요. 전체 화면으로 보기 ↗
3줄 요약. 인스타그램에서 본 3D 카드 회전 캐러셀을 그대로 재현했다. 이미지는 나노바나나(제미나이)로 20장, 코드는 바이브코딩으로 붙였고 결과물은 HTML 파일 한 개 + three.js 하나다. 빌드 도구도 프레임워크도 없다. 실제로 막힌 지점은 딱 세 개였고, 그 세 개만 알면 나머지는 요청으로 끝난다.
글보다 직접 굴려보는 게 빠릅니다.
드래그로 돌리고, 카드를 클릭하면 확대됩니다.
데모 열기 →

1.무엇을 만들었나

카드 45장이 지구본처럼 구를 이루고 천천히 돈다. 그러다 카드 하나를 향해 화면이 쑥 들어가 1초쯤 머물고, 다시 물러나 전체 구를 보여준다. 마우스로 드래그하면 손으로 굴릴 수 있고, 카드를 클릭하면 확대, 한 번 더 클릭하면 원래 크기로 돌아온다.

말로 쓰면 복잡한데 구성은 이게 전부다.

빌드 도구 없음. npm install 없음. 파일을 브라우저로 열면 바로 돈다.

2.왜 이게 따라 할 만한 작업인가

인터랙티브 웹사이트라고 하면 보통 웹팩 설정, React, 상태관리, 셰이더 공부 6개월을 떠올린다. 실제로는 막히는 지점이 딱 3개였고 나머지는 AI에게 시키면 됐다. 바이브코딩이 실패하는 경우는 대부분 이 3개를 모르는 채로 "멋있게 만들어줘"라고 하기 때문이다.

  1. 카드를 구 표면에 어떻게 배치하지?
  2. 선택한 카드가 정면에 오게 하려면 몇 도를 돌려야 하지?
  3. 확대했을 때 왜 화면이 뿌옇게 뭉개지지?

이 3개만 이해하면 나머지는 "이렇게 해줘"로 끝난다. 하나씩 보자.

3.이미지부터 — 나노바나나는 프롬프트를 JSON으로 쓴다

먼저 이미지가 있어야 한다. 카드는 45장이지만 이미지는 20장이면 충분하다. 중요한 건 20장이 따로 노는 게 아니라 한 세트로 보이는 것이다. 그래서 프롬프트를 문장이 아니라 JSON으로 썼다.

{
  "style": "editorial studio photograph, soft diffused light",
  "background": "seamless warm grey #dedede, no gradient",
  "subject": "a single ceramic vase, centered",
  "framing": "subject fills 66% of frame height",
  "aspect_ratio": "4:3",
  "negative": "text, watermark, harsh shadow, busy background"
}

style, background, framing, aspect_ratio는 20장 전부 고정하고 subject만 바꾼다. 이렇게 하면 매번 다른 톤으로 뽑히는 사고가 안 난다.

꼭 지킬 것 하나. aspect_ratio를 격자 칸 비율과 똑같이 맞춰라. 나는 1.27:1을 썼다. 이 기준이 틀리면 카드마다 이미지가 잘려 지저분해진다. 피사체가 프레임에서 차지하는 비율(66%)까지 고정한 이유도 같다. 어떤 건 크고 어떤 건 작으면 구가 울퉁불퉁해 보인다.

4.문제 1 — 카드를 구 표면에 배치하기

가장 많이 헤맨 부분이다. 원통, 바퀴, 나선까지 네 번을 다시 만들었다. 정답은 지구본의 위도·경도 격자였고, 핵심은 한 줄이다. 적도에서 멀어질수록 열 개수를 cos(위도)만큼 줄인다.

for (let r = 0; r < ROWS; r++) {
  const lat  = -Math.PI/2 + (Math.PI/ROWS) * (r + 0.5);
  const cols = Math.max(MIN_COLS, Math.round(COLS_EQ * Math.cos(lat)));
  //                                          ↑ 이 cos 하나가 전부다
  for (let c = 0; c < cols; c++) {
    const lon = (Math.PI*2/cols) * c + (r % 2) * (Math.PI/cols); // 벽돌쌓기
    const cl = Math.cos(lat), sl = Math.sin(lat);
    cells.push({ dir: new THREE.Vector3(Math.cos(lon)*cl, sl, Math.sin(lon)*cl) });
  }
}

이걸 빼면 극지방에서 카드들이 서로를 뚫고 겹친다. cos을 넣는 순간 위로 갈수록 자연스럽게 줄어든다. 위도 띠 7줄, 적도 11열로 시작해 총 45장이 나왔다.

여기에 두 가지를 더했다.

이미지 20장으로 45장을 채우는 방법도 여기서 나온다. i % 20을 쓰면 같은 세로줄에 같은 이미지가 반복돼 티가 확 난다. 황금비를 쓰면 흩어진다.

const IMG_IDX = i => Math.floor(((i * 0.6180339887498949) % 1) * 20);
//                                    ↑ 황금비. 규칙적으로 보이지 않게 퍼뜨린다

5.문제 2 — 선택한 카드를 정면으로 가져오기

카드 위치 벡터만 알면 삼각함수 두 줄이다.

const d = card.position.clone().normalize();
const spinY = Math.atan2(-d.x, d.z);                     // 좌우로 몇 도
const tiltX = Math.atan2(d.y, Math.hypot(d.x, d.z));     // 위아래로 몇 도

여기서 한 번 크게 깨졌다. 회전과 기울기를 같은 오브젝트에 넣었더니 카드가 엉뚱한 데로 갔다. 3D 회전은 순서가 바뀌면 결과가 달라지기 때문이다(오일러 각). 그룹을 세 겹으로 쪼개니 해결됐다.

roll(Z축) > tilt(X축) > ring(Y축) > 카드들

각 그룹이 축 하나씩만 담당하면 순서가 꼬일 일이 없다. 이런 건 AI에게 "회전이 이상해"라고만 하면 못 고친다. "오일러 순서 때문인 것 같으니 그룹을 축별로 분리해줘"라고 해야 한 번에 된다. 바이브코딩이 잘 먹히는 조건이 이것이다 — 증상이 아니라 원인을 말할 것.

그리고 카메라가 카드까지 날아가는 대신 카드를 앞으로 끌어내고 1.12배 키웠다. 카메라만 들이대면 화면이 카드로 꽉 차서(94%) 답답하다. 원본 영상은 63%였고, 카드를 끌어내는 방식이 이 여백을 살린다.

6.문제 3 — 확대하면 왜 뿌옇게 뭉개지나

거리에 따라 흐리게 만드는 코드를 넣었더니 카메라가 구 안쪽으로 들어갔을 때 전부 뭉개졌다. 안쪽에서는 카드가 가까운데도 "멀다"고 계산된 것이다. 한 줄로 끝났다.

t *= (1 - inside01 * 0.88);   // 구 안에 있으면 흐리게 할 이유가 없다

블러 자체도 후처리 없이 간다. 밉맵 레벨만 낮춰서 읽으면 공짜다.

vec4 c = texture2D(uMap, uv, dim * uBlur);   // 세 번째 인자가 밉 바이어스

포스트프로세싱을 붙였으면 코드가 세 배로 늘고 모바일에서 버벅였을 것이다.

7.남은 잔버그 3개 — 여러분도 똑같이 만난다

완성 후에도 잔버그 3개가 남는다 — 모바일 터치 관성, 다크모드 전환 시 잔상, 첫 로딩 흰 화면. 셋 다 원인이 같고(상태 초기화 누락) 아래 코드로 해결된다.

8.타이밍은 감이 아니라 숫자로 맞췄다

"영상처럼 자연스럽게"는 AI에게 시켜도 안 된다. 원본 영상을 ffmpeg으로 프레임 단위로 뜯어 재봤다.

구간원본 영상내 결과
한 바퀴 주기3.42~3.72초3.64초
확대 상태 유지1.88초1.83초
축소 상태 유지1.40초1.64초
전환0.35초0.42초

이 숫자를 그대로 상수로 박았다.

const TOUR = { OUT_HOLD: 1820, IN_HOLD: 880, FLY_IN: 420, FLY_OUT: 420, FLY_BACK: 540 };

이 방법을 추천한다. 참고 영상이 있다면 ffmpeg -i ref.mp4 -vf fps=10 f%03d.png으로 프레임을 뽑고, 어느 프레임에서 멈추고 어느 프레임에서 움직이는지 세보라. 30분이면 된다. "느낌"으로 열 번 고치는 것보다 빠르다.

9.정리 — 순서대로만 하면 된다

순서만 지키면 된다 — 이미지(JSON 프롬프트) → 뼈대(three.js 6단계) → 인터랙션 → 잔버그 수리. 코딩 지식보다 "무엇을 요청할지"가 결과를 가른다.

  1. 나노바나나로 이미지 20장. 스타일·배경·비율은 JSON으로 고정, subject만 교체
  2. 격자 좌표 만들기. cos(위도)로 열 줄이기 + 빈칸 14%
  3. 카드 배치. mesh.lookAt(pos.clone().multiplyScalar(2)) — PlaneGeometry는 +Z가 정면이라 이 한 줄로 끝난다
  4. 클릭 시 정면 정렬. atan2 두 줄 + 그룹 3단 분리
  5. 타이밍 상수 박기. 참고 영상에서 실측
  6. 잔버그 3종 처리. 뒷면 UV / 실시간 트윈 / 각도 래핑

총 962줄, 파일 하나다. 나는 구조를 네 번 갈아엎었는데 위 순서대로 가면 그럴 일이 없다.

바이브코딩으로 인터랙티브 웹을 만들 때 진짜 실력은 코드를 쓰는 게 아니라 어디서 막힐지 미리 아는 것이다. 이 글의 4~7번이 그 목록이다. 그대로 들고 시작하면 된다.

이런 인터랙티브 웹, 견적 확정가 안에서 만듭니다.
결제 전 1차 초안을 먼저 확인하고 결정하세요.
SLOHERO 견적 문의 →

§자주 묻는 질문

Q. three.js를 몰라도 되나요?
Scene, Camera, Mesh 세 단어의 뜻만 알면 됩니다. 나머지는 위 6단계를 그대로 요청하면 나옵니다. 정확히 말하면 three.js를 배우는 게 아니라 무엇을 요청해야 하는지를 배우는 겁니다. 바이브코딩의 실제 학습 곡선은 여기에 있습니다.
Q. 이미지가 20장보다 적으면요?
됩니다. IMG_COUNT만 바꾸면 됩니다. 다만 10장 밑으로 내려가면 황금비로 흩어도 반복이 눈에 띕니다.
Q. 모바일에서 돌아가나요?
돕니다. 포스트프로세싱을 안 썼기 때문입니다. 대신 카드 45장 × 텍스처 20장이라 이미지는 가로 1100px 정도로 줄여서 넣으세요.
Q. 라이선스는요?
three.js는 MIT입니다. 이미지는 나노바나나로 직접 생성한 것이니 문제없습니다.