동기 : "쓸 수 있는" 서비스에서 "쓰고 싶은" 서비스로
- Day2에서 누구나 방을 만들고 링크를 공유할 수 있게 만들었다.
- 기능적으로는 다 된 셈인데, 막상 써보면 아쉬운 부분이 한둘이 아니었다.
- 메뉴 정리해서 주문할 때 일일이 타이핑해야 한다. 복사 기능이 있으면 좋겠는데?
- 카카오톡으로 링크 보내면 미리보기가 안 뜬다. 밋밋한 URL만 덩그러니.
- 밤에 쓰면 눈이 부시다. 다크모드가 없으니까.
- 옆에 있는 사람한테 링크를 공유하려면? 카톡을 보내야 하나?
- 누군가 악의적으로 투표를 도배하면? 과금 폭탄 맞으면?
- 그래서 Day3의 목표는 "실제로 쓸 때 불편한 점을 하나씩 잡아내는 것"이었다.
큰 흐름
1. 주문 요약 복사
- 투표가 끝나면 결국 주문을 해야 한다. 그때 "짜장 3개, 짬뽕 2개..." 이걸 일일이 적고 있으면 귀찮다.
- 📋 주문 복사 버튼을 누르면 팝업이 뜨고, 두 가지 형식 중 하나를 고를 수 있다.
- 메뉴만 복사 :
짜장면 3 / 짬뽕 2
- 사람 포함 복사 :
짜장면 3 — 철수, 영희, 민수 / 짬뽕 2 — 지원, 하늘
- 복사하면 하단에 토스트 알림이 잠깐 뜨고 사라진다.
- 검색 키워드 : navigator.clipboard.writeText, toast notification
2. 카카오톡 미리보기 (OG 태그)
- 카카오톡에 링크를 보내면 제목, 설명, 이미지가 미리보기로 뜬다.
index.html에 Open Graph 메타 태그를 추가했다.
og:title, og:description, og:image 이 세 개가 핵심이다.
- Twitter Card 메타 태그도 같이 넣어서 트위터에서도 미리보기가 나온다.
- 검색 키워드 : og:title, og:description, og:image, twitter:card
3. 만료 시간 안내
- Day2에서 24시간 자동 만료를 넣었는데, 사용자 입장에서는 "이 방 언제 사라지지?"를 모른다.
- 페이지 최하단에 남은 시간을 작게 표시했다.
- 1분마다 자동 갱신되고, 남은 시간에 따라 색상이 바뀐다.
- 6시간 이상 → 회색 (안심)
- 1~6시간 → 주황 (주의)
- 1시간 미만 → 빨강 (긴급)
- 투표나 사진 업로드 같은 활동을 하면 24시간이 리셋되면서 안내도 갱신된다.
- 검색 키워드 : setInterval, Date.now, 조건부 CSS 클래스
4. SEO (검색엔진 최적화)
- 구글이나 네이버에서 검색하면 이 사이트가 나오게 하고 싶었다.
- 한 작업들 :
sitemap.xml 생성 → 검색엔진에 페이지 구조 알려주기
robots.txt 수정 → 크롤러 허용 범위 설정
index.html 메타 태그 보강 → 제목, 설명, 키워드
- Google Search Console 등록 →
google-site-verification 메타 태그
- 네이버 서치어드바이저 등록 →
naver-site-verification 메타 태그
- 사이트 제목을 "vote-eat | 메뉴 수합(아아 하나 추가요!)"로 정했다.
- 검색 키워드 : sitemap.xml, robots.txt, Google Search Console, 네이버 서치어드바이저
5. 악의적 트래픽 방어
- Firebase는 무료 한도를 넘으면 과금된다. 누군가 자동화 도구로 투표를 수천 번 날리면?
- 두 가지 레이어로 막았다.
서버단 : Firestore 보안 규칙
- 그룹 이름 50자 제한
- 투표 항목 이름 50자 제한
- 투표자 배열 100명 제한
- 데이터 타입 검증 (문자열 필수 등)
클라이언트단 : 쿨다운
- 투표 : 2초
- 빠른 참여 : 2초
- 이미지 업로드 : 5초 + 방당 최대 20장
- 방 생성 : 3초
useRef로 마지막 액션 시각을 추적하고, 쿨다운 중에는 버튼이 비활성화된다.
- 검색 키워드 : Firestore Security Rules, rate limiting, useRef cooldown
6. 다크모드
- 밤에 쓰면 눈이 부시다는 건 사실 처음부터 알고 있었다. 미루다가 드디어 넣었다.
- 구현 방식 :
- CSS 변수(
--bg-color, --text-color 등)로 전체 색상을 관리
[data-theme="dark"] 선택자로 다크모드 색상 오버라이드
themeUtils.js에서 테마 상태를 관리 (localStorage 저장 + 시스템 설정 감지)
- 헤더에 ☀️/🌙 토글 버튼
- 시스템 다크모드를 감지해서 처음 접속 시 자동 적용되고, 수동으로 바꾸면 그 선택을 기억한다.
- 카드, 입력 필드, 팝업, 토글 스위치 등 모든 UI 요소에 다크모드를 적용했다. 이게 생각보다 손이 많이 갔다.
- 검색 키워드 : CSS custom properties, data-theme, prefers-color-scheme, localStorage
7. 반응형 UI 개선
- 모바일에서 쓰는 사람이 대부분인데, 작은 화면에서 깨지는 부분들이 있었다.
480px, 360px 브레이크포인트를 추가하고 세부 조정했다.
- 투표 항목 헤더가 한 줄에 안 들어가면 자연스럽게 줄바꿈
- 메뉴 이름이 너무 길면 말줄임(...)
- 가격 행, 배지, 버튼 크기 축소
- 터치 타겟 확대 (손가락으로 누르기 편하게)
- 검색 키워드 : media query, flex-wrap, text-overflow ellipsis, min-width 44px
8. 공유 팝업 (QR 코드 + 네이티브 공유)
- Day2에서 만든 🔗 공유 버튼은 링크 복사 한 가지뿐이었다.
- 근데 옆에 있는 사람한테 공유하려면? 카톡을 보내야 하나? QR이면 바로 스캔하면 되는데.
- 기존 버튼을 공유 팝업으로 교체했다. 누르면 3가지 옵션이 나온다.
- 🔗 링크 복사 — 클립보드에 복사
- ⊞ QR 코드 — 화면에 QR 표시, 옆 사람이 카메라로 스캔
- ↗ 다른 앱으로 공유 — 카카오톡, 메시지, AirDrop 등 (모바일에서만 표시)
- QR 코드는
qrcode.react 라이브러리로 SVG 렌더링. 다크모드에서도 잘 보이도록 색상 분기 처리.
- "다른 앱으로 공유"는 Web Share API를 사용하는데, 이건 모바일 브라우저에서만 지원한다. PC에서는 자동으로 숨겨진다.
- 검색 키워드 : qrcode.react, QRCodeSVG, navigator.share, Web Share API
9. 관리자 페이지
- Firebase 과금이 걱정되면 서비스를 통째로 꺼버릴 수 있어야 한다.
/admin?key=비밀키 URL로 접근하는 관리자 페이지를 만들었다.
- 두 가지 토글 스위치 :
- 🌐 전체 서비스 on/off — 끄면 방 생성, 투표, 참여 모두 차단
- 📷 사진 업로드 on/off — 끄면 업로드만 차단 (Storage 비용 절감)
- Firestore의
config/service 문서에 설정값을 저장하고, 앱 로드 시 1회 읽기.
- 끈 상태에서 사용자에게는 커스텀 점검 안내 메시지가 표시된다. 메시지도 관리자 페이지에서 수정 가능.
- 관리자 키는
.env에 저장해서 코드에 노출되지 않게 했다.
- 검색 키워드 : Firestore config document, admin panel, feature toggle, environment variable
조금 헤맸던 부분들
1. 다크모드 CSS 변수 지옥
- 처음에는 "색상만 바꾸면 되지" 했는데, 실제로는 카드, 입력 필드, 팝업, 모달, 프로그레스 바, 캐러셀 화살표 등등 전부 개별적으로 색상을 잡아줘야 했다.
- CSS 변수를 50개 넘게 만들었다.
--card-bg, --border-color, --input-bg, --carousel-arrow-bg ...
- 한번 체계를 잡아놓으니 이후에 추가되는 컴포넌트도 변수만 쓰면 자동으로 다크모드가 적용되어서, 투자할 만했다.
2. Web Share API의 브라우저 호환성
navigator.share()는 모바일 Safari, Chrome에서는 잘 동작하는데 데스크톱에서는 대부분 지원하지 않는다.
- 그래서
typeof navigator.share === "function"으로 체크해서, 지원하지 않는 환경에서는 해당 버튼을 아예 숨겼다.
- 사용자가 공유를 취소하면
AbortError가 발생하는데, 이건 에러가 아니니까 무시하도록 처리했다.
3. QR 코드 다크모드 대응
- QR 코드의 기본 배경은 흰색, 패턴은 검정이다. 다크모드에서 이러면 네모난 흰색 덩어리가 떠 있는 것처럼 보인다.
bgColor="transparent"로 배경을 투명하게 하고, fgColor를 테마에 따라 분기시켰다.
- 라이트모드 :
#1f2937 (어두운 회색)
- 다크모드 :
#f1f5f9 (밝은 회색)
오늘 추가된 기능 정리
| 기능 | 설명 |
|---|
| 주문 요약 복사 | 메뉴만 / 사람 포함, 두 가지 형식 클립보드 복사 |
| OG 태그 | 카카오톡 등 SNS 링크 미리보기 |
| 만료 시간 안내 | 페이지 하단에 남은 시간 표시 (색상 변화) |
| SEO 최적화 | sitemap, robots.txt, Google/네이버 등록 |
| 트래픽 방어 | Firestore 보안 규칙 + 클라이언트 쿨다운 |
| 다크모드 | 시스템 감지 + 수동 토글 + 전체 UI 대응 |
| 반응형 UI | 480px/360px 미디어 쿼리 보강 |
| 공유 팝업 | 링크 복사 + QR 코드 + 네이티브 공유 |
| 관리자 페이지 | 전체 서비스 / 사진 업로드 on·off 원격 제어 |
현재까지의 전체 기능
| 기능 | 설명 | Day |
|---|
| 실시간 투표 | 이름 + 메뉴 입력 → 모든 사용자 화면에 즉시 반영 | 1 |
| 빠른 참여 | 기존 메뉴에 "+ 참여" 버튼으로 바로 합류 | 1 |
| 메뉴 사진 | 다중 업로드, 캐러셀, 전체화면 모달, 개별 삭제 | 1 |
| 가격 & 총 금액 | 메뉴별 가격 입력 → 소계, 총 예상 금액 자동 계산 | 1 |
| 그룹 | 여러 팀이 독립 그룹으로 동시 사용 가능 | 1 |
| 전체 초기화 | 투표 + 사진 한 번에 리셋 | 1 |
| 이미지 자동 압축 | 큰 사진 자동 리사이즈 + JPEG 압축 | 2 |
| 공개 서비스 모드 | 방 만들기 + 내 투표방 + 링크 공유 | 2 |
| 자동 만료 | 24시간 미사용 방 자동 삭제 | 2 |
| 주문 요약 복사 | 메뉴만/사람 포함 두 가지 형식 복사 | 3 |
| OG 태그 | 카카오톡 링크 미리보기 | 3 |
| 만료 시간 안내 | 남은 시간 표시 + 색상 변화 | 3 |
| SEO | 구글/네이버 검색엔진 등록 | 3 |
| 트래픽 방어 | Firestore 규칙 + 쿨다운 | 3 |
| 다크모드 | 시스템 감지 + 토글 + 전체 UI | 3 |
| 반응형 UI | 모바일 최적화 | 3 |
| 공유 팝업 | 링크 복사 + QR + 네이티브 공유 | 3 |
| 관리자 페이지 | 서비스/업로드 원격 on·off | 3 |
기술 스택
| 분류 | 기술 |
|---|
| 프론트엔드 | React 19, CSS (순수) |
| 라우팅 | react-router-dom |
| 백엔드/DB | Firebase Firestore (실시간 NoSQL) |
| 파일 저장 | Firebase Storage |
| 배포 | Firebase Hosting |
| QR 코드 | qrcode.react |
| 기타 | Canvas API, localStorage, Web Share API, CSS custom properties |
결과
👉 https://vote-eat.web.app
- Day1은 "돌아가게", Day2는 "남한테 보여줄 수 있게", Day3는 "편하게 쓸 수 있게" 다듬었다.
- 기능 자체보다는 쓰는 사람 입장에서의 편의성에 집중했다. 주문 복사, 다크모드, QR 공유 같은 건 없어도 되지만 있으면 확실히 다르다.
- 관리자 페이지까지 넣으니 과금 걱정 없이 서비스를 열어둘 수 있게 됐다. 위험하면 토글 하나로 끄면 되니까.