목록으로
ARCANE BREW 개발기

ARCANE BREW 개발기 (6) - 지팡이 공방과 업적 시스템

2026년 02월 19일11 min read
#SVG#Vue#LocalStorage#게임화#ARCANE BREW

목차 보기
시리즈 목차 보기6 / 7

조제 너머의 경험

가마솥 시스템이 ARCANE BREW의 "심장"이라면, 지팡이 공방과 업적 시스템은 "근육"이다. 조제만 반복하면 금방 질린다. 지팡이를 커스터마이징하고, 업적을 하나씩 해금하고, 레벨이 오르는 경험이 플레이어를 붙잡아두는 장치다.

이번 편에서는 SVG 기반 지팡이 렌더링, 108가지 조합의 커스터마이저, 이벤트 기반 업적 추적, 그리고 XP/레벨 시스템을 다룬다.

🪄 SVG 지팡이 미리보기

왜 SVG인가

가마솥은 Canvas로 구현했지만, 지팡이에는 SVG를 선택했다. 이유는 명확했다. 지팡이는 유체 시뮬레이션처럼 매 프레임 전체를 다시 그릴 필요가 없고, 몸체/심/장식 같은 개별 부위를 독립적으로 조작해야 한다. SVG의 선언적 구조가 Vue의 반응형 시스템과 자연스럽게 맞물린다.

WandPreview.vue 구조

WandPreview.vue는 나무(wood), 심(core), 장식(decoration) 세 가지 옵션을 받아 지팡이를 실시간으로 렌더링한다. SVG 내부 구조는 이렇다.

  • 몸체<rect>로 지팡이의 기본 형태. 나무 종류에 따라 색상과 질감이 달라진다.
  • 테이퍼<polygon>으로 끝부분이 가늘어지는 형태. 지팡이답게 보이려면 이 디테일이 필수다.
  • 심 라인<line>에 glow filter를 적용해서 지팡이 내부를 관통하는 마법 심을 표현한다. 불사조 깃털 심은 붉은 발광, 용 심줄은 녹색 발광.
  • 손잡이 — 하단의 그립 영역.

장식 패턴

장식은 5종류다. 룬 문자, 덩굴, 수정, 용린, 월석 밴드. 각각 SVG 엘리먼트로 구현된다. 가장 마음에 들었던 건 룬 문자 장식이다.

vue
<!-- 룬 문자 장식 -->
<template v-if="deco.pattern === 'runes'">
  <text
    v-for="(char, i) in 'ᚠᚢᚦᚨ'"
    :key="i"
    :x="25 + i * 11" y="44"
    :fill="core.color" font-size="8" opacity="0.5"
  >{{ char }}</text>
</template>

유니코드 룬 문자(ᚠᚢᚦᚨ)를 심 색상으로 렌더링한다. 별도의 이미지 에셋 없이 텍스트 엘리먼트만으로 "마법 지팡이에 새겨진 고대 문자"를 표현할 수 있었다. SVG의 강점이 빛나는 부분이다.

스탯 계산

지팡이는 단순히 보기 좋은 것만이 아니라 스탯을 가진다. 위력, 정밀도, 속도, 내구도, 친화도 — 5가지 수치가 나무, 심, 유연성, 길이에 따라 결정된다.

ts
const stats = computed<WandStats>(() => ({
  power: Math.min(100, 40 + w.powerBonus + Math.floor(config.length * 1.5)),
  precision: Math.min(100, 35 + c.precisionBonus + (config.flexibility === 'rigid' ? 10 : 0)),
  speed: Math.min(100, 50 + (15 - config.length) * 3 + (config.flexibility === 'supple' ? 15 : 0)),
  // ...
}))

설계 의도는 트레이드오프다. 긴 지팡이는 위력이 높지만 속도가 떨어진다. 단단한(rigid) 유연성은 정밀도에 유리하지만 유연한(supple) 쪽이 속도가 빠르다. 나무와 심에도 각각 보너스가 있어서, 108가지 조합마다 스탯 분포가 다르다. 단 하나의 "최강 조합"이 없도록 밸런싱했다.

🎨 커스터마이저 UI 패턴

커스터마이저의 흐름은 옵션 선택 → 미리보기 → 저장이다. 나무 6종, 심 6종, 장식 3종을 조합하면 총 108가지. 라디오 버튼 그룹으로 옵션을 선택하면, 오른쪽 패널에서 SVG 지팡이와 스탯 레이더 차트가 실시간으로 갱신된다.

Vue의 computed 반응성 덕분에 옵션 변경 → SVG 업데이트 → 스탯 재계산이 별도의 이벤트 핸들링 없이 자동으로 이루어진다. 이 부분은 프레임워크의 강점을 그대로 활용한 케이스다.

🏆 업적 시스템

조건 타입과 이벤트 추적

업적 시스템은 7가지 조건 타입을 지원한다.

  • brew_count — 총 조제 횟수
  • collect_potions — 물약 수집 수
  • discover_recipes — 레시피 발견 수
  • brew_quality — 최고 품질 달성
  • brew_recipe — 특정 레시피 조제
  • create_wand — 지팡이 제작
  • reach_level — 레벨 도달

매 게임 이벤트(조제 완료, 레시피 발견, 지팡이 저장 등)마다 checkAchievements를 호출해서 해금 여부를 판별한다.

ts
function checkAchievements() {
  for (const ach of achievements) {
    if (unlockedIds.value.has(ach.id)) continue
    let met = false
    switch (cond.type) {
      case 'brew_count': met = potionCount >= cond.count; break
      case 'discover_recipes': met = discoveredCount >= cond.count; break
      case 'brew_quality': met = maxQuality >= cond.minQuality; break
      // ...
    }
    if (met) unlock(ach.id, ach.xpReward)
  }
}

이미 해금된 업적은 Set으로 관리해서 빠르게 건너뛴다. unlock 함수는 XP를 지급하고 토스트 알림을 띄운다. 19개 업적 전부가 이 하나의 함수로 처리된다. 조건 타입을 switch로 분기하는 구조라 새 업적을 추가할 때도 케이스 하나만 넣으면 된다.

19개 업적 설계

업적은 난이도별로 분포시켰다. 첫 조제 성공(초반), 10가지 레시피 발견(중반), 금단의 물약 조제 성공(후반)처럼 플레이 전체를 관통하는 마일스톤이다. 각 업적에는 XP 보상이 붙어있어서 레벨 시스템과 직결된다.

📈 XP와 레벨 시스템

10단계 레벨 시스템을 만들었다. 경험치 곡선은 직접 설계한 thresholds 배열이다.

ts
const thresholds = [0, 100, 350, 850, 1650, 2850, 4550, 6850, 9850, 13650]

등차도 등비도 아닌, 대략 "이전 간격의 1.4~1.6배"로 증가하는 곡선이다. 초반에는 빠르게 레벨업해서 성취감을 주고, 후반으로 갈수록 간격이 넓어져서 도전 의식을 자극한다. MMORPG에서 흔히 쓰는 패턴을 참고했다.

각 레벨에는 타이틀이 붙는다. 1레벨 "견습 연금술사"에서 시작해 10레벨 "대마법사"까지. 타이틀이 바뀔 때마다 프로필 페이지의 호칭이 갱신되는데, 이 작은 요소가 생각보다 강한 동기부여가 된다.

💾 LocalStorage 영속성 전략

서버 없는 프로젝트에서 게임 상태를 유지하려면 LocalStorage가 유일한 선택지다. 보유 재료, 해금된 업적, 제작한 물약, 지팡이 설정, XP — 모든 진행 상황을 브라우저에 저장한다.

구현은 단순하다. JSON.stringify로 직렬화해서 저장하고, JSON.parse로 복원한다. 다만 방어 코드가 중요하다.

ts
function loadState<T>(key: string, fallback: T): T {
  try {
    const raw = localStorage.getItem(key)
    if (!raw) return fallback
    return JSON.parse(raw) as T
  } catch {
    return fallback
  }
}

try-catch로 파싱 에러를 방어한다. LocalStorage에 손상된 데이터가 들어있거나, 스키마가 바뀌어서 이전 버전 데이터와 호환되지 않을 때 앱이 크래시하는 것을 방지한다. fallback으로 기본값을 반환하면 마치 처음 시작하는 것처럼 자연스럽게 동작한다.

각 스토어가 초기화 시점에 loadState를 호출하고, 상태 변경 시마다 watch로 자동 저장한다. Pinia store와 LocalStorage가 단방향으로 동기화되는 구조다.

정리

지팡이 공방은 SVG의 선언적 특성과 Vue 반응성의 시너지를 보여주는 기능이다. 108가지 조합이 별도의 이미지 에셋 없이 SVG만으로 렌더링된다. 업적 시스템은 단순한 조건 체크 로직이지만, 19개의 마일스톤이 플레이 전반에 걸쳐 목표를 제공한다. XP 곡선과 레벨 타이틀이 그 위에 성장의 감각을 더한다.

다음 편에서는 마지막 여정 — 폴리싱과 배포를 다룬다. 접근성 점검, 퍼포먼스 최적화, GitHub Pages 배포까지, 프로젝트를 세상에 내놓기 직전의 마무리 과정이다.

검색...

검색...