사주고사 제작기 04. 다시 오고 싶은 앱으로
결과 페이지 대수술, 온보딩 플로우, 풀었던 문제 제외, GA 연동까지. "쓸 수 있는 앱"에서 "다시 오고 싶은 앱"으로.
결과 페이지 3섹션 재설계
3일차 제작기에서 예고했던 결과 페이지 리디자인을 마무리했다. 핵심은 세 가지.
데이터 모델 분리: explanation → story + analysis
기존에는 explanation 하나에 서사와 명리학 분석이 뒤섞여 있었다. 이걸 두 필드로 쪼갰다.
// Before
explanation: string; // "이 사람은 이렇게 살았다... 壬水 일간이..."
// After
story: string; // 순수 일상 서사
analysis: string; // 만세력 + 오행 + 대운 해석story는 "정답" 탭에서, analysis는 "사주 분석" 탭에서 보여준다. 역할이 명확해지니 UI도 깔끔해졌다.
Gemini 구조화 비교 — 하이라이트 UI
단순 점수 피드백이 아니라, 유저 답변과 정답을 구절 단위로 대조한 JSON을 Gemini에게 받도록 프롬프트를 바꿨다.
type ComparisonResult = {
summary: string;
matches: {
userExcerpt: string; // 유저 에세이 중 정답과 유사한 구절
storyExcerpt: string; // 정답에서 대응하는 구절
comment: string; // 유사 이유 설명
}[];
};이 데이터로 유저 에세이에 보라색 하이라이트를 입혔다. 마우스를 올리면 tooltip으로 "정답의 어떤 구절과 대응하는지" 보여준다. 프롬프트 설계가 곧 UX 설계라는 걸 다시 한번 체감.
접근 정책: 소유자 vs 비소유자
결과 URL을 공유받은 사람이 정답까지 다 보면 직접 풀 이유가 사라진다. 쿠키 기반으로 분기했다.
| 소유자 (제출자) | 비소유자 (공유 수신자) | |
|---|---|---|
| 점수/등급 | O | O |
| 채점 상세·에세이·정답 | O | X |
| "나도 풀어보기" CTA | X | O |
정보 비대칭이 공유 → 유입 → 전환 퍼널을 만든다.
온보딩 페이지 추가
첫 방문자에게 서비스 컨셉을 3장 슬라이드로 설명하는 온보딩을 추가했다.
🔮 사주로 운명을 읽다 → 🧩 추리하듯 풀어보세요 → ✨ AI가 채점합니다구현 포인트:
- 스와이프 제스처:
touchStart/touchEnd로 좌우 스와이프 감지 (threshold 50px) - CSS 트랜지션:
translateX기반 슬라이드 전환, 별도 라이브러리 없이 구현 - 완료 처리: localStorage + 쿠키 이중 저장. 미들웨어에서 쿠키로 체크해서 온보딩 완료자는 바로 랜딩으로 보낸다
- 건너뛰기: 상단 우측 "건너뛰기" 버튼으로 즉시 완료 처리
- 스크롤 바운스 차단:
overscroll-none touch-none으로 모바일에서 당겨서 새로고침 방지
캐러셀 라이브러리를 쓸까 고민했는데, 3장짜리 슬라이드에 swiper 같은 걸 넣는 건 과하다. translateX + transition-transform이면 충분했다.
SSR 이슈
온보딩 페이지를 "use client"로 만들었는데, 서버에서 프리렌더링할 때 localStorage가 없어서 하이드레이션 에러가 났다. 컴포넌트 마운트 후에만 localStorage를 읽도록 수정해서 해결.
풀었던 문제 제외 + 문제 재선택
같은 문제가 반복 출제되면 재미없다. localStorage에 제출 완료한 문제 ID를 저장하고, 다음 문제 요청 시 exclude 파라미터로 넘겨서 제외하도록 했다.
// 제출 완료 시
const solved = JSON.parse(localStorage.getItem("sajugosa_solved") || "[]");
solved.push(questionId);
localStorage.setItem("sajugosa_solved", JSON.stringify(solved));
// 다음 문제 요청 시
fetch(`/api/quiz/random?exclude=${solved.join(",")}`);퀴즈 페이지에는 RefreshCw 아이콘의 재선택 버튼도 추가했다. 이미 에세이를 작성 중이면 confirm 다이얼로그로 한 번 확인한다. 재선택 시 key prop을 바꿔서 컴포넌트를 완전히 리마운트시킨다.
Google Analytics 연동
@next/third-parties 패키지의 GoogleAnalytics 컴포넌트를 사용했다. Next.js 공식 지원이라 설정이 간단하다.
// app/layout.tsx
import { GoogleAnalytics } from "@next/third-parties/google";
// ...
{process.env.NEXT_PUBLIC_GA_ID && (
<GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
)}환경변수가 없으면 GA 스크립트 자체가 렌더링되지 않는다. 개발 환경에서 불필요한 트래킹을 자동으로 차단하는 셈.
자잘한 수정들
- 프로그레스 바 애니메이션: 에세이 입력 시 진행률 바에
transition추가. 글자 입력마다 뚝뚝 끊기던 게 부드럽게 채워진다. - OG 이미지 수정: 공유 시 미리보기 이미지 레이아웃 조정
- 에세이 텍스트 overflow: 결과 페이지에서 긴 에세이가 컨테이너를 뚫고 나가던 문제.
break-all로 해결. - .omc 디렉토리 git tracking 제거: 개발 도구 설정 파일이 저장소에 올라가 있던 문제 정리
오늘의 커밋 로그
badf93a feat: 결과 페이지 3섹션 재설계 + 접근 정책
3a2539b fix: progress bar에 transition 애니메이션 추가
5cacfe1 chore: .omc 디렉토리 git tracking 제거
e2e2bd7 docs: devlog 구조로 문서 재편성 + CLAUDE.md 현행화
aab097c feat: 풀었던 문제 제외 + 문제 재선택 버튼
26314e0 add: onboarding
96d5c22 fix: onboarding ssr
2c7b48b fix: og image
88835dd fix: 온보딩 페이지 스크롤 바운스 차단
be18403 feat: Google Analytics 연동 (@next/third-parties)
024fa38 fix: 결과 페이지 에세이 텍스트 overflow 방지 (break-all)배운 것
-
LLM 출력 구조화 = UX 설계. "비교해줘"라고 하면 텍스트 덩어리가 온다. JSON 스키마를 지정하면 하이라이트 UI를 만들 수 있다. 프롬프트에 투자한 시간이 프론트엔드 코드량을 줄여준다.
-
정보 비대칭이 전환율을 만든다. 결과를 전부 공개하면 공유받은 사람이 직접 풀 동기가 없다. 점수만 보여주고 나머지를 가리면 "나도 해볼까?"가 된다. 컨텐츠 접근 정책이 곧 그로스 전략이다.
-
3장짜리 캐러셀에 라이브러리는 과하다.
translateX+transition-transform+ 터치 이벤트면 충분하다. 의존성 하나를 줄이면 번들 사이즈와 유지보수 부담이 함께 줄어든다. -
localStorage는 SSR의 적이다. Next.js에서
"use client"컴포넌트도 서버에서 프리렌더링된다.localStorage/document접근은 반드시useEffect안에서. 당연한 건데 매번 한 번씩 걸린다. -
"다시 오고 싶은" 경험은 디테일에 있다. 풀었던 문제 제외, 프로그레스 바 부드러운 전환, 온보딩 슬라이드 — 각각은 작은 변경이지만, 합치면 "이 앱 좀 신경 썼네"라는 인상을 준다.
남은 과제
- generated-questions
story필드 배치 보강 (품질 개선) - generated-questions
analysis만세력 + 평생 사주풀이로 보강 - 문제/인물 데이터 Supabase 이관
- Rate limiter Redis 전환 (Upstash)
- Vercel Analytics 연동