텍스트 어드벤처 게임을 개발하는 바이브코딩 삽질의 기록

소개

주말에 학생을 대상으로 바이브코딩을 진행하고 있습니다. 원래 고등학생들을 대상으로 모집을 했는데 모집이 안 되니까 진행하시는 곳에서 초등학생을 대상으로 개강을 해 버렸습니다.

첫번째 4회 수업에서 알게된 사실은 초등학생은 구글 AI 스튜디오 정도 사용이 가능한데 구글 로그인을 못 한다는 것입니다. 13세는 되어야 합니다.

부모님이 로그인을 해 주셔야 하는데 엄마~ 를 너무 부르고 전화를 걸고 난리라 2번째 개강 때는 아예 부모님 1인과 함께하는 과정으로 오픈했습니다.

진행 방법

어떤 도구를 사용했고, 어떻게 활용하셨나요?

구글 AI 스튜디오를 사용할 수도 있었지만, 부모님 구글 계정을 사용할 수 있었기 때문에 18세 이상 가능한 Antigravity를 이용하기로 했습니다. 부모님들도 유료 계정이 없으셔서 최대한 적은 토큰으로 3시간 안에 완성을 해야 하는 어려움이 있습니다.

토큰이 모자라면 구글 AI 스튜디오를 사용하도록 해서 개발을 하고 있습니다.

Tip: 사용한 프롬프트 전문을 꼭 포함하고, 내용을 짧게 소개해 주세요.

// 아래 내용을 구현하지 말고 설계 예시로 만들어줘.
게임명: 한국사 답사 어드벤처
배경/주인공 설정: 현대/초등학교 학생
성별: 남
이름: 연우
시놉시스(줄거리)
모험의 시작(왜 모험을 시작해야 하는가를 설명하는 곳): 주인공 연우는 삼촌을 만나러 가던 길에 우연히 상자를 하나 발견하게 된다. 이거 뭐지? 상자를 열었더니 나비 한 마리가 날아오르며 "고맙다"고 인사를 한다. 자신은 나비 왕국의 왕자인데 사악한 주술에 갇혀있었다는 것이다. 나비가 고맙다고 하고 사라진 후 연우가 다시 길을 가고 있는데 연우의 앞에 갑자기 나타난 사악한 악령.
사악한 악령: 네 녀석이 나비 왕국의 왕자를 풀어줬겠다.
연우:
대사1-1A. 저는 모르는 일입니다. -> 2번
대사1_2A. 왕자를 가둔건 잘못한 일 아닌가요? -> 3번
사악한 악령:
대사2. 모르는 일이라고 거짓말을 하다니. 너의 손에 있는 나비의 가루가 그 증거인데 죽어라. 얏! -> Ending(Bad) 화면으로 이동
대사3. 그렇다면 네가 얼마나 도덕적인 인간인지 시험해 보겠다. 내가 내는 문제를 맞춰보아라.
AI에게 요청: 사지선다 도덕문제 10개를 출제해줘.
문제를 10개 ~ 8개를 맞추면 Ending(Good) 화면으로 이동
문제를 7개 ~ 4개를 맞추면 Ending(Soso) 화면으로 이동
문제를 3개 ~ 0개를 맞추면 Ending(Bad) 화면으로 이동
Ending(Good)
연우(주인공이름)는 사악한 악령을 물리치고 나비 왕국에 초대되어 금은보화를 선물로 받았습니다.
Ending(Soso)
연우(주인공이름)는 사악한 악령의 공격을 받고 기절했다가 반나절 만에 집에 겨우 돌아왔습니다.
Ending(Bad)
연우(주인공이름)는 사악한 악령의 공격을 받고 부산으로 버려져 며칠간 떠돌다 겨우 집에 돌아왔습니다.

예전에 교육용 콘텐츠 개발했던 내용과 비슷하게 마크다운 파일을 만들고, 채우는 형태로 진행을 했습니다.

Tip: 활용 이미지나 캡처 화면을 꼭 남겨주세요.

아이폰 한국어 앱 스크린샷

원래 대학생들은 엑셀로 퀘스트 기획서를 쓰도록 하는데... 이걸 마크다운으로 처음 만들었는데 아이들도 너무 힘들어하고 어머님들도 너무 힘들어하고. 학생 개개인에게 설명을 하려니 날씨도 더운데 너무 힘들었습니다.

Tip: 코드 전문은 코드블록에 감싸서 작성해주세요. ( / 을 눌러 '코드 블록'을 선택)


<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>한국사 답사 어드벤처</title>
<link href="https://fonts.googleapis.com/css2?family=Black+Han+Sans&family=Noto+Sans+KR:wght@400;700&display=swap" rel="stylesheet">
<style>
  :root{
    --stage-1:#1a1440;
    --stage-2:#2d1b69;
    --stage-3:#3d2381;
    --gold:#ffc93c;
    --gold-deep:#e6a800;
    --coral:#ff6b9d;
    --teal:#4ecdc4;
    --silver:#cfd8e3;
    --wrong:#ff4d6d;
    --good:#4ecdc4;
    --soso:#ffc93c;
    --bad:#ff4d6d;
    --ink:#f5f3ff;
    --ink-dim:#c9c2ea;
  }
  *{box-sizing:border-box; margin:0; padding:0;}
  html,body{height:100%;}
  body{
    font-family:'Noto Sans KR', sans-serif;
    color:var(--ink);
    background:
      radial-gradient(ellipse 60% 45% at 50% 0%, rgba(255,201,60,0.18), transparent 60%),
      radial-gradient(ellipse 80% 60% at 20% 100%, rgba(255,107,157,0.12), transparent 60%),
      linear-gradient(180deg, var(--stage-1) 0%, var(--stage-2) 55%, var(--stage-3) 100%);
    min-height:100vh;
    overflow-x:hidden;
    position:relative;
  }
  .beams{ position:fixed; inset:0; pointer-events:none; z-index:0; opacity:0.35; }
  .beam{ position:absolute; top:-10%; width:140px; height:120%;
    background:linear-gradient(180deg, rgba(255,201,60,0.35), transparent 70%);
    filter:blur(6px); transform-origin:top center; }
  .beam:nth-child(1){ left:8%; transform:rotate(-14deg); animation:sway 7s ease-in-out infinite; }
  .beam:nth-child(2){ left:38%; transform:rotate(6deg); animation:sway 9s ease-in-out infinite reverse; }
  .beam:nth-child(3){ left:68%; transform:rotate(-4deg); animation:sway 8s ease-in-out infinite; }
  .beam:nth-child(4){ left:88%; transform:rotate(12deg); animation:sway 6.5s ease-in-out infinite reverse; }
  @keyframes sway{ 0%,100%{ transform:rotate(var(--r,0deg)); } 50%{ transform:rotate(calc(var(--r,0deg) + 5deg)); } }

  .app{ position:relative; z-index:1; max-width:720px; margin:0 auto; min-height:100vh;
    display:flex; flex-direction:column; padding:32px 20px 48px; }
  .screen{ display:none; flex:1; flex-direction:column; }
  .screen.active{ display:flex; }

  .eyebrow{ text-align:center; letter-spacing:0.3em; font-size:12px; color:var(--gold);
    font-weight:700; margin-bottom:10px; }
  .title-main{ font-family:'Black Han Sans','Noto Sans KR',sans-serif; font-size:clamp(32px, 8vw, 52px);
    text-align:center; line-height:1.2;
    background:linear-gradient(180deg, #fff 10%, var(--gold) 100%);
    -webkit-background-clip:text; background-clip:text; color:transparent;
    text-shadow:0 0 40px rgba(255,201,60,0.35); margin-bottom:6px; }
  .title-sub{ text-align:center; color:var(--ink-dim); font-size:14px; margin-bottom:36px; }

  .butterfly{ text-align:center; font-size:64px; margin:12px 0 28px; animation:float 3.5s ease-in-out infinite; }
  @keyframes float{ 0%,100%{ transform:translateY(0);} 50%{ transform:translateY(-12px);} }

  .card{ background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.12);
    border-radius:18px; padding:24px 22px; backdrop-filter:blur(6px); margin-bottom:20px; }

  .speaker{ font-size:13px; font-weight:700; letter-spacing:0.05em; margin-bottom:8px; }
  .speaker.hero{ color:var(--teal); }
  .speaker.villain{ color:var(--coral); }
  .story-text{ font-size:16px; line-height:1.75; color:var(--ink); white-space:pre-line; }

  .btn{ display:block; width:100%; text-align:left; font-family:inherit; font-size:15px;
    color:var(--ink); background:rgba(255,255,255,0.07); border:1px solid rgba(255,255,255,0.16);
    border-radius:12px; padding:14px 18px; margin-top:10px; cursor:pointer; transition:.15s; }
  .btn:hover{ background:rgba(255,201,60,0.14); border-color:var(--gold); transform:translateX(2px); }
  .btn.correct{ background:rgba(78,205,196,0.22); border-color:var(--teal); }
  .btn.wrong{ background:rgba(255,77,109,0.22); border-color:var(--wrong); }
  .btn[disabled]{ cursor:default; opacity:.7; }

  .btn-primary{ display:block; width:100%; text-align:center; font-family:inherit; font-size:16px;
    font-weight:700; color:#1a1440; background:linear-gradient(180deg,#fff, var(--gold));
    border:none; border-radius:14px; padding:16px 18px; margin-top:18px; cursor:pointer;
    box-shadow:0 8px 24px rgba(255,201,60,0.25); transition:.15s; }
  .btn-primary:hover{ transform:translateY(-2px); box-shadow:0 12px 28px rgba(255,201,60,0.35); }

  .progress{ display:flex; justify-content:space-between; align-items:center; margin-bottom:14px;
    font-size:13px; color:var(--ink-dim); }
  .progress-bar{ flex:1; height:6px; margin:0 12px; background:rgba(255,255,255,0.12);
    border-radius:6px; overflow:hidden; }
  .progress-fill{ height:100%; background:linear-gradient(90deg, var(--teal), var(--gold)); width:0%;
    transition:width .3s; }

  .feedback{ margin-top:14px; font-size:14px; text-align:center; min-height:20px; font-weight:700; }
  .feedback.correct{ color:var(--teal); }
  .feedback.wrong{ color:var(--wrong); }

  .end-badge{ text-align:center; font-size:13px; letter-spacing:0.3em; font-weight:700; margin-bottom:8px; }
  .end-badge.good{ color:var(--good); }
  .end-badge.soso{ color:var(--soso); }
  .end-badge.bad{ color:var(--bad); }
  .end-title{ font-family:'Black Han Sans','Noto Sans KR',sans-serif; font-size:clamp(28px,7vw,40px);
    text-align:center; margin-bottom:18px; }
  .end-title.good{ color:var(--good); }
  .end-title.soso{ color:var(--soso); }
  .end-title.bad{ color:var(--bad); }
  .score-line{ text-align:center; color:var(--ink-dim); font-size:14px; margin-top:12px; }

  .spacer{ flex:1; }
</style>
</head>
<body>

<div class="beams"><div class="beam"></div><div class="beam"></div><div class="beam"></div><div class="beam"></div></div>

<div class="app">

  <!-- 타이틀 화면 -->
  <div class="screen active" id="screen-title">
    <div class="spacer"></div>
    <div class="eyebrow">TEXT ADVENTURE SAMPLE</div>
    <div class="title-main">한국사 답사 어드벤처</div>
    <div class="title-sub">선택에 따라 이야기가 달라집니다</div>
    <div class="butterfly">🦋</div>
    <button class="btn-primary" onclick="goTo('screen-story1')">모험 시작하기</button>
    <div class="spacer"></div>
  </div>

  <!-- Scene 1: 모험의 시작 -->
  <div class="screen" id="screen-story1">
    <div class="spacer"></div>
    <div class="card">
      <div class="speaker hero">연우</div>
      <div class="story-text">삼촌을 만나러 가던 길, 연우는 우연히 낡은 상자 하나를 발견했다.
"이거 뭐지?"

상자를 열자 나비 한 마리가 날아올랐다. 나비는 사람의 말로 "고맙다"고 인사했다.
"저는 나비 왕국의 왕자입니다. 사악한 주술에 갇혀 있었어요."

나비는 그렇게 인사를 남기고 사라졌다. 연우가 다시 길을 걷기 시작하는데,
갑자기 눈앞에 사악한 악령이 나타났다!</div>
    </div>
    <button class="btn-primary" onclick="goTo('screen-dialogue1')">다음</button>
    <div class="spacer"></div>
  </div>

  <!-- Scene 2: 악령과의 대화 (선택지) -->
  <div class="screen" id="screen-dialogue1">
    <div class="spacer"></div>
    <div class="card">
      <div class="speaker villain">사악한 악령</div>
      <div class="story-text">"네 녀석이 나비 왕국의 왕자를 풀어줬겠다."</div>
    </div>
    <button class="btn" onclick="chooseLie()">"저는 모르는 일입니다."</button>
    <button class="btn" onclick="chooseHonest()">"왕자를 가둔 건 잘못한 일 아닌가요?"</button>
    <div class="spacer"></div>
  </div>

  <!-- 거짓말 선택 결과 -->
  <div class="screen" id="screen-lieResult">
    <div class="spacer"></div>
    <div class="card">
      <div class="speaker villain">사악한 악령</div>
      <div class="story-text">"모르는 일이라고 거짓말을 하다니. 너의 손에 있는 나비의 가루가 그 증거인데... 죽어라, 얏!"</div>
    </div>
    <button class="btn-primary" onclick="showEnding('bad')">결과 확인하기</button>
    <div class="spacer"></div>
  </div>

  <!-- 정직 선택 결과 -> 퀴즈 시작 -->
  <div class="screen" id="screen-honestResult">
    <div class="spacer"></div>
    <div class="card">
      <div class="speaker villain">사악한 악령</div>
      <div class="story-text">"그렇다면 네가 얼마나 도덕적인 인간인지 시험해 보겠다. 내가 내는 문제를 맞춰보아라."</div>
    </div>
    <button class="btn-primary" onclick="startQuiz()">도전하기</button>
    <div class="spacer"></div>
  </div>

  <!-- 퀴즈 화면 -->
  <div class="screen" id="screen-quiz">
    <div class="progress">
      <span id="quiz-count">1 / 10</span>
      <div class="progress-bar"><div class="progress-fill" id="quiz-progress-fill"></div></div>
      <span id="quiz-score">정답 0</span>
    </div>
    <div class="card">
      <div class="speaker villain">도덕 문제</div>
      <div class="story-text" id="quiz-question"></div>
    </div>
    <div id="quiz-options"></div>
    <div class="feedback" id="quiz-feedback"></div>
  </div>

  <!-- 엔딩 화면 -->
  <div class="screen" id="screen-ending">
    <div class="spacer"></div>
    <div class="end-badge" id="end-badge">ENDING</div>
    <div class="end-title" id="end-title"></div>
    <div class="card">
      <div class="story-text" id="end-text"></div>
    </div>
    <div class="score-line" id="end-score"></div>
    <button class="btn-primary" onclick="restart()">다시 하기</button>
    <div class="spacer"></div>
  </div>

</div>

<script>
  const HERO_NAME = "연우";

  const QUIZ = [
    { q:"친구가 실수로 물건을 망가뜨렸을 때 가장 바람직한 행동은?", options:["모른 척한다","솔직히 말하고 사과한다","다른 친구 탓을 한다","화를 낸다"], answer:1 },
    { q:"길에서 주인 없는 지갑을 주웠을 때 올바른 행동은?", options:["그냥 가진다","친구에게 자랑한다","분실물 센터나 경찰서에 가져다준다","버린다"], answer:2 },
    { q:"친구가 어려운 일을 겪고 있을 때 가장 좋은 태도는?", options:["모른 척 지나간다","놀린다","공감하고 도와준다","소문을 낸다"], answer:2 },
    { q:"내가 잘못을 했을 때 가장 용기 있는 행동은?", options:["거짓말로 둘러댄다","솔직하게 인정하고 사과한다","남 탓을 한다","도망간다"], answer:1 },
    { q:"약속을 지키는 것이 중요한 이유는 무엇일까?", options:["그냥 습관이라서","남에게 보여주기 위해서","신뢰를 쌓기 위해서","상관없다"], answer:2 },
    { q:"친구와 다투었을 때 가장 바람직한 해결 방법은?", options:["무시한다","대화로 오해를 풀어본다","더 크게 싸운다","다른 친구들에게 험담한다"], answer:1 },
    { q:"공공장소에서 쓰레기를 버리려 할 때 올바른 행동은?", options:["아무데나 버린다","쓰레기통을 찾아 버린다","남에게 버리라고 시킨다","숨긴다"], answer:1 },
    { q:"친구가 나쁜 행동을 같이 하자고 할 때 가장 바람직한 태도는?", options:["무조건 따라간다","정중히 거절하고 이유를 설명한다","같이 하고 나중에 후회한다","모른 척 한다"], answer:1 },
    { q:"다른 사람의 도움을 받았을 때 마땅히 해야 할 행동은?", options:["당연하다고 여긴다","고맙다는 인사를 한다","더 요구한다","잊어버린다"], answer:1 },
    { q:"위기에 처한 사람을 봤을 때 가장 바람직한 행동은?", options:["그냥 지나간다","동영상만 찍는다","주변에 도움을 요청하거나 직접 돕는다","비웃는다"], answer:2 }
  ];

  const ENDINGS = {
    good:{ label:"GOOD ENDING", cls:"good",
      text: HERO_NAME + "는 사악한 악령을 물리치고 나비 왕국에 초대되어 금은보화를 선물로 받았습니다." },
    soso:{ label:"SOSO ENDING", cls:"soso",
      text: HERO_NAME + "는 사악한 악령의 공격을 받고 기절했다가 반나절 만에 집에 겨우 돌아왔습니다." },
    bad:{ label:"BAD ENDING", cls:"bad",
      text: HERO_NAME + "는 사악한 악령의 공격을 받고 부산으로 버려져 며칠간 떠돌다 겨우 집에 돌아왔습니다." }
  };

  let quizIndex = 0;
  let correctCount = 0;
  let answered = false;

  function goTo(id){
    document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
    document.getElementById(id).classList.add('active');
    window.scrollTo(0,0);
  }

  function chooseLie(){ goTo('screen-lieResult'); }
  function chooseHonest(){ goTo('screen-honestResult'); }

  function startQuiz(){
    quizIndex = 0;
    correctCount = 0;
    goTo('screen-quiz');
    renderQuestion();
  }

  function renderQuestion(){
    answered = false;
    const item = QUIZ[quizIndex];
    document.getElementById('quiz-count').textContent = (quizIndex+1) + " / " + QUIZ.length;
    document.getElementById('quiz-score').textContent = "정답 " + correctCount;
    document.getElementById('quiz-progress-fill').style.width = ((quizIndex+1)/QUIZ.length*100) + "%";
    document.getElementById('quiz-question').textContent = item.q;
    document.getElementById('quiz-feedback').textContent = "";
    document.getElementById('quiz-feedback').className = "feedback";

    const optionsEl = document.getElementById('quiz-options');
    optionsEl.innerHTML = "";
    item.options.forEach((opt, i) => {
      const btn = document.createElement('button');
      btn.className = "btn";
      btn.textContent = opt;
      btn.onclick = () => selectAnswer(i, btn);
      optionsEl.appendChild(btn);
    });
  }

  function selectAnswer(i, btn){
    if(answered) return;
    answered = true;
    const item = QUIZ[quizIndex];
    const buttons = document.querySelectorAll('#quiz-options .btn');
    buttons.forEach(b => b.disabled = true);

    const feedback = document.getElementById('quiz-feedback');
    if(i === item.answer){
      correctCount++;
      btn.classList.add('correct');
      feedback.textContent = "정답입니다!";
      feedback.className = "feedback correct";
    } else {
      btn.classList.add('wrong');
      buttons[item.answer].classList.add('correct');
      feedback.textContent = "아쉬워요, 정답은 따로 있어요.";
      feedback.className = "feedback wrong";
    }

    document.getElementById('quiz-score').textContent = "정답 " + correctCount;

    setTimeout(() => {
      quizIndex++;
      if(quizIndex < QUIZ.length){
        renderQuestion();
      } else {
        document.getElementById('quiz-progress-fill').style.width = "100%";
        finishQuiz();
      }
    }, 1200);
  }

  function finishQuiz(){
    let result;
    if(correctCount >= 8) result = 'good';
    else if(correctCount >= 4) result = 'soso';
    else result = 'bad';
    setTimeout(() => showEnding(result), 500);
  }

  function showEnding(key){
    const e = ENDINGS[key];
    document.getElementById('end-badge').textContent = e.label;
    document.getElementById('end-badge').className = "end-badge " + e.cls;
    document.getElementById('end-title').textContent =
      key === 'good' ? '해피 엔딩' : key === 'soso' ? '아쉬운 엔딩' : '나쁜 엔딩';
    document.getElementById('end-title').className = "end-title " + e.cls;
    document.getElementById('end-text').textContent = e.text;
    document.getElementById('end-score').textContent =
      key === 'bad' && quizIndex === 0 ? "" : ("맞춘 문제: " + correctCount + " / " + QUIZ.length);
    goTo('screen-ending');
  }

  function restart(){
    quizIndex = 0;
    correctCount = 0;
    goTo('screen-title');
  }
</script>

</body>
</html>

결과와 배운 점

배운 점과 나만의 꿀팁을 알려주세요.

이렇게는 도전히 안 되겠다는 생각이 들어서 학생들이 뭘 입력해야 하는지를 정리된 웹 페이지를 만들고, 이걸로 개발을 해야지 안 되겠다는 생각이 들었습니다.

과정 중에 어떤 시행착오를 겪었나요?

일단 학생들이 마크다운 파일을 작성하기 너무 힘들어 하더라구요. AI한테 요구할 것과 내가 작업할 것 두 가지가 있으니 나도 헛갈리고 학생들도 헛갈리고 아주 난장판이었습니다.

진행 방법도 내가 설명하기 보다는 메뉴얼을 만들어야 겠다는 생각이 들었습니다. 연령이 낮다보니 챙겨야할 것들이 너무 많습니다.

앞으로의 계획이 있다면 들려주세요.

웹에서 작성하고 마크다운 파일을 다운로드 받거나 기록하는 형태의 생성기(?)를 만들어봐야 겠다고 생각했고 도전해 보겠습니다.

학생들이 어떻게 진행을 해야 할지에 대한 메뉴얼을 만들어 놓고 보면서 할 수 있도록 가이드를 만들어서 제공해 보려고 합니다.

2
6개의 답글
밀어주고 끌어주는

온·오프라인 AI 스터디

AI로 어디까지 할 수 있는지
직접 확인하실 분만 신청하세요.