AppOrbit
Back to DevLog

합법적 월급 루팡을 위한 도구, Project Lupin 개발기 (feat. Next.js 16)

Next.jsTailwind CSSPWASide Project

1. 들어가며: 왜 이 서비스를 만들었나?

"똥 싸는데 돈이 나온다?"

직장인이라면 한 번쯤 화장실에 앉아 이런 엉뚱한 상상을 해보셨을 겁니다. '내가 지금 이 변기 위에 앉아있는 시간 동안 회사는 나에게 얼마를 지불하고 있을까?'라는 아주 원초적이고 자본주의적인 호기심 말이죠.

Project Lupin은 바로 그 호기심을 기술로 풀어낸 프로젝트입니다. 단순한 계산기를 넘어, 삭막한 회사 생활 속에서 직장인들에게 소소한 해방감과 '금융 치료'의 시각적 즐거움을 주고 싶었습니다. 또한 10년 차 개발자로서, 단순히 돌아가는 앱을 만드는 것을 넘어 Next.js 16React 19, Tailwind CSS 4 같은 최신 기술 스택을 실전(사이드 프로젝트)에 도입해보고, PWA를 통해 네이티브 앱처럼 몰입감 있는 사용자 경험을 웹표준으로 구현해보고자 하는 기술적 욕심도 컸습니다.

2. 핵심 기능과 UX

Project Lupin은 사용자가 '루팡 모드'에 진입하는 순간부터 경험이 시작됩니다.

  • 실시간 수입 계산 (Real-time Calculator): 사용자가 연봉을 입력하고 타이머를 시작하면, 근무 시간 대비 초당 급여가 실시간으로 카운팅 됩니다. 숫자가 맹렬하게 올라가는 모습을 보며 시각적 쾌감을 느낄 수 있도록 디자인했습니다.
  • 사이버펑크 테마 (Cyberpunk UI): 몰래 하는 '해킹' 같은 느낌을 주기 위해 블랙&네온 그린 컬러의 터미널/대시보드 스타일을 차용했습니다. Framer Motion을 활용해 숫자와 UI 요소들이 살아있는 듯한 생동감을 부여했습니다.
  • 데이터 프라이버시 (Privacy First): 연봉이라는 민감한 정보는 서버로 전송되지 않고, 오직 사용자 기기에만 저장되도록 설계하여 심리적 거부감을 없앴습니다.

3. 기술 스택 선정 이유 (Tech Stack)

이번 프로젝트는 최신 기술의 안정성을 테스트하고, 빠르고 가벼운 앱을 만드는 데 주안점을 두었습니다.

  • Next.js 16 (App Router): React 19의 최신 기능들을 가장 잘 지원하는 프레임워크입니다. 특히 서버 컴포넌트와 클라이언트 컴포넌트의 경계를 명확히 하여, 초기 로딩 속도(FCP)를 극대화하고 SEO 성능을 챙기기 위해 선택했습니다.
  • Tailwind CSS 4: 기존 v3 대비 설정 파일이 획기적으로 줄어들고, CSS 변수(Variables)를 활용한 동적 스타일링이 훨씬 강력해졌습니다. 사이버펑크 스타일의 복잡한 그라디언트와 애니메이션을 구현하는 데 있어 'Zero-runtime'에 가까운 가벼움을 유지할 수 있었습니다.
  • Zustand: Redux나 Context API보다 보일러플레이트가 훨씬 적고 직관적입니다. 특히 persist 미들웨어를 통해 로컬 스토리지(Local Storage)와의 연동을 코드 몇 줄로 끝낼 수 있어, 서버 없이 클라이언트에서만 데이터를 관리해야 하는 이 프로젝트의 요구사항에 딱 맞았습니다.
  • PWA (next-pwa): 이 서비스의 주 사용 공간은 '화장실'입니다. 네트워크가 불안정할 수 있고, 매번 브라우저 주소창을 치고 들어오는 번거로움을 없애야 했습니다. 홈 화면에 추가하여 네이티브 앱처럼 전체 화면으로 실행되도록 구현했습니다.

4. 개발 과정의 챌린지 (Deep Dive)

단순해 보이는 기능이지만, 디테일을 잡는 과정에서 몇 가지 기술적 난관이 있었습니다.

4.1. 모바일 뷰포트 이슈 (The 100vh Problem)

모바일 브라우저(특히 Safari/Chrome iOS)에서 주소창이나 하단 툴바 때문에 100vh가 의도한 대로 동작하지 않는 고질적인 문제가 있었습니다. 스크롤이 생기거나 중요한 하단 버튼이 가려지는 현상이 발생했죠.

초기에는 JavaScript로 window.innerHeight를 계산해 CSS 변수로 주입하는 방식을 고려했으나, Tailwind CSS 4와 최신 CSS 스펙인 dvh (Dynamic Viewport Height) 단위를 적극 활용하는 방향으로 리팩토링했습니다.

/* Before: 스크롤 발생 가능성 있음 */
.container { min-height: 100vh; }

/* After: 모바일 UI에 완벽 대응 */
.container { min-height: 100dvh; }

이를 통해 브라우저 UI가 동적으로 변하더라도 항상 꽉 찬 화면을 유지하여, 앱 같은 몰입감을 줄 수 있었습니다.

4.2. Hydration Mismatch 해결 (Zustand Persist)

Zustand의 persist 기능을 사용하여 사용자의 연봉 정보를 로컬 스토리지에 저장했는데, Next.js의 SSR(Server Side Rendering) 과정에서 문제가 발생했습니다.

서버에서 렌더링 된 초기 HTML(데이터 없음)과, 클라이언트에서 로컬 스토리지 값을 불러온 후의 HTML(데이터 있음)이 달라 'Hydration Mismatch' 에러가 발생한 것입니다.

이를 해결하기 위해 커스텀 훅 등의 복잡한 방법 대신, useStateuseEffect를 사용해 '마운트 여부'를 체크하는 직관적인 방법을 택했습니다.

// src/store/useUserStore.ts (예시)
const [isHydrated, setIsHydrated] = useState(false);

useEffect(() => {
  setIsHydrated(true);
}, []);

if (!isHydrated) {
  // 로딩 상태 혹은 빈 화면을 렌더링하여 매칭 오류 방지
  return <LoadingSkeleton />; 
}

이 방식은 깜빡임(Flash of Unstyled Content)을 최소화하면서도, 데이터 무결성을 보장하고 에러 콘솔을 깨끗하게 유지해주었습니다.

5. 마치며

Project Lupin은 "기술로 웃음을 줄 수 없을까?"라는 작은 질문에서 시작했지만, 개발 과정에서 Next.js 16과 PWA의 잠재력을 깊게 파고들 수 있었던 값진 경험이었습니다.

기술적으로는 최신 스택의 편리함을 누리면서도, 사용자 입장에서는 '내 숨겨진 돈을 찾아주는' 유쾌한 경험을 제공했다는 점에서 만족도가 높습니다. 향후에는 주간/월간 루팡 랭킹 시스템을 도입해, 전국의 '월급 루팡러'들이 서로 경쟁(?) 할 수 있는 기능을 추가해볼 생각입니다.

잠시 화장실에 가실 예정인가요? 그렇다면 지금 바로 접속해서 여러분의 소중한 시간을 돈으로 환산해 보세요.

👉 서비스 바로가기