<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 김기선</title>
    <description>The latest articles on DEV Community by 김기선 (@slohero).</description>
    <link>https://dev.to/slohero</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4099924%2F32d16a18-9e17-4413-9e62-a3723c06a697.png</url>
      <title>DEV Community: 김기선</title>
      <link>https://dev.to/slohero</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/slohero"/>
    <language>en</language>
    <item>
      <title>바이브코딩으로 인터랙티브 웹 만들기 — 나노바나나 이미지 20장으로 끝낸 3D 캐러셀</title>
      <dc:creator>김기선</dc:creator>
      <pubDate>Sat, 29 Aug 2026 07:38:53 +0000</pubDate>
      <link>https://dev.to/slohero/baibeukodingeuro-inteoraegtibeu-web-mandeulgi-nanobanana-imiji-20jangeuro-ggeutnaen-3d-kaereosel-298i</link>
      <guid>https://dev.to/slohero/baibeukodingeuro-inteoraegtibeu-web-mandeulgi-nanobanana-imiji-20jangeuro-ggeutnaen-3d-kaereosel-298i</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;이 글은 원래 &lt;a href="https://slohero.com/blog/vibe-coding-interactive-web" rel="noopener noreferrer"&gt;SLOHERO 블로그&lt;/a&gt;에 게시되었습니다.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. 무엇을 만들었나
&lt;/h2&gt;

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

&lt;p&gt;말로 쓰면 복잡한데 구성은 이게 전부다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;orbit.html&lt;/strong&gt; — 962줄. HTML·CSS·JS·셰이더가 전부 여기 있다&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;images/&lt;/strong&gt; — card-01.jpg ~ card-20.jpg&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;빌드 도구 없음. &lt;code&gt;npm install&lt;/code&gt; 없음. 파일을 브라우저로 열면 바로 돈다.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. 왜 이게 따라 할 만한 작업인가
&lt;/h2&gt;

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

&lt;ol&gt;
&lt;li&gt;카드를 구 표면에 어떻게 배치하지?&lt;/li&gt;
&lt;li&gt;선택한 카드가 정면에 오게 하려면 몇 도를 돌려야 하지?&lt;/li&gt;
&lt;li&gt;확대했을 때 왜 화면이 뿌옇게 뭉개지지?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;이 3개만 이해하면 나머지는 "이렇게 해줘"로 끝난다. 하나씩 보자.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. 이미지부터 — 나노바나나는 프롬프트를 JSON으로 쓴다
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"editorial studio photograph, soft diffused light"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"background"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"seamless warm grey #dedede, no gradient"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a single ceramic vase, centered"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"framing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subject fills 66% of frame height"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aspect_ratio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4:3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"negative"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"text, watermark, harsh shadow, busy background"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;h2&gt;
  
  
  4. 문제 1 — 카드를 구 표면에 배치하기
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ROWS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lat&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;ROWS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MIN_COLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;COLS_EQ&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="c1"&gt;//                                          ↑ 이 cos 하나가 전부다&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;cols&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 벽돌쌓기&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;sl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;cells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;THREE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Vector3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lon&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;cl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;여기에 두 가지를 더했다.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;빈칸 14%&lt;/strong&gt; — 일부러 자리를 비운다. 그 틈으로 반대편 카드가 비쳐 보이면서 "속이 빈 껍질"이라는 게 드러난다. 꽉 채우면 그냥 공이다.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;홀수 줄 반 칸 밀기&lt;/strong&gt; — 벽돌 쌓듯 어긋나게 해서 세로줄이 눈에 띄지 않게 한다.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IMG_IDX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.6180339887498949&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;//                                    ↑ 황금비. 규칙적으로 보이지 않게 퍼뜨린다&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. 문제 2 — 선택한 카드를 정면으로 가져오기
&lt;/h2&gt;

&lt;p&gt;카드 위치 벡터만 알면 삼각함수 두 줄이다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;spinY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                     &lt;span class="c1"&gt;// 좌우로 몇 도&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tiltX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hypot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;     &lt;span class="c1"&gt;// 위아래로 몇 도&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;roll(Z축) &amp;gt; tilt(X축) &amp;gt; ring(Y축) &amp;gt; 카드들
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

&lt;h2&gt;
  
  
  6. 문제 3 — 확대하면 왜 뿌옇게 뭉개지나
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;inside01&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.88&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 구 안에 있으면 흐리게 할 이유가 없다&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;블러 자체도 후처리 없이 간다. 밉맵 레벨만 낮춰서 읽으면 공짜다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="kt"&gt;vec4&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;texture2D&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;uBlur&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// 세 번째 인자가 밉 바이어스&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  7. 남은 잔버그 3개 — 여러분도 똑같이 만난다
&lt;/h2&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;뒷면 이미지가 좌우 반전&lt;/strong&gt; → 셰이더에서 &lt;code&gt;gl_FrontFacing&lt;/code&gt;으로 UV를 뒤집는다&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;60fps에선 0.6초, 15fps에선 2.4초&lt;/strong&gt; → 매 프레임 일정 비율로 보간(lerp)하면 이렇게 된다. &lt;code&gt;performance.now()&lt;/code&gt; 기준 실제 경과 시간으로 바꿔야 한다&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;각도가 계속 커져서 이상해짐&lt;/strong&gt; → 삼각함수에 넣기 전에 &lt;code&gt;((v % m) + m) % m&lt;/code&gt;으로 감싼다&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. 타이밍은 감이 아니라 숫자로 맞췄다
&lt;/h2&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;구간&lt;/th&gt;
&lt;th&gt;원본 영상&lt;/th&gt;
&lt;th&gt;내 결과&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;한 바퀴 주기&lt;/td&gt;
&lt;td&gt;3.42~3.72초&lt;/td&gt;
&lt;td&gt;3.64초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확대 상태 유지&lt;/td&gt;
&lt;td&gt;1.88초&lt;/td&gt;
&lt;td&gt;1.83초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;축소 상태 유지&lt;/td&gt;
&lt;td&gt;1.40초&lt;/td&gt;
&lt;td&gt;1.64초&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;전환&lt;/td&gt;
&lt;td&gt;0.35초&lt;/td&gt;
&lt;td&gt;0.42초&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;이 숫자를 그대로 상수로 박았다.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TOUR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;OUT_HOLD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1820&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;IN_HOLD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;880&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;FLY_IN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;420&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;FLY_OUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;420&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;FLY_BACK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;540&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h2&gt;
  
  
  9. 정리 — 순서대로만 하면 된다
&lt;/h2&gt;

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

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

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

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

&lt;h2&gt;
  
  
  자주 묻는 질문
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q. three.js를 몰라도 되나요?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Scene&lt;/code&gt;, &lt;code&gt;Camera&lt;/code&gt;, &lt;code&gt;Mesh&lt;/code&gt; 세 단어의 뜻만 알면 됩니다. 나머지는 위 6단계를 그대로 요청하면 나옵니다. 정확히 말하면 three.js를 배우는 게 아니라 &lt;strong&gt;무엇을 요청해야 하는지&lt;/strong&gt;를 배우는 겁니다. 바이브코딩의 실제 학습 곡선은 여기에 있습니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. 이미지가 20장보다 적으면요?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;됩니다. &lt;code&gt;IMG_COUNT&lt;/code&gt;만 바꾸면 됩니다. 다만 10장 밑으로 내려가면 황금비로 흩어도 반복이 눈에 띕니다.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. 모바일에서 돌아가나요?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;됩니다. 포스트프로세싱을 안 썼기 때문입니다. 대신 카드 45장 × 텍스처 20장이라 이미지는 가로 1100px 정도로 줄여서 넣으세요.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q. 라이선스는요?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;three.js는 MIT입니다. 이미지는 나노바나나로 직접 생성한 것이니 문제없습니다.&lt;/p&gt;




&lt;p&gt;원문: &lt;a href="https://slohero.com/blog/vibe-coding-interactive-web" rel="noopener noreferrer"&gt;https://slohero.com/blog/vibe-coding-interactive-web&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>threejs</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
