본문으로 건너뛰기
# 개발 실무

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

HTML 파일 1개, three.js 1개. 막히는 지점은 딱 3개였다. 실제 코드와 함께 정리했다.

요약. 인스타그램에서 본 3D 카드 회전 캐러셀을 그대로 재현했다. 이미지는 나노바나나(제미나이)로 20장, 코드는 바이브코딩으로 붙였고 결과물은 HTML 파일 한 개 + three.js 하나다. 빌드 도구도 프레임워크도 없다. 실제로 막힌 지점은 딱 세 개였고, 그 세 개만 알면 나머지는 요청으로 끝난다.

마지막 업데이트 : 2026.08.19

목차
  1. 무엇을 만들었나
  2. 왜 이게 따라 할 만한 작업인가
  3. 이미지부터 — 나노바나나는 프롬프트를 JSON으로 쓴다
  4. 문제 1 — 카드를 구 표면에 배치하기
  5. 문제 2 — 선택한 카드를 정면으로 가져오기
  6. 문제 3 — 확대하면 왜 뿌옇게 뭉개지나
  7. 남은 잔버그 3개 — 여러분도 똑같이 만난다
  8. 타이밍은 감이 아니라 숫자로 맞췄다
  9. 정리 — 순서대로만 하면 된다
  10. 자주 묻는 질문
↑ 이 글에서 만드는 결과물입니다. 드래그해서 돌려보세요. 전체 화면으로 보기 ↗

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번이 그 목록이다. 그대로 들고 시작하면 된다.

§자주 묻는 질문

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

칼럼을 읽고 궁금한 점이 생기셨다면,

결제 전에 1차 초안부터
먼저 받아보세요.

무료로 초안 받기

5페이지 기업 홈페이지 25만원 · 기능 추가 비용 0원 · 평균 응답 6시간 이내

S
슬로히어로 SLOHERO
웹 기획 · 제작

한의원, 웨딩 스튜디오, 갤러리 홈페이지를 직접 기획하고 만들었습니다. 만드는 사람이 직접 쓰기 때문에, 실제로 해본 것만 적습니다.