요즘 유틸리티 앱을 만들 때는 앱 안의 멋진 인터페이스만으로는 부족합니다. 사용자 유지의 진짜 격전지는 홈 화면입니다.
미니멀 수분 섭취 트래커 WaterPaw를 만들 때 네이티브 iOS 위젯은 반드시 필요한 기능이었습니다. 사용자가 홈 화면을 힐끗 보기만 해도 수분 섭취 링을 확인할 수 있어야 하니까요. 하지만 React Native / Expo 생태계에서 일해 본 분이라면 이 작업에 대한 오랜 두려움을 아실 겁니다. iOS 위젯은 네이티브 Swift 작성, 커스텀 App Extension 구성, Xcode의 코드 서명 타깃 관리가 필요하며, 이를 수동으로 하면 매끄러운 관리형 Expo 워크플로가 완전히 깨집니다.
다음은 Expo Config Plugins와 공유 로컬 우선 저장소로 네이티브 iOS 위젯을 출시할 때 사용한 깔끔한 자동화 아키텍처입니다.
아키텍처 과제: 앱과 위젯 사이의 샌드박스
WaterPaw는 로컬 우선이며 개인정보를 중시하기 때문에 데이터는 전적으로 기기에 머뭅니다. 그런데 iOS 위젯은 별도의 운영체제 백그라운드 프로세스에서 실행되므로 앱의 표준 AsyncStorage나 문서 디렉터리 파일을 읽을 수 없습니다.
클라우드 백엔드를 띄우지 않고 이 간극을 메우려면 App Group이라는 Apple 샌드박스 컨테이너를 구성해야 합니다.
[ WaterPaw React Native App ]
→ (Writes to Shared Suite)
→ [ iOS App Group Container ]
→ [ iOS Native Home Widget ] ← (Reads via Swift)1단계: React Native에서 데이터 쓰기
사용자가 JavaScript 계층에서 물 섭취를 기록할 때마다 그 데이터를 공유 App Group에 넣어야 합니다. 네이티브 브리지 바인딩은 react-native-shared-group-preferences로 처리할 수 있습니다.
import SharedGroupPreferences from 'react-native-shared-group-preferences';
const APP_GROUP = 'group.com.yourgroupname';
export const syncDataToWidget = async (currentMl: number, goalMl: number) => {
const dataForWidget = {
progress: currentMl / goalMl,
displayString: `${currentMl} / ${goalMl} ml`,
updatedAt: new Date().toISOString()
};
try {
await SharedGroupPreferences.setItem('WidgetData', dataForWidget, APP_GROUP);
} catch (error) {
console.error('Failed to sync data to iOS App Group Suite', error);
}
};2단계: Swift에서 공유 데이터 사용하기
네이티브 Swift 위젯 디렉터리 안에서 UserDefaults(suiteName:)를 사용해 동일한 App Group 스위트에서 즉시 읽을 수 있습니다. 모든 것이 로컬에 있고, 즉각적이며, 100% 비공개로 유지됩니다.
import WidgetKit
import SwiftUI
struct HydrationEntry: TimelineEntry {
let date: Date
let progress: Double
let displayString: String
}
struct WaterPawWidgetEntryView: View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("WaterPaw")
.font(.caption)
.foregroundColor(.secondary)
Text(entry.displayString)
.font(.title2)
.bold()
ProgressView(value: entry.progress)
.progressViewStyle(LinearProgressViewStyle(tint: .blue))
}
.padding()
}
}3단계: Expo Config Plugins로 Xcode 자동화하기 🛠️
개발자가 흔히 어려움을 겪는 부분입니다. npx expo prebuild를 실행하면 Expo는 위젯을 위해 수동으로 설정한 Xcode 수정 사항이나 타깃을 모두 지워 버립니다.
완전한 관리형 워크플로를 유지하려면 Swift 코드를 독립된 targets/ 디렉터리에 분리하고 Config Plugin(예: expo-target-extension)으로 네이티브 조립을 자동화합니다. 그러면 Expo가 매번 위젯 타깃을 자동으로 만들고, Swift 컴파일 단계를 연결하며, App Group 권한을 주입합니다.
{
"expo": {
"name": "WaterPaw",
"slug": "waterpaw",
"plugins": [
[
"expo-target-extension",
{
"targetName": "WaterPawWidget",
"dir": "targets/widgets",
"entitlements": {
"com.apple.security.application-groups": ["group.com.yourgroupname"]
}
}
]
]
}
}이 설정을 app.json에 넣으면 ios/ 디렉터리를 지우고 npx expo prebuild --clean을 실행하는 것만으로 네이티브 Xcode 환경이 바로 완전하게 구성됩니다.
결론: Eject하지 말고 자동화하라
네이티브 확장 워크플로를 모두 Config Plugins로 옮기면 모바일 프로젝트가 깔끔하고 미래에도 안전하게 유지됩니다. Expo의 빠른 반복 주기를 유지하면서 사용자에게는 깊이 통합된 지연 없는 iOS 시스템 기능을 제공할 수 있습니다.
유틸리티를 만들 때 데이터를 홈 화면에서 한눈에 볼 수 있게 하려는 추가 노력은 장기적인 참여도에서 크게 보답받습니다. 저는 이 아키텍처로 WaterPaw의 한눈에 보는 진행 링과 마스코트 추적 기능을 구현했습니다.
