기존 Node.js 크로스워드 앱을 현대적인 Next.js 스택으로 리버스 엔지니어링하기
요약
기존 Node.js 기반 크로스워드 앱을 Next.js 16, Neon PostgreSQL, Better Auth 등 현대적인 스택으로 리버스 엔지니어링하여 재구축한 과정을 다룹니다. Vercel의 v0를 활용해 UI 프로토타이핑을 가속화하고 핵심 비즈니스 로직을 분리하는 방법론을 제시합니다.
핵심 포인트
- 기존 시스템의 복잡한 구조에서 핵심 비즈니스 로직을 분리하는 리버스 엔지니어링 전략
- Vercel v0를 활용한 React 컴포넌트 및 Tailwind CSS 기반 UI의 빠른 프로토타이핑
- Next.js 16, Drizzle ORM, Neon PostgreSQL을 활용한 현대적 웹 아키텍처 구축
- 그리드 생성, 상태 관리, 세션 유지 등 핵심 시스템의 현대화 과정
Moratuwa 대학교의 IT 학부생으로서, 저는 꽤 많은 프로젝트를 만들어 왔습니다. 최근에 저는 제가 만든 오래된 프로젝트인 전통적인 Node.js Crossword App을 다시 살펴보았고, 이제는 완전히 해체할 때가 되었다는 것을 깨달았습니다.
기존 앱은 작동했지만, 아키텍처(Architecture)가 구식처럼 느껴졌습니다. 저는 이를 현대화하고, 성능을 개선하며, 더 깔끔한 편집 스타일의 UI를 구현하고 싶었습니다. 단순히 리팩토링(Refactoring)하는 대신, 제 앱의 핵심 로직을 리버스 엔지니어링(Reverse-engineering)하여 Next.js 16, Neon PostgreSQL, Better Auth, 그리고 Vercel의 v0를 사용하여 처음부터 다시 구축하기로 결정했습니다.
다음은 제가 레거시 시스템(Legacy system)을 어떻게 분해하고 새로운 애플리케이션인 Crosshatch를 설계했는지에 대한 과정입니다. 라이브 배포 버전은 여기에서 확인할 수 있습니다: crossword-web-app.vercel.app
해체: 핵심 루프(Core Loop) 리버스 엔지니어링
기존 앱을 리버스 엔지니어링할 때는—심지어 본인의 앱이라 할지라도—그 목표는 기존의 복잡한 구조(Plumbing)로부터 근본적인 비즈니스 로직(Business logic)을 분리하는 것입니다. 저는 기존의 라우팅(Routing)과 뷰(View) 레이어를 무시하고 데이터 구조(Data structures)와 게임 루프(Game loop)에 완전히 집중했습니다.
저는 추출 및 현대화가 필요한 세 가지 핵심 시스템을 식별했습니다:
- 그리드 생성 (Grid Generation): 단어들이 어떻게 교차하고 10x10 매트릭스(Matrix)에 들어맞는지에 대한 방식.
- 상태 관리 (State Management): 사용자 입력, 활성 셀(Active cells), 그리고 방향 탐색(가로 vs 세로)을 추적하는 방식.
- 세션 및 진행 상황 (Session & Progress): 데이터베이스(Database)에 무리를 주지 않으면서 사용자의 진행 상황을 유지하는 방식.
핵심 로직을 모두 파악한 후, 재구축을 시작했습니다.
v0를 통한 UI 가속화
저는 새 앱이 신문에서 영감을 받은 차분한 라이트 테마(Light theme)를 갖기를 원했습니다. 빠르게 진행하기 위해, 저는 Vercel의 v0를 활용하여 초기 React 컴포넌트(Components)를 생성했습니다.
필요한 게임 상태 제약 조건(예: 탐색을 위한 Tab, Shift+Tab, 화살표 키와 같은 키보드 이벤트 처리)을 v0에 프롬프트로 입력함으로써, 10x10 대화형 그리드(Interactive Grid)를 빠르게 프로토타이핑할 수 있었습니다. v0가 Tailwind CSS 보일러플레이트(Boilerplate)를 처리해 준 덕분에, 저는 커스텀 useCrossword React 훅을 사용하여 복잡한 상태 머신(State Machine)을 연결하는 데 집중할 수 있었습니다.
현대적인 스택 및 아키텍처 (The Modern Stack & Architecture)
UI의 형태가 갖춰짐에 따라, 이를 지원하기 위한 견고한 백엔드 아키텍처를 구축했습니다.
- 프레임워크 (Framework): Next.js 16 (App Router)
- 데이터베이스 (Database): Drizzle ORM을 사용하는 Neon PostgreSQL
- 인증 (Authentication): Better Auth (안전한 이메일/비밀번호 흐름 및 세션 관리 처리)
재구축 과정에서 해결해야 했던 주요 기술적 문제들은 다음과 같습니다:
1. 결정론적 퍼즐 생성 (Deterministic Puzzle Generation)
기존 앱에서는 퍼즐 생성이 예측 불가능할 수 있었습니다. 새로운 빌드에서는 시드 기반 난수 생성(Seeded RNG) 알고리즘을 구현했습니다. 퍼즐의 ID(예: animals-1)를 시드(Seed)로 사용함으로써, 생성 과정이 완전히 결정론적(Deterministic)으로 이루어집니다. 이 알고리즘은 단어 목록을 섞고, 첫 번째 단어를 (0,0)에 배치하며, 교차하는 셀들을 매핑합니다. 만약 당신과 친구가 모두 animals-1을 불러온다면, 정확히 동일한 레이아웃을 얻게 될 것이라고 보장할 수 있습니다.
퍼즐 생성은 CPU 집약적(CPU-intensive)이기 때문에, 캐싱 레이어(Caching Layer)를 구현했습니다. 서버는 퍼즐을 한 번 생성하여 Map에 캐싱하고, 이후 요청 시 즉시 제공합니다.
2. 스마트 자동 저장 및 Visibility API (Smart Autosave & The Visibility API)
크로스워드 진행 상황을 잃어버리는 것은 매우 짜증 나는 일입니다. 저는 5초마다 자동 저장되는 시스템을 구축했습니다. 과도한 데이터베이스 쓰기(Write)를 방지하기 위해, Drizzle을 통해 PostgreSQL의 UPSERT 패턴을 사용했습니다:
INSERT INTO puzzle_progress (userId, puzzleId, entries, elapsedSeconds)
VALUES ($1, $2, $3, $4)
ON CONFLICT (userId, puzzleId)
...
또한, 퍼즐 타이머는 브라우저의 Visibility API를 사용합니다. 탭을 전환하면 타이머가 자동으로 일시 중지되어,
저는 완료 기록, 최고 기록, 그리고 평균 해결 시간을 추적하기 위한 사용자 대시보드 (user dashboard)를 구축했습니다. 하지만 사용자가 동일한 완료된 퍼즐을 두 번 제출하면 어떻게 될까요?
복잡한 애플리케이션 레벨 (application-level)의 체크 로직을 작성하는 대신, 데이터베이스 (database)가 이를 처리하도록 맡겼습니다. completion 테이블의 (userId, puzzleId)에 유니크 제약 조건 (unique constraint)을 추가하고 ON CONFLICT DO NOTHING 절을 활용했습니다. 사용자가 퍼즐을 처음으로 해결하면 기록이 저장됩니다. 만약 다시 제출한다면, 데이터베이스는 삽입 (insert)을 무시하며, 대시보드 분석 (analytics) 데이터를 왜곡하지 않으면서 정답을 확인했다는 응답을 반환합니다.
마치며
오래된 프로젝트를 리버스 엔지니어링 (reverse-engineering)하고 현대적인 도구로 다시 구축하는 것은 개발자로서 자신의 성장을 측정할 수 있는 가장 좋은 방법 중 하나입니다. Next.js로 전환하고 빠른 UI 프로토타이핑 (prototyping)을 위해 v0를 활용함으로써, 기본적인 CSS와 씨름하는 대신 실제 엔지니어링 문제(결정론적 생성 (deterministic generation) 및 상태 동기화 (state synchronization) 등)를 해결하는 데 시간을 집중할 수 있었습니다.
라이브 앱을 crossword-web-app.vercel.app에서 확인해 보시고, 아키텍처 (architecture)에 대한 의견을 들려주세요! 기술적인 피드백은 언제나 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기