우리는 Game-Dev 로직을 Telegram에 포팅했습니다: React와 RxJS로 촉각적인 LifeOS 구축

발행: (2026년 1월 30일 오후 11:17 GMT+9)
11 min read
원문: Dev.to

Source: Dev.to

(번역할 텍스트를 제공해 주시면 한국어로 번역해 드리겠습니다.)

소개

우리는 UltyMyLife를 만들고 싶었습니다 – 당신이 시간의 90 %를 보내는 바로 그곳, Telegram에 살아있는 통합 LifeOS입니다. VPN도, Notion도, 마찰도 없습니다.

하지만 우리는 장벽에 부딪혔습니다: 일반적인 Telegram Mini Apps는 종종 2010년대 웹사이트처럼 느껴집니다. 우리는 “Apple 분위기” — 프리미엄하고 어두우며 깊은 그림자와, 무엇보다도 촉각적인 느낌을 원했습니다.

코드 뒤의 시너지

인물역할비고
Dmitriy Spirihin (Me)시스템 아키텍처 및 풀스택Unity 개발에서 왔습니다. UniRx와 게임 루프에 익숙해지면 표준 웹 개발이 “플라스틱 같은” 느낌이 듭니다. 제 목표는 게임‑개발 아키텍처를 React 스택으로 포팅하는 것이었습니다.
Demian Avolstyny제품 비전, 디자인 & 바이브 코딩‘바이브’를 지키는 수호자. 미학, 촉각 피드백, 그리고 앱이 자신을 확장한 듯한 느낌을 주는 매끄러운 인터랙션 로직에 집중합니다.

이 글에서는 다음을 살펴봅니다:

  • 게임 개발 사고방식이 RxJS를 사용한 복잡한 상태 시스템 구축에 어떻게 도움이 되었는지.
  • Framer Motion이 촉각 UI의 비밀인 이유.
  • AI를 활용해 지루한 로그를 실행 가능한, “잔인하게 솔직한” 인사이트로 전환한 방법.

문제

시장은 트래커들로 포화 상태입니다:

  • 천 개의 습관 트래커.
  • 또 다른 천 개의 헬스 로그.
  • 백 개의 명상 앱.

문제는? 이들은 파편화되어 있습니다. *“잠을 못 잤다”*는 데이터가 다른 앱의 운동 계획에 전혀 영향을 주지 못합니다. 명상은 진공 상태에 존재하며, 스트레스 수준과 연결되지 않습니다.

우리는 LifeOS — 내가 시간의 90 %를 보내는 곳, Telegram에 존재하는 통합 자기 관리 시스템을 만들고 싶었습니다. VPN도, Notion도, 마찰도 없습니다.

하지만 문제가 있었습니다: 표준 Telegram WebApps는 종종 2010년대 웹사이트처럼 보입니다. 저는 “Apple 분위기”— 프리미엄, 다크, 깊은 그림자와 무엇보다 촉각적인 느낌을 원했습니다. 그래서 UltyMyLife가 탄생했습니다.

진행‑계산 로직 (HabitCalendar.js)

// Progress calculation logic in HabitCalendar.js
const dayKey = formatDateKey(new Date(cellYear, cellMonth, day));
let percentNum = 0;

if (Object.keys(AppData.habitsByDate).includes(dayKey)) {
  const dayHabits = Array.from(Object.values(AppData.habitsByDate[dayKey]));
  // Calculate percentage of completed (v > 0) habits
  percentNum = dayHabits.length > 0
    ? Math.round((dayHabits.filter((v) => v > 0).length / dayHabits.length) * 100)
    : 0;
}

// Render cell with dynamic color

  {day}

ScrollPicker: 시스템 입력 극복

표준 <select> 또는 <input type="date">는 웹앱에서 어색하게 느껴집니다.

  • iOS에서는 시스템 휠이 팝업됩니다.
  • Android에서는 모달 형태로 표시됩니다.

두 경우 모두 즉시 몰입감을 깨뜨립니다. 우리는 자체 ScrollPicker를 작성하여 다음을 처리합니다:

  • Scroll Snapping – 아이템이 중앙에 “붙어” 있습니다.
  • Initial Mount – 부자연스러운 애니메이션 없이 현재 값으로 즉시 점프합니다.
// Instant scroll trick on mount
useEffect(() => {
  if (scrollRef.current) {
    const selectedIndex = items.findIndex((item) => item === value);
    if (selectedIndex !== -1) {
      // Jump instantly
      scrollRef.current.scrollTop = selectedIndex * ITEM_HEIGHT;
    }
    // Enable smooth scrolling for the user only after the jump
    requestAnimationFrame(() => {
      setIsLoaded(true);
    });
  }
}, []);

Persistent Storage – IndexedDB over localStorage

Many Mini‑App developers fall for the localStorage trap. In Telegram, your app runs in a system WebView; if the device runs low on memory or clears its cache, localStorage can be wiped.

For UltyMyLife we chose IndexedDB (via the idb wrapper). This allows us to store megabytes of data:

  • Years of workout history.
  • Custom icons.
  • Sleep logs.

We use class‑transformer to serialize our business‑logic classes into flat JSON and back again, ensuring we get objects with working methods, not just raw data.

Cloud Storage Limits → Compression & Custom Backend

Telegram의 CloudStorage는 엄격한 제한이 있으며, 1년치 학습 로그는 용량이 크게 늘어날 수 있습니다. 우리는 제어가 필요했습니다.

The Solution

  • Pako Compression (zlib) + Base64 인코딩.
  • 100 KB JSON이 70‑80 % 정도 압축되어, 2G 연결에서도 즉시 업로드할 수 있는 작은 문자열이 됩니다.

Defensive Programming

우리의 복구 로직은 다목적 스위스‑아미‑나이프와 같습니다:

  1. 데이터가 압축된 것인지 “구버전” JSON인지 감지합니다.
  2. Base64 아티팩트를 정리합니다 (Safari 버그 덕분에 필요합니다).
  3. 스키마 마이그레이션을 처리합니다.

Backend Design

백엔드는 최소화된 형태로 유지됩니다: PostgreSQL은 순수히 “Blind Keeper” 역할만 수행합니다. 서버는 사용자의 습관을 파싱하지 않고, ID와 연결된 바이너리 블롭(BYTEA)을 저장합니다. 이는 확장을 손쉽게 만들어 줍니다.

반응형 상태 관리 – “신경계” 같은 RxJS

Unity 개발자로서 저는 UniRx에 익숙합니다. 프로젝트가 커지면서 단순한 “스토어”(Zustand)가 아니라 신경계가 필요했습니다.

RxJS를 사용하면 사용자 상호작용을 이벤트 스트림으로 다룰 수 있어 UI가 즉시 반응하는 “게임 루프” 효과를 만들 수 있습니다.

  • BehaviorSubjects – 오래 지속되는 상태(테마, 현재 페이지)용.
  • Subjects – 일회성 이벤트(팝업, 햅틱)용.
// Our Event Bus (HabitsBus.js)
export const theme$ = new BehaviorSubject('dark');
export const setPage$ = new BehaviorSubject('LoadPanel');
export const showPopUpPanel$ = new BehaviorSubject({
  show: false,
  header: '',
  isPositive: true,
});

export const setPage = (page) => {
  setPage$.next(page);
  // Auto‑switch bottom menu context
  if (page.startsWith('Habit')) bottomBtnPanel$.next('BtnsHabits');
  else if (page.startsWith('Training')) bottomBtnPanel$.next('BtnsTraining');
};

AI‑Powered Insights

우리는 앱에게 think하도록 가르쳤습니다. 단순히 “You slept 6 hours.” 라고 말하지 않죠. 대신 이렇게 말합니다:

“Your squat dropped 15 % after nights with UltyMyLife is proof that even within the constraints of a WebView, you can deliver a next‑level user experience—no VPNs, no Notion, no friction.”

글래스모피즘 성능

“브라우저에서 진정한 글래스모피즘은 성능 악몽이다. Demian이 디자인을 가져왔을 때, 내 안의 Unity 개발자는 **‘드로우 콜!’**이라고 외쳤지만, 내 안의 풀스택 개발자는 바로 작업에 착수했다.”

해결책: 이중 레이어 방어

  • 우리는 backdrop-filter: blur(15px)를 사용했지만, 일부 WebView에서는 신뢰성이 떨어지기 때문에 opacity: 0.85 폴백을 추가했습니다.

  • 앱을 60 FPS로 유지하기 위해, 유리 효과는 가장 중요한 UI 요소에만 제한했습니다:

    1. 네비게이션 바
    2. 모달
    3. 습관 카드
  • 내부 그라디언트와 box‑shadow를 추가해 “LED 엣지 라이트”를 시뮬레이션함으로써 패널이 인터페이스 위에 떠 있는 듯한 인상을 주었습니다.

Source:

XP 시스템

대부분의 앱은 앱을 열기만 해도 XP를 제공합니다. UltyMyLife에서는 XP를 직접 획득해야 합니다.

TotalXP = (Training × 50) + (Mental × 30) + (Sleep × 20) + (Habits × 10)
  • 레벨(레벨업)은 RPG처럼 지수적으로 계산됩니다: 각 레벨은 이전 레벨보다 15 % 더 많은 XP가 필요합니다.
  • 만화 캐릭터 대신, 우리는 계급과 색상의 시각적 계층 구조를 사용합니다.
  • 당신은 게임을 하는 것이 아니라 스스로를 레벨업하고 있는 것입니다.

기술 스택

레이어기술
UltyMyLife단순한 텔레그램 봇이 아니라 LifeOS이며, 당신이 있는 곳에 존재합니다. 설치도, 권한도, 마찰도 없습니다. 순수한 훈련을 60 FPS로 제공합니다.

특정 컴포넌트(예: 스와이프 가능한 습관 카드 또는 커스텀 피커)의 코드가 궁금하면 댓글로 알려 주세요!

👉 확인해 보기

t.me/UltyMyLife_bot/umlminiapp

Back to Blog

관련 글

더 보기 »