모여모임 조율 서비스

여러 사람의 가능한 날짜와 시간을 수집해 모임 일정을 조율하는 모바일 중심 서비스입니다.

2026.06 — 2026.08 · FE 개발 · 5인 (FE 2인)

  • Next.js
  • React
  • TypeScript
  • Zustand
  • TanStack Query
  • Axios
  • Tailwind CSS
  • Vitest
  • React Testing Library
  • Expo
  • React Native WebView
  • Storybook
  • Orval
  • pnpm
  • Turborepo
  • 모여 홈 화면
    홈진행 중 · 확정된 모임
  • 모여 초대장 공유 화면
    초대장 공유카카오톡 · SMS · URL 복사
  • 모여 초대 받기 화면
    초대 받기링크로 회원 · 게스트 참여
  • 모여 조율 현황 화면
    조율 현황일정 · 위치 후보 추천

개요

  • 팀 구성: 프론트엔드 2명 / 백엔드 1명 / 디자이너 1명 / PM 1명 (총 5인)
  • 개발 기간: 2026.06 — 2026.08 (2개월)
  • 출시 결과: iOS 8/17 · Android 8/21 출시, 가입 72명 · 앱 다운로드 68회

핵심 담당 범위

웹 화면을 Expo 앱의 WebView로 감싸 iOS·Android 앱을 출시했습니다. 모임 생성과 회원·게스트 참여 흐름, 웹과 앱을 잇는 브릿지를 맡았고, 출시 직전 심사를 막던 연결·로그인 문제에 대응했습니다.

  • 모임 생성 · 회원/게스트 참여 플로우와 세션 · OAuth 연결
  • 웹–네이티브 브릿지(postMessage) 구현, Android 메시지 수신 버그 해결
  • 딥링크 · 카카오톡/SMS 초대 공유
  • 앱 심사 환경의 API 연결 · 해외 로그인 문제 대응

심사에서만 생기는 로그인 무한 로딩을 좁혀 출시 성공

iOS 심사에서 애플 로그인 뒤 무한 로딩으로 인해 거절됐지만, 저희 팀이 소유한 모든 기기에서 재현되지 않았습니다. 서버 오류라면 안내 토스트가 떴어야 하므로 「응답이 아예 오지 않는 경우」로 범위를 좁히고, timeout 설정과 서버를 고정 도메인으로 변경해 발생 경로를 차단했습니다. 이후 카카오 해외 로그인 차단까지 대응해 iOS 8/17 · Android 8/21에 출시했습니다.

증상

  • 심사 기기(iPad Air 11, iPadOS 26.6)에서 애플 로그인 뒤 로딩이 끝나지 않아 무한 스피너 발생 (Guideline 2.1(a))
  • 팀 기기에서는 OS를 동일한 버전으로 업데이트해도 재현되지 않고, 서버 오류 로그도 없음

가설과 검증

로그인은 취소 · state 불일치 · 4xx/5xx를 사유별 안내 화면으로 돌려보내도록 이미 처리돼 있었습니다. 그래서 무한 로딩은 「응답이 아예 오지 않는」 경우에만 생길 수 있다고 범위를 좁혔습니다.

가설근거확인한 것조치
서버가 오류를 줌배포 시점과 심사 시각이 겹침오류라면 실패 안내가 떴어야 함 → 배제—
응답이 오지 않아 요청이 끝나지 않음Axios 기본 timeout이 0(무제한)으로 설정되어 있었음요청이 대기 중이면 오류 처리가 아예 불리지 않음모든 요청에 15초 상한 설정 + 시간 초과 안내 처리
심사망에서 서버 주소에 연결 실패API 주소가 IP 매핑용 테스트 도메인(sslip.io)서버가 모든 요청을 기록하지 않아 확인 불가 → 요청 로그 추가 요청고정 도메인 구매 · 연결

판단

  • 원인을 하나로 확정하지 못해, 응답이 없는 경우(무한 대기 · 테스트 도메인)의 원인을 제거
  • 웹 도메인까지 바꾸면 앱 재빌드와 이미 제출한 Android 재심사가 필요해, 서버 API 주소만 교체

결과

  • 다음 심사에서는 해당 오류가 해결되어 정상적으로 API가 호출됨을 확인
  • 그러나 그다음 심사에서는 해외 심사자의 카카오 로그인이 차단되는 문제(해외 로그인 차단)로 인해 거절되었고, 심사 계정 전략을 바꿔 단일 카카오 계정이 아닌 3개의 신규 계정을 추가로 생성해 제출하고 통과
  • 이후 iOS 8/17 · Android 8/21 출시 완료

실기기 WebView 개발에서 막힌 API 호출을 같은 origin 프록시로 해결

휴대폰의 WebView로 로컬 개발 서버를 열면 주소가 공유기 IP로 바뀌어, 백엔드 CORS 허용 목록에 없는 origin이 되며 API 호출이 모두 막혔습니다. origin을 매번 등록하는 대신, Next.js Route Handler가 같은 origin에서 요청을 받아 백엔드로 전달하도록 바꿨습니다. 이후 어느 네트워크의 실기기든지 백엔드 설정 변경 없이 개발할 수 있게 됐습니다.

증상

  • 폰 WebView에서 로컬 dev 서버(192.168.x.x:3000)를 열면 모든 API 요청이 CORS 오류로 실패
  • 백엔드 CORS는 정해진 origin만 허용해, 네트워크가 바뀔 때마다 서버 설정 변경을 요청해야 했음
  • PC 브라우저(localhost)에서는 같은 화면이 정상이라, 실기기 확인만 막히는 상태

원인과 대안 비교

브라우저는 페이지 origin과 API origin이 다르면 CORS 허용 여부를 확인합니다. 실기기 개발 origin은 공유기마다 달라서 백엔드 허용 목록에 임시 추가하는 건 정답이 아니었습니다.

대안장점문제판단
백엔드 CORS에 origin 추가프론트 변경 없음네트워크가 바뀔 때마다 백엔드 수정 · 배포✕
next.config rewrites설정 몇 줄로 끝남전달할 헤더 정리 · 실패 응답을 직접 다루기 어려움△
네이티브 브릿지로 요청 대행WebView의 CORS를 거치지 않음모든 API 호출이 앱에 묶여 웹 단독 실행 · 테스트가 어려움✕
Route Handler 프록시같은 origin, 헤더 정리와 502 응답을 코드로 제어요청이 Next 서버를 한 번 더 거침○ 선택

구현 방법

  • 브라우저 · WebView는 /backend-api/*로 같은 origin 요청, Route Handler가 백엔드로 전달
  • Origin · Referer · sec-fetch 헤더를 지워 백엔드가 서버 간 요청으로 받게 하고, 실패는 502로 반환
  • 프록시는 개발 환경 전용으로만 사용하여 배포 앱은 기존처럼 백엔드를 직접 호출

결과

  • 네트워크가 바뀌어도 백엔드 설정 변경 요청 없이 실기기에서 바로 개발할 수 있게 됨

WebView 앱의 로그인 세션을 웹과 네이티브가 나눠 지키도록 설계

웹 화면을 WebView로 띄우는 구조라, 웹 저장소(localStorage)는 앱이 직접 관리하지 못해 앱을 다시 켤 때 토큰이 남아 있다고 보장할 수 없었습니다. 로그인이 앱 수준에서 오래 유지되도록 토큰을 네이티브 보안 저장소(SecureStore)에도 보관하고, 앱이 켜질 때 웹과 네이티브가 메시지로 세션을 맞추게 구현했습니다. 이 과정에서 Android에서만 네이티브 메시지를 받지 못하는 문제를 찾아, 두 플랫폼의 전달 방식 차이를 테스트로 고정하고 문제를 해결했습니다.

설계

  • 웹에서는 토큰을 localStorage에 두고, 앱에서는 네이티브 보안 저장소 SecureStore(iOS 키체인 · Android Keystore)에도 함께 보관
  • 웹은 SecureStore에 직접 접근할 수 없어, 메시지(postMessage)로 로그인 · 로그아웃 · 복원을 주고받음
  • 메시지 이름과 내용 타입은 웹과 앱이 함께 쓰는 패키지(@repo/types)에서 한 곳에 관리

앱을 켤 때 세션 복원 순서

네이티브 응답이 오지 않아도 화면이 멈추지 않도록, 기다리는 시간에 1.5초 상한을 두었습니다.

상황웹네이티브결과
웹 저장소에 토큰 있음바로 세션 복원—로그인 상태
웹 저장소가 비어 있음READY를 보내고 대기SecureStore 확인 → AUTH_TOKEN네이티브 토큰으로 복원
네이티브에도 없음대기 종료AUTH_NONE 응답비로그인 확정
응답이 오지 않음1.5초 뒤 대기 종료—비로그인 확정 (화면 멈춤 없음)

Android에서만 메시지를 받지 못하던 문제

  • 증상: Android 앱에서만 네이티브가 보낸 메시지가 웹에 도착하지 않음
  • 원인: react-native-webview는 iOS에서 window, Android에서 document에 message 이벤트를 보내고, Android는 버블링도 켜지 않아 window 리스너가 받지 못함
  • 해결: window에 캡처 단계 리스너를 걸어 두 경로를 한 곳에서 받고, 메시지 형식 검증을 함수로 분리해 해결
  • 검증: 플랫폼별 전달 방식 · 중복 전달 · 잘못된 형식을 흉내 낸 테스트 8개로 고정

실패 테스트로 AI 구현 결과를 검증하는 TDD 흐름 하네스 구현

AI에게 구현을 맡기면 완료했다는 말과 실제 동작이 어긋날 수 있어, AC를 테스트로 옮기고 그 테스트로 구현을 검증하는 흐름을 만들었습니다. 시나리오 확정(AC) → 실패하는 테스트 코드 → 테스트 코드가 성공하는 최소 구현 → 리팩터를 단계별 스킬로 나누고, 구현 단계에서 테스트 파일을 고칠 수 없게 했습니다. 또한 사용하며 대기 시간이 길어지는 문제와 타입 검사 공백을 규칙으로 고쳤습니다.

테스트를 먼저 작성한 이유

  • AI가 테스트와 구현을 함께 쓰면, 구현에 맞춰 테스트를 고치는 식으로 통과시킬 수 있음
  • 그래서 무엇을 검증할지는 사람이 먼저 승인하고, 이후 단계에서 테스트 코드를 바꿀 수 없게 함

단계별 흐름

앞 단계의 결과물이 다음 단계의 검증 기준이 되도록, 다음 단계에서는 이전 결과물을 고칠 수 없게 막았습니다. 예를 들어 Green 단계는 Red에서 만든 테스트를 수정할 수 없어, 구현이 그 테스트를 통과해야만 Refactor로 넘어갑니다.

단계하는 일막아 둔 것다음으로 넘기는 것
1. 시나리오 확정시그니처와 테스트 시나리오 도출사람이 승인하기 전 테스트 코드 작성이슈별 시나리오 문서
2. Red시나리오를 실패 테스트로 작성동작 코드 작성 (시그니처만 있는 빈 함수만 허용)의도한 이유(구현없음)로 실패하는 테스트 파일들
3. Green실패 테스트를 하나씩 통과시키는 최소 구현테스트 파일 수정 — 테스트가 틀렸으면 멈추고 보고모든 테스트 코드가 통과하는 최소 구현
4. Refactor · AC 검증초록을 유지하며 구조 정리, 별도 에이전트가 AC 충족 판정새 기능 · 범위 밖 수정, 「테스트 통과 = 완료」로 판단사람이 커밋할 변경 파일들

직접 쓰면서 고친 규칙

겪은 문제바꾼 규칙
루프마다 전체 테스트를 돌려 jsdom 준비만 23개 파일에 70초, 시나리오 10개 이슈에서 대기가 10분을 넘음루프 중에는 바뀐 범위만 돌리고, 전체 테스트는 이슈를 마칠 때 한 번만
테스트는 모두 통과했는데 빌드가 깨짐. 원인은 같은 키를 두 번 쓴 타입 오류(TS1117)였고, vitest는 타입을 검사하지 않아 못 잡음이슈를 닫기 전 타입 검사(check-types)를 돌리도록 Green 스킬에 규칙 추가
원인을 모르는 실패를 AI가 추측으로 계속 고침같은 실패로 두 번 고치지 못하면 멈추고 원인 진단부터

결과

  • 시나리오 문서 33개, apps/web 테스트 파일 183개