사주고사 제작기 04. 온보딩과 재방문 동선 만들기
쓸 수 있는 앱은 됐는데 다시 올 이유가 없었다. 처음 온 사람에게 뭐 하는 서비스인지 알려주는 것과, 두 번째 왔을 때 같은 문제가 안 나오게 하는 것을 붙였다.
결과 페이지 재설계를 마무리했다
explanation 한 필드를 story와 analysis로 쪼개 정답 탭과 사주 분석 탭으로 나누는 작업, Gemini에게 구절 단위 대조 JSON을 받아 에세이에 하이라이트를 입히는 작업, 결과 URL을 공유받은 사람에게는 점수만 보여주는 접근 분기까지 정리를 끝냈다.
작업하면서 하나 남는 게 있었다. 하이라이트 UI를 만들 수 있었던 건 프론트 코드를 잘 짜서가 아니라 Gemini에게 userExcerpt를 원문 그대로 달라고 요청했기 때문이다. 프롬프트에서 무엇을 받을지 정하는 게 그대로 화면 구조를 정했다.
처음 온 사람이 뭐 하는 곳인지 몰랐다
랜딩에 바로 떨어지면 사주 모의고사라는 게 안 읽혔다. 3장짜리 슬라이드 온보딩을 넣었다. 사주로 운명을 읽고, 추리하듯 풀고, AI가 채점한다는 흐름이다.
캐러셀 라이브러리를 쓸까 고민했는데 3장에 swiper를 넣는 건 과했다. translateX와 transition-transform으로 충분했다. 스와이프는 touchStart와 touchEnd의 x 좌표 차이를 재서 50px 넘으면 넘긴다.
완료 처리는 localStorage와 쿠키에 둘 다 저장한다. 둘로 나눈 이유가 있다. 미들웨어는 서버에서 도는데 localStorage를 읽을 수 없다. 쿠키에 있어야 미들웨어가 온보딩을 이미 본 사람을 랜딩으로 바로 보낼 수 있다.
모바일에서 당겨서 새로고침되는 것도 막아야 했다. 슬라이드를 넘기려고 위아래로 손가락이 미끄러지면 페이지가 새로고침돼서 온보딩이 처음부터 다시 시작됐다. overscroll-none touch-none으로 잡았다.
"use client"로 만든 온보딩 페이지에서 하이드레이션 에러도 났다. 서버에서 프리렌더링할 때는 localStorage가 없어서 서버가 그린 결과와 브라우저가 그린 결과가 달라진 것이다. 마운트된 뒤에만 localStorage를 읽도록 바꿔 해결했다.
두 번째 방문에 같은 문제가 나왔다
풀었던 문제를 기억해야 했다. 로그인이 없으니 localStorage에 제출한 문제 ID를 쌓고, 다음 문제를 요청할 때 제외 목록으로 넘긴다.
// 제출 완료 시
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(",")}`);문제가 마음에 안 들 때 바꿀 수 있는 재선택 버튼도 넣었다. 에세이를 이미 쓰고 있으면 확인 다이얼로그를 한 번 띄운다.
재선택할 때 key prop을 바꿔서 컴포넌트를 통째로 리마운트시킨다. props만 갈아끼우면 이전 문제에 쓰던 에세이 입력값이 그대로 남는다. key가 바뀌면 React가 다른 컴포넌트로 보고 상태를 버린다.
GA는 환경변수가 없으면 안 붙게 했다
@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} />
)}환경변수가 없으면 스크립트 자체가 렌더링되지 않는다. 로컬 개발 중 클릭이 통계에 섞이는 걸 따로 신경 쓸 필요가 없어졌다.
막힌 지점과 원인
| 문제 | 원인 | 해결 |
|---|---|---|
| 온보딩 페이지 하이드레이션 에러 | 서버 프리렌더링 시 localStorage가 없어 렌더 결과 불일치 | 마운트 이후에만 localStorage 조회 |
| 모바일에서 슬라이드 중 새로고침 | 세로 스크롤 바운스가 당겨서 새로고침을 유발 | overscroll-none touch-none 적용 |
| 미들웨어가 온보딩 완료 여부를 모름 | localStorage는 서버에서 읽을 수 없음 | 쿠키에 함께 저장 |
| 문제 재선택 후 이전 에세이가 남음 | props만 바꿔서 컴포넌트 상태가 유지됨 | key 변경으로 리마운트 |
| 긴 에세이가 컨테이너를 뚫고 나감 | 줄바꿈 지점이 없는 긴 문자열 | break-all 적용 |
에세이 입력 진행률 바에 transition을 넣어 글자마다 끊기던 걸 부드럽게 만들었고, 공유 미리보기 이미지 레이아웃도 조정했다. 저장소에 올라가 있던 개발 도구 설정 디렉토리도 추적에서 뺐다.
생성 문제의 story와 analysis 품질 보강, 문제와 인물 데이터 이관, rate limiter Redis 전환은 아직 남아 있다.