한 코드베이스, 세 채널 동시 출격: Next.js + Capacitor

한 코드베이스, 세 채널 동시 출격: Next.js + Capacitor

한 문장 요약

하나의 코드로 웹과 앱(안드로이드/아이폰)을 동시에 만듭니다. 유지보수는 한 곳, 배포는 세 채널.

배경지식 체크: 아래 용어가 낯설면 ‘용어 30초 요약’을 먼저 읽어보세요.

용어 30초 요약

  • PWA: 앱처럼 설치되는 웹
  • SSR: 서버가 미리 화면을 만들어 속도/검색에 유리
  • ISR: 캐시된 화면을 일정 시간마다 새로고침
  • Capacitor: 웹을 앱으로 감싸 스토어에 올리게 해주는 도구
  • 딥링크/유니버설 링크: 링크로 앱을 바로 여는 방법

왜 단일 코드베이스인가

PWA만으로는 스토어 신뢰·인앱결제·푸시를 모두 담기 어렵습니다. 웹 팀만으로 앱 생태계에 빠르게 진입해야 했습니다.

그림으로 이해

src (web)
          ↓ build (SSR | export)
        out (정적 결과)
          ↓ copy
        apps/www (앱에 탑재)
          ↓ 래퍼(Android/iOS)
        Google Play / App Store (배포)

작동 방식(3단계)

  • 웹을 빌드합니다(SSR 또는 정적 export).
  • 빌드 결과를 앱(www) 폴더로 복사합니다.
  • Capacitor로 Android/iOS 래퍼를 열어 스토어에 올립니다.

기술적 의사결정

  • Next.js + Capacitor: 웹 코드 90% 재사용, 네이티브 권한만 최소 래핑 → 채택
  • BUILD_TARGET=web|export 로 SSR/Static 빌드 분리
  • 의사결정 기준: 팀 규모 대비 ‘신뢰 지표’ 상승폭

언제 좋은가

  • 소수 인원으로 웹·앱을 동시에 내야 할 때
  • 웹과 앱의 화면/경험을 거의 동일하게 유지하고 싶을 때
  • 시장 반응을 빠르게 보고 자주 업데이트해야 할 때

주의할 점

  • 고성능 네이티브(대형 3D/AR/초고빈도 센서)는 별도 네이티브 고려
  • 스토어 심사 요건(ATS, Data Safety, 권한 목적문구) 미리 준비
  • 딥링크/앱링크/리다이렉트 주소 일관성 유지

프로젝트 구조

hybrid-app/
          ├─ web/                  # Next.js(App Router) — PWA/SSR/ISR
          │  ├─ src/app            # 라우트·서버 액션
          │  ├─ src/api            # API 핸들러(edge/node)
          │  ├─ src/lib            # 공통 유틸·도메인 로직
          │  └─ public | out       # 정적 자산/정적 export 결과
          ├─ apps/                 # Capacitor 컨테이너
          │  ├─ android | ios      # 네이티브 래퍼
          │  └─ capacitor.config.ts
          └─ 빌드 흐름:
             1) web: npm run build:web / build:app
             2) copy: web/out → apps/www
             3) apps: npx cap copy && npx cap sync → 스토어 빌드

빌드/배포 플로우

# Web (SSR)
        cd web
        npm run build:web && npm run dev  # http://localhost:3000

        # App (Android)
        npm run build:app && npm run copy:to-app
        cd ../apps && npx cap copy && npx cap sync
        npm run android:build && npm run android

        # iOS (준비 필요)
        npm run ios

경계(Interface) 우선

웹-네이티브 경계를 먼저 정의합니다. Capacitor 플러그인 호출 시그니처를 표준화하고 브라우저 폴백을 제공해 테스트 가능성과 SSR 안전성을 확보합니다.

// device/clipboard.ts (web fallback)
        import { Clipboard } from '@capacitor/clipboard';

        export async function writeClipboard(text: string) {
          try {
            await Clipboard.write({ string: text });
          } catch {
            await navigator.clipboard?.writeText(text);
          }
        }
배경지식 체크: ‘플러그인’은 카메라/푸시처럼 네이티브 기능입니다. 웹에서는 실패할 수 있어 try/catch + 웹 폴백을 같이 둡니다.

Next.js + Capacitor 핵심 라이브러리/사용법

  • @capacitor/app: 앱 상태/딥링크(appUrlOpen) 이벤트
  • @capacitor/push-notifications: 권한 요청/토큰 발급/알림 수신
  • @capacitor/browser: 인앱 브라우저(OAuth 리다이렉트 도움)
  • @capacitor/clipboard: 복사/붙여넣기
  • @capacitor/preferences(또는 community secure storage): 민감 정보 보관
  • next-pwa: PWA 서비스워커/오프라인/설치 지원
// platform-guard.ts (SSR/웹/앱 안전 가드)
        import { Capacitor } from '@capacitor/core';
        import { App } from '@capacitor/app';

        export function getPlatform() {
          // SSR에서는 window가 없음
          if (typeof window === 'undefined') return 'server';
          return Capacitor.getPlatform(); // 'ios' | 'android' | 'web'
        }

        export function listenDeepLink(onOpen: (url: string) => void) {
          if (typeof window === 'undefined') return; // SSR 가드
          App.addListener('appUrlOpen', ({ url }) => onOpen(url));
        }
// push.ts (푸시 최소 예시)
        import { PushNotifications } from '@capacitor/push-notifications';

        export async function initPush() {
          const perm = await PushNotifications.requestPermissions();
          if (perm.receive !== 'granted') return;
          await PushNotifications.register();

          PushNotifications.addListener('registration', (token) => {
            // 서버에 token.value 전송
          });
          PushNotifications.addListener('pushNotificationReceived', (n) => {
            // 알림 수신 시 UX 처리
          });
        }
// next.config.js (next-pwa 간단 설정)
        const withPWA = require('next-pwa')({
          dest: 'public',
          disable: process.env.NODE_ENV === 'development',
        });
        module.exports = withPWA({});

SSR 안전 수칙

  • 플러그인 호출은 항상 클라이언트에서만(SSR 가드: typeof window 체크)
  • 네이티브 전용 코드는 dynamic import({ ssr: false })로 분리
  • 항상 웹 폴백을 준비(클립보드/브라우저/알림)

난제 & 해결

  • Google OAuth 리다이렉트: 앱 스킴 등록(kr.zolbo.app:/oauth2redirect)으로 해결
  • WS 끊김: visibilitychange/foreground에 재구독 + 지수 백오프
  • 번들 용량: route-level chunk·이미지 최적화·dynamic import
  • 심사 리젝: ATS/권한 최소·개인정보 고지/리다이렉트 일관성 체크리스트

임팩트(지표)

  • 고객 신뢰 설문 48% → 79%
  • 가입→활성 전환율 30% → 56%
  • 출시 리드타임 1.0x → 0.7x(웹 플로우 재사용)

운영 체크리스트

  • PWA: manifest/서비스워커/오프라인 전략
  • Android: keystore/권한/IAP 선언
  • iOS: Provisioning/Capabilities/ATS
  • 공통: SSO redirect, App/Universal Links
한 팀으로 네이티브 신뢰를 가져오는 가장 짧은 길.

핵심 배움

  • 코드는 재사용하고, 경험은 네이티브처럼
  • 결정 기준은 비용이 아니라 ‘신뢰 지표’
  • 웹 팀만으로 앱 생태계 진입 가능