[개발로그] 웹 개발 단체가 네이티브 앱을 만들어야 할 때 벌어지는 일 -React + Tauri
"손전등 켜는 거… 웹에서는 안 되잖아요?"
이 한마디에서 시작된, SCG의 모바일 앱 개발 대장정을 기록합니다.
프롤로그: 새로운 의뢰, 그리고 낯선 영역
어느 날, SCG에 새로운 프로젝트 의뢰가 들어왔습니다.
요구사항을 정리해보니, 이건 단순한 웹사이트가 아니었어요. 푸시 알림, 생체 인증, 네이티브 UI 위젯, 앱스토어 배포까지. 지금까지 SCG가 해왔던 웹 개발의 영역을 한참 벗어나는 스펙이었습니다.
우리는 25년 동안 웹을 해온 단체입니다. React, Next.js, TypeScript — 이런 건 눈 감고도 합니다. 그런데 모바일 네이티브? iOS 빌드? APK 사이닝? 이건 처음이었어요.
보통 이런 상황에서 선택지는 두 가지입니다.
"외주 맡기자" 아니면, "우리가 직접 부딪혀보자."
SCG는 당연히 후자를 택했습니다.
Chapter 1. 스택 전쟁 — "Flutter냐, WebView냐, 그것이 문제로다"
프로젝트의 첫 번째 관문은 기술 스택 선정이었습니다. 이 단계에서 구성원들 사이에 상당히 치열한 논의가 오갔는데요, 크게 세 가지 선택지가 테이블 위에 올라왔습니다.
Flutter. 모든 걸 네이티브 수준으로 만들 수 있다는 점에서 매력적이었습니다. 실제로 팀 내에서도 개발이 용이하다는 평가가 나왔어요. 툴링이 압도적이고, VS Code 안에서 DevTools까지 쓸 수 있고, Hot Reload도 훌륭하다고요.
하지만 저희의 고민은 다른 곳에 있었습니다.
"Flutter에 익숙한 개발자를 지속적으로 공급받을 수 있을지가 가장 큰 문제입니다."
학생 개발 단체의 숙명이죠. 매 학기 구성원이 바뀝니다.
누군가가 졸업하면 그 자리를 새로운 신입 멤버가 채워야 하는데, Flutter 숙련자를 매번 구할 수 있을까요?
기능 구현 수준의 학습은 가능하겠지만, 코드 리뷰를 해주고 좋은 아키텍처로 설계할 수 있는 수준의 개발자는 웹에 비해 희소합니다.
거기에 한 가지 더. Flutter는 OTA(Over-The-Air) 업데이트가 공식적으로 지원되지 않습니다. Shorebird라는 서드파티 솔루션이 있긴 한데, iOS에서는 약관 위반의 회색지대에 놓여 있었어요. 전문 QA 인력이 없는 학생 단체에서 매번 앱 심사를 거쳐 릴리즈하기엔 에너지 소모가 너무 컸습니다.
Capacitor + React. 오래된 만큼 플랫폼 연동이 풍부하다는 장점이 있었지만, 팀원이 직접 PoC를 돌려본 결과 "툴링이 별로"라는 냉정한 평가가 돌아왔습니다. DevTools도 수동으로 Chrome Inspect를 써야 했고, 개발 서버 연동도 ionic 프레임워크 없이는 수동이었어요.
Tauri + React. 여기서 반전이 일어났습니다. Tauri는 상대적으로 신생이지만, HMR 지원도 잘 되고, Android Studio 설치 없이도 빌드가 가능했어요. 무엇보다 웹 기술 그대로 — React + Vite — 를 쓸 수 있다는 점이 결정적이었습니다.
그리고 결정적인 한 마디가 나왔습니다.
"nav bar는 네이티브 위젯으로 관리하고, 주 화면은 웹뷰로 관리하는 등 섞었을 때 장점이 많은 것 같습니다."
이 한 줄이 방향을 바꿨어요.
Chapter 2. "코드 섞어 쓰는 건 항상 지옥입니다"
Flutter + WebView 하이브리드라는 매력적인 아이디어가 나왔지만, 곧바로 반론이 터졌습니다.
"코드 섞어 쓰는 건 항상 지옥입니다…"
핵심 쟁점은 이거였어요. 웹뷰 안에서 Flutter가 제공하는 네이티브 UI 엘리먼트를 positioning하는 것이 기술적으로 가능한지, 그리고 가능하더라도 유지보수가 지속 가능한지.
토스급의 sleek한 UX를 구현할 수 있을지에 대한 질문도 나왔습니다. 한 팀원이 실제로 React에서 iOS 스타일의 bottom drawer를 구현해본 경험을 공유했는데 — "리액트로 거의 흑마법을 써야 합니다"라는 코멘트와 함께 말이죠.
JavaScript와 네이티브 영역을 이어주는 브릿지 함수, IPC(Inter-Process Communication) 같은 솔루션도 논의됐지만 "trivial하지 않을 것 같다"는 의견이 지배적이었습니다.
결론적으로, 논의는 이쪽으로 수렴하기 시작했습니다.
"대부분의 기능은 웹으로 충분하다. 네이티브가 필요한 영역은 극히 제한적이다."
푸시 알림, 외부 링크 열기, 결제 연동 정도. 나머지는 전부 웹으로 커버할 수 있었습니다. 그리고 웹뷰 방식은 모바일 웹과 PC를 최소 공수로 동시에 커버할 수 있다는 강력한 장점이 있었어요.
그래서 최종 방향이 정해졌습니다.
Tauri + React (WebView 기반) + 필요한 곳만 네이티브 플러그인.
Chapter 3. 프로토타입의 탄생 — "의외로 큰 난관은 없었습니다"
방향이 정해지자 속도가 붙었습니다. 우리는 PoC(Proof of Concept) 프로토타입 스펙을 정의했어요.
프로토타입에서 검증할 것들:
- 웹의 버튼으로 디바이스 손전등 켜기/끄기
- 손전등 상태를 웹뷰 내에 표시
- 외부 링크를 디바이스 기본 브라우저로 이동
- 커스텀 스킴 처리 (카카오톡, 페이북 등)
- 내부 링크는 SPA처럼 동작
Tauri로 작업을 시작했는데, 결과는 예상 밖이었습니다.
import { toggle } from "@sosweetham/tauri-plugin-torch-api";
function App() {
const [flashOn, setFlashOn] = useState(false);
useEffect(() => { toggle(flashOn) }, [flashOn]);
return (
<main className="container">
<button onClick={() => setFlashOn((f) => !f)}>
Toggle Flash ({flashOn ? "Off" : "On"})
</button>
</main>
);
}
손전등? 플러그인 하나로 해결
생체인증?
import { authenticate } from "@tauri-apps/plugin-biometric";
// ...
<button onClick={() => authenticate("몰?루")}>생체 인증</button>
이것도 해결
알림 전송까지.
import { requestPermission, sendNotification } from '@tauri-apps/plugin-notification';
// ...
<button onClick={() => sendNotification("몰?루")}>알림 전송</button>
전부 해결.
안드로이드 APK를 빌드해서 self-sign하고, Cloudflare 터널로 웹 서버를 올리고, 실제 디바이스에서 테스트까지. 이 모든 과정이 한 시간도 채 걸리지 않았습니다.
"역시 플러그인이 사기네요."
맞는 말이었어요. Tauri 플러그인 생태계가 생각보다 훨씬 풍부했습니다.
Chapter 4. iOS, 그 험난한 길
안드로이드에서의 성공에 고무되어, 다음 타겟은 iOS였습니다.
여기서부터 진짜 전쟁이 시작됐어요.
처음부터 순탄치 않았습니다. npm run tauri ios dev를 실행했더니 Xcode가 열리고, 시뮬레이터가 뜨고, 55701 포트로 뭔가 통신하기 시작하는데 — 빌드가 실패합니다. 에러 로그도 제대로 안 보여요.
우리는 Xcode에 익숙하지 않아 로그를 찾는 것부터 난관이었습니다. 결국 Slack 허들(음성 통화)을 켜고 화면공유를 하면서 실시간 디버깅에 돌입했죠.
그 뒤에 이어진 1시간 29분의 기록은, 그야말로 처절했습니다.
문제 1: Rust 에디션 호환성. Cargo가 2024 edition을 요구했는데 맞지 않았어요. rustup update로 해결.
문제 2: 네트워크. iOS 디바이스는 개발 컴퓨터와 같은 와이파이 네트워크에 있어야 했습니다. 근데 학교 와이파이가 기기 간 통신을 막고 있었어요. 안드로이드는 USB로 그냥 되는데, iOS는 와이파이가 필수.
문제 3: 디바이스 페어링 무한루프. "Device is busy"라는 메시지가 반복적으로 나타나면서 앱 설치가 안 됐습니다. Xcode에서 언페어링(unpair)을 시도하는데, 우클릭이 안 먹히고, 설정이 꼬이고…
문제 4: 1Password(Warp) 간섭. 알고 보니 개발자의 맥북에 켜져 있던 VPN 앱이 로컬 네트워크 통신을 방해하고 있었습니다. 이걸 끄자 페어링이 됐어요.
문제 5: 로컬 네트워크 퍼미션. iOS는 앱이 로컬 네트워크에 접근하려면 Info.plist에 권한을 명시해야 합니다. 이걸 설정하지 않으면 웹뷰가 로컬 개발 서버에 접근할 수 없었어요.
이 모든 문제를 하나씩 격파하고, 마침내 —
"오, 됐다, 됐다. 완벽해요. 네이티브 완전 떠요."
손전등 토글, 생체인증(Face ID), 네이티브 다이얼로그, 알림 전송. 전부 iOS 실기기에서 완벽하게 동작했습니다.
"와 미쳤는데. 이거 DX 괜찮은데요."
웹 코드를 수정하면 iOS 기기에서 즉시 반영되는 HMR(Hot Module Replacement)도 확인. 이 순간, Tauri + WebView 조합의 가능성이 완전히 검증됐습니다.
Chapter 5. 런타임에서 갈라지는 세계
프로토타입을 통해 흥미로운 발견도 있었습니다.
웹 브라우저에서 같은 URL을 열어보니, PC 크롬에서는 알림 권한 요청 팝업이 뜹니다. 네이티브 앱에서만 되는 줄 알았던 기능이 웹에서도 작동한 거죠.
이 발견이 아키텍처 결정에 중요한 영향을 미쳤습니다.
// 앱 환경인지 판별하는 한 줄
const isApp = '__TAURI__' in window;
이 한 줄이면 충분했어요. 기본적으로는 웹사이트를 개방하고, 네이티브에서만 가능한 기능(생체인증, 푸시 알림 등)은 런타임 분기로 처리하면 됩니다. 개발할 때도 그냥 브라우저에서 모바일 viewport로 세팅해두고 작업하면 돼요.
이렇게 하면:
- 모바일 웹 + 데스크톱 웹 + 네이티브 앱을 하나의 코드베이스로 커버
- 기존 웹 개발 노하우를 그대로 활용
- 신입 멤버 온보딩 비용 최소화 (React만 알면 됨)
- 대부분의 업데이트는 서버사이드(웹)에서만 하면 끝
Chapter 6. 아직 남은 전투들
프로토타입은 성공적이었지만, 프로덕션까지는 아직 갈 길이 멀어요. 우리가 앞으로 풀어야 할 과제들을 정리해봤습니다.
1. 앱스토어 배포와 심사
애플 개발자 프로그램 연회비 ₩129,000. 금액 자체보다, 앱 심사를 통과해야 한다는 게 핵심입니다. 권한 요청에 대한 명확한 사유, 사용하지 않는 권한의 제거, 심사 가이드라인 준수 등을 꼼꼼히 챙겨야 합니다. 생각보다 까다로운 영역이에요.
2. 푸시 알림 — 로컬이 아닌 리모트
프로토타입에서 구현한 건 로컬 알림이었습니다. 실제 서비스에서는 FCM(Firebase Cloud Messaging)을 통한 리모트 푸시가 필요해요. Tauri에 FCM 플러그인이 있긴 하지만, 서버 인프라 구축과 토큰 관리까지 고려해야 합니다.
3. 오프라인 대응과 PWA
Tauri 앱 안에 PWA(Progressive Web App)를 래핑하면 오프라인에서도 동작하는 앱을 만들 수 있습니다. Vite PWA 플러그인을 사용하면 캐싱과 서비스 워커 설정을 간단하게 할 수 있어요. 한 팀원의 표현을 빌리자면 "마트로슈카 인형급 래핑"이지만, 이게 OTA 업데이트 문제에 대한 가장 현실적인 해답이기도 합니다.
4. SPA 라우팅과 외부 접근
React Router로 SPA 라우팅을 구현하되, 외부에서 특정 경로로 직접 접근할 수 있어야 합니다. 해시(#) 라우팅을 쓸지, History API를 쓸지. 웹과 앱 양쪽에서 모두 자연스럽게 동작하도록 설계해야 해요.
5. 네이티브 Alert의 부재
의외의 발견이었는데, iOS 웹뷰에서는 JavaScript의 window.alert()가 먹히지 않았습니다. 대신 Tauri의 다이얼로그 플러그인을 사용해야 했어요. 이런 식으로 웹에서 당연히 되는 것들이 네이티브 래퍼 안에서는 안 되는 케이스가 곳곳에 숨어 있을 수 있습니다.
6. 지속 가능한 인력 구조
이건 기술적 과제가 아니라 조직적 과제입니다. 1학기 스터디에서 React를 배우는 신입 멤버들이, 2학기에 이 프로젝트에 자연스럽게 합류할 수 있어야 합니다. Tauri + React 조합을 선택한 가장 큰 이유가 바로 이 지점이에요.
에필로그: "그만큼 안전하다는 거죠"
iOS 디버깅 중, 허들에서 나온 대화가 있습니다.
디바이스 페어링을 할 때마다 매번 "Trust this computer?"를 물어보는 iOS에 지쳐가던 그때, 반 농담으로 던진 재원님의 한 마디...
"그만큼 안전하다는 거죠."
웃긴 말이었지만, 어떻게 보면 이번 프로토타이핑의 교훈을 관통하는 말이기도 합니다. iOS 개발이 까다로운 데에는 이유가 있고, 네이티브 앱 개발이 복잡한 데에도 이유가 있습니다.
중요한 건, 그 복잡성 앞에서 멈추지 않고 하나씩 격파해 나가는 것.
SCG는 웹 개발 단체입니다. 하지만 이번 프로젝트를 통해 우리의 웹 기술이 네이티브 영역까지 확장될 수 있다는 걸 직접 증명했어요. 하루 저녁, Slack 허들 하나, 디바이스 두 대. 그리고 끈질긴 삽질.
"iOS 유기가 마려워지는 하루였습니다 ㅋㅋㅋㅋ"
유기하진 않았습니다. 대신 정복했어요.
TL;DR
결정 사항 선택 이유
| 프레임워크 | Tauri + React | 웹 기술 재사용, 신입 멤버 온보딩 용이 |
| UI 구현 | 95% WebView + 5% 네이티브 플러그인 | 유지보수 단순화, 모바일 웹 동시 대응 |
| 업데이트 전략 | PWA 래핑으로 캐싱/OTA 해결 | 앱 심사 없이 빠른 배포 |
| 환경 분기 | '__TAURI__' in window | 하나의 코드베이스로 웹/앱 분리 |
| 라우팅 | React Router (해시 라우팅) | 정적 배포 + 외부 링크 대응 |
이 글은 SCG 내부 실제 기술 논의를 바탕으로 작성되었습니다.
실시간으로 의견을 나누고, 즉석에서 프로토타입을 만들고, 함께 삽질하는 — 이게 SCG가 문제를 푸는 방식입니다.
함께 삽질하고 싶은 분, 언제든 환영합니다!