재하의 개발 블로그
회고 5 분 소요

모바일 청첩장 만들기 (2) - 데이터 분리와 UX 개선

react typescript scroll-snap mobile-ux wedding-invitation side-project

MVP 다음 날

MVP를 만든 다음 날(10/26), 가장 먼저 한 건 코드를 정리하는 것이었다. MVP는 빠르게 만드는 게 목적이었기에, 결혼식 날짜, 이름, 장소 같은 데이터가 컴포넌트 곳곳에 하드코딩되어 있었다.

하드코딩에서 JSON 데이터 분리로

문제

// Before: 컴포넌트에 데이터가 흩어져 있음
<h2>신부이름 ♥ 신랑이름</h2>
<p>202X년 X월 X일 토요일</p>
<p>서울 시내 예식장...</p>

이름이나 날짜를 바꾸려면 여러 파일을 열어서 하나씩 수정해야 했다. 실제로 MVP 마지막 커밋이 "잘못된 날짜와 이름 변경"이었는데, 7개 파일을 수정해야 했다.

해결: wedding.json + TypeScript 타입

모든 데이터를 src/data/wedding.json 하나로 모았다.

{
  "couple": {
    "groom": { "name": "신랑이름", "fullName": "신랑 풀네임" },
    "bride": { "name": "신부이름", "fullName": "신부 풀네임" }
  },
  "wedding": {
    "date": "202X년 X월 X일",
    "time": "오후 X시 X분",
    "venue": {
      "name": "예식장명",
      "address": "서울 시내...",
      "location": { "latitude": 37.xxxx, "longitude": 127.xxxx }
    }
  }
}

그리고 src/types/wedding.ts에 TypeScript 인터페이스를 정의했다.

export interface WeddingData {
  couple: {
    groom: PersonInfo;
    bride: PersonInfo;
  };
  wedding: WeddingInfo;
  accounts: { groom: {...}; bride: {...}; };
  gallery: { images: string[]; };
  metadata: { title: string; description: string; ogImage: string; };
}

이제 데이터를 바꾸려면 JSON 파일 하나만 수정하면 된다. 컴포넌트는 props로 데이터를 받아서 렌더링할 뿐이다.

재사용 가능한 구조

이 분리 덕분에 프로젝트가 "이 친구만의 청첩장"에서 "누구나 쓸 수 있는 청첩장 템플릿"이 될 가능성도 열렸다. JSON만 바꾸면 다른 커플의 청첩장이 되니까.

갤러리 스와이프 기능

MVP의 갤러리는 그리드에서 사진을 클릭하면 풀스크린 모달이 뜨는 단순한 구조였다. 모바일에서는 스와이프로 이전/다음 사진을 넘기는 게 자연스럽다.

터치 이벤트 처리

const handleTouchStart = (e: React.TouchEvent) => {
  touchStartX.current = e.touches[0].clientX;
};
 
const handleTouchEnd = (e: React.TouchEvent) => {
  const diff = touchStartX.current - e.changedTouches[0].clientX;
  if (Math.abs(diff) > 50) { // 50px 이상 스와이프
    diff > 0 ? nextImage() : prevImage();
  }
};

touchstart에서 시작 좌표를 기록하고, touchend에서 이동 거리를 계산한다. 50px 이상 움직이면 스와이프로 판단한다.

포커스 처리

갤러리 모달이 열릴 때 해당 이미지에 자동으로 포커스되도록 했다. 이건 작은 디테일이지만 UX에 큰 차이를 만든다.

Scroll Snap: 섹션 단위 스크롤

모바일 청첩장의 핵심 UX 중 하나가 섹션 단위 스크롤이다. 스크롤하면 각 섹션에 "착" 달라붙는 느낌.

CSS Scroll Snap

.snap-container {
  scroll-snap-type: y proximity;
  scroll-behavior: smooth;
  overflow-y: scroll;
  height: 100dvh;
  overscroll-behavior: none;
}
 
.snap-section {
  scroll-snap-align: start;
  scroll-snap-stop: normal;
  min-height: 100dvh;
}

scroll-snap-type: y proximity를 선택한 이유가 있다. mandatory는 무조건 스냅하기 때문에 긴 섹션에서 중간까지만 스크롤하고 싶을 때 불편하다. proximity는 스냅 포인트 근처에서만 스냅하므로 더 자연스럽다.

100dvh(dynamic viewport height)를 사용한 것도 포인트다. 모바일 브라우저에서 주소창이 나타나고 사라질 때 100vh는 고정이지만, 100dvh는 실제 보이는 높이에 맞춰진다.

메인 화면 디자인과 콘텐츠 추가

10/27~28에는 디자인 작업과 콘텐츠 보강에 집중했다.

양가 부모님 섹션

부모님 정보와 편지 기능을 추가했다. 신랑측, 신부측 각각의 부모님 소개와 짧은 편지를 넣었다.

22bf0f2 신부측 부모님 섹션 변경
fbb3072 양가 부모님 편지 추가
af4bc11 부모님 파트 배경 색상 변경

이 작업에서 느낌 건, 코드보다 콘텐츠가 더 오래 걸린다는 것이다. 어떤 내용을 넣을지, 어떤 톤으로 쓸지 친구와 상의하는 시간이 코드 작성 시간보다 훨씬 길었다.

이 시기에 배운 것

  1. 데이터와 뷰의 분리: 당연한 원칙이지만 MVP 단계에서는 무시하기 쉽다. 나중에 분리하려면 작업량이 배로 든다.
  2. CSS Scroll Snap의 proximity vs mandatory: 모바일에서는 proximity가 훨씬 자연스럽다.
  3. 100dvh vs 100vh: 모바일 브라우저 주소창을 고려하면 dvh 단위가 필수적이다.
  4. 사이드 프로젝트에서 콘텐츠의 중요성: 기술적 완성도보다 실제 콘텐츠의 질이 최종 결과물의 품질을 결정한다.

다음 편에서는 이미지 최적화와 성능 개선 작업을 다룬다.