Apple의 폴더블 iPhone Duo는 시스템 크롬과 세이프 에어리어의 작동 방식을 바꿉니다. Expo가 실용 가이드를 게시했습니다 SDK 58의 React Native 앱을 위한 글입니다. 이 글은 Capacitor에 대응합니다: 하나의 WebView, CSS env insets, Capacitor 코어 플러그인, 그리고 선택적 네이티브 바를 사용합니다. @capgo/capacitor-native-navigation.
iPhone Duo에서 달라지는 점
많은 포즈에서 iOS는 상태 표시줄, 내비게이션 바, 탭 바를 세로 사이드 레일 leading 또는 trailing 가장자리로 이동시킵니다. 가로 바는 세로 모드의 펼쳐진 내부 디스플레이와 클래식 폰 레이아웃에서 여전히 나타납니다.
위 이미지는 펼쳐진 디스플레이입니다. 가로 모드에서는 툴바와 탭 바가 레일에 쌓입니다. 세로 모드에서는 툴바가 다시 상단으로, 탭 바가 다시 하단으로 돌아갑니다.
Capacitor WebView 안의 웹 콘텐츠에서 중요한 변화는 다음과 같습니다:
env(safe-area-inset-top)가능합니다0상태 표시줄이 노치 아래가 아니라 레일에 있을 때.- leading 및 trailing 인셋이 커집니다 (Apple 문서에 따르면 가로 모드와 접힌 포즈에서 레일은 약 84pt입니다.) 왼쪽과 오른쪽은 서로 대칭이 아닙니다.
- 윈도우 크기 변경 사용자가 접기, 펼치기, 회전 또는 내장 디스플레이에서 Split View를 사용할 때. 캐시된 레이아웃 값은 빠르게 구식이 됩니다.
- 네이티브 탭 바가 레일을 따릅니다 사용할 때
UITabBar(또는 시스템 탭 바를 사용한 Capgo 네이티브 내비게이션). 커스텀 고정 HTML 탭 바는 해당되지 않습니다.
접힌 외부 디스플레이를 컴팩트 폭 으로, 펼친 내부 디스플레이를 레귤러 폭으로 취급하세요. 다른 앱 셸로 전환하지 않고 레이아웃을 넓히세요.

시스템 바 위치: 기기가 접혀 있으면 오른쪽 레일, Split View에서는 각 창 외부 가장자리, 세로로 펼친 디스플레이에서는 다시 가로 바.
요구 사항
- Capacitor 8 켜짐 iOS 27 Xcode 27.1(iPhone Duo 시뮬레이터 런타임 포함).
- Device Hub (Xcode > Open Developer Tool > Device Hub)에서 Duo 시뮬레이터를 접고, 힌지를 조절하고, 회전할 수 있습니다.
- 어떤 viewport safe area를 사용하도록 설정한 경우(아래 참조).
viewport-fit=cover 및 CSS safe area
Safe area env 변수는 viewport가 전체 디스플레이를 덮을 때만 적용됩니다:
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
여백 인터랙티브 가장자리별 콘텐츠. 몰입감을 원할 때는 배경이 화면 끝까지 이어지게 하세요.
다음을 선택하세요: 하나의 앱 전체의 inset owner입니다. 아래 두 스니펫은 대안이며, 함께 조합할 쌍이 아닙니다.
옵션 A: 페이지별 셸
.page {
min-height: 100dvh;
padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
}
Option A를 사용한다면 Option B는 건너뛰세요.
스크롤 뷰에서는 콘텐츠를 inset하세요, 외부 스크롤 컨테이너뿐 아니라, 배경은 전체 너비로 스크롤되면서 텍스트와 버튼은 레일과 겹치지 않도록.
사용 네 개의 독립적인 값. 가정하지 마세요 padding-left 같음 padding-right. RTL에서는 레일이 카메라 쪽에 있으므로, 두 가지 left 및 right env 값 모두 읽으세요.
놓치기 쉬운 두 곳:
- 이미 패딩이 적용된 페이지 안의 가로 스크롤러도 자체 끝 패딩이 필요합니다. 그렇지 않으면 마지막 카드가 레일 아래로 들어갑니다.
- 설정된 플로팅 버튼은
right: 16px레일을 무시합니다. 실시간 왼쪽 또는 오른쪽 inset을 추가하고, 창 크기가 바뀔 때마다 다시 계산하세요.
Tailwind 앱의 경우, @capgo/tailwind-capacitor 는 제공하며, pt-safe, px-safe매번 env()를 반복하는 대신 관련 유틸리티도 제공합니다.
Capacitor System Bars (Android의 safe area와 iOS 설정)
Capacitor 8 버전에는 시스템 바 에서 @capacitor/core가 포함되어 있습니다. 기존의 Android 전용 adjustMarginsForEdgeToEdge 플래그를 대체합니다. 다음에서 설정하세요: capacitor.config.ts:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
plugins: {
SystemBars: {
insetsHandling: 'css',
},
},
};
export default config;
insetsHandling: 'css'(Android 기본값): 다음 경우 CSS 변수를 주입합니다:--safe-area-inset-*WebView env 값이 잘못된 경우(이전 Android WebView 빌드).- 옵션 B(사이트 전체 셸)는 폴백 패턴 에서 System Bars 문서. Option B를 사용하는 경우 Option A는 건너뛰세요.
html {
padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
}
런타임에서 바를 스타일링하거나 숨길 수 있습니다 SystemBars.setStyle, SystemBars.show, 그리고 SystemBars.hide 과 @capacitor/core. 별도의 @capacitor/safe-area 패키지가 없습니다: Capacitor 앱의 safe area는 CSS env, Android의 System Bars 변수, 그리고 직접 등록한 리스너를 resize / visualViewport.
@capacitor/status-bar
이 Status Bar 플러그인 iOS에서 오버레이와 스타일을 여전히 제어합니다:
import { StatusBar, Style } from '@capacitor/status-bar';
await StatusBar.setOverlaysWebView({ overlay: true });
await StatusBar.setStyle({ style: Style.Dark });
setOverlaysWebView({ overlay: true }) HTML이 상태 표시줄 영역 아래에 그려지도록 합니다. 콘텐츠가 잘리지 않도록 env padding과 함께 사용하세요. iPhone Duo에서도 overlay 모드가 적용되며, 지오메트리 어느 inset이 상단이고 어느 것이 측면인지는 Status Bar API만이 아니라 시스템 safe area에서 결정됩니다.
중복 적용하지 마세요 WebView 자동 마진, 전체 페이지 env padding, 그리고 네이티브 바 인셋 변수 명시적인 전략 없이 사용할 때입니다. 이 패턴은 모든 iPhone에서 이중 간격을 유발하며, 레일이 인셋을 측면으로 이동하면 더 악화됩니다.
iOS WebView contentInset
다음에서 capacitor.config.ts, ios.contentInset WebView 스크롤 뷰의 contentInsetAdjustmentBehavior을(를) 사용합니다. 많은 앱이 'never' CSS에서 인셋을 처리할 때 사용합니다. UIKit의 자동 조정에 의존한다면 및 CSS env 패딩, 단일 진실 공급원을 선택하세요.
자세한 내용은 Capacitor iOS 설정.
폴딩/언폴딩 시 WebView 리사이징
Capacitor는 전체 화면 WebView 하나를 유지합니다. Duo의 포즈가 변경되면:
- UIKit이 뷰 컨트롤러의 safe area insets를 업데이트합니다.
- WebView 레이아웃 패스가 새 너비와 높이로 실행됩니다.
- CSS
env(safe-area-inset-*)및100dvh/visualViewport값은 다음 스타일 flush에서 업데이트됩니다.
JavaScript에서 geometry 변경을 감지하세요 inset에 의존하는 위치를 캐시할 때(플로팅 버튼, toast, canvas 오버레이):
function onGeometryChange() {
const vv = window.visualViewport;
const width = vv?.width ?? window.innerWidth;
const height = vv?.height ?? window.innerHeight;
const offsetLeft = vv?.offsetLeft ?? 0;
const offsetTop = vv?.offsetTop ?? 0;
document.documentElement.style.setProperty('--app-vv-width', `${width}px`);
document.documentElement.style.setProperty('--app-vv-height', `${height}px`);
document.documentElement.style.setProperty('--app-vv-offset-left', `${offsetLeft}px`);
document.documentElement.style.setProperty('--app-vv-offset-top', `${offsetTop}px`);
}
window.addEventListener('resize', onGeometryChange);
window.visualViewport?.addEventListener('resize', onGeometryChange);
window.visualViewport?.addEventListener('scroll', onGeometryChange);
onGeometryChange();
scroll listener는 너비나 높이를 조정하지 않고 발생하는 offsetLeft / offsetTop visual-viewport 팬을 처리합니다(예: iOS에서 키보드로 인한 뷰포트 이동).
Android에서는 다음을 포함하세요 density 에서 android:configChanges 메인 액티비티에 적용해 폴더블 기기와 디스플레이 크기 변경으로 WebView가 파괴되지 않도록 하세요. Capacitor 8 템플릿에서 이 내용을 문서화합니다 Capacitor 8 업그레이드 가이드.
화면 방향 및 window size classes
사용 live 레이아웃 폭에 맞춰 분기하지 말고 screen.width 시작 시 한 번만 캡처됩니다:
const compactQuery = window.matchMedia('(max-width: 600px)');
function readCompactWidth() {
return compactQuery.matches;
}
compactQuery.addEventListener('change', () => {
document.documentElement.dataset.widthClass = readCompactWidth() ? 'compact' : 'regular';
});
document.documentElement.dataset.widthClass = readCompactWidth() ? 'compact' : 'regular';
CSS에서는 고정된 minmax() 보다 flex와 grid를 100vw 폭을 우선하세요. 100vw 스크롤바와 레일 비대칭을 무시해 가로 오버플로를 유발합니다.
내부 디스플레이의 Split View에서는 각 창이 좁을 수 있습니다(Apple은 약 469pt 폭을 언급). 앱을 왼쪽과 오른쪽에 도킹하여 테스트하세요: 레일은 창 끝에 붙으므로 창 사이에서 가로 인셋이 반대쪽으로 바뀔 수 있습니다.
커스텀 HTML chrome vs 네이티브 바
Expo 가이드는 네이티브 Stack 및 NativeTabs 즉, iOS가 레일 배치를 담당합니다. 동일한 개념이 Capacitor에도 적용됩니다:
| 접근 방식 | iPhone Duo 동작 |
|---|---|
| 고정 HTML 헤더 + 하단 탭 | 클래식 상단/하단 안전 영역에 유지됩니다. 레일로 이동하지 않습니다. |
@capgo/capacitor-native-navigation 시스템과 함께 UITabBar / UINavigationBar |
바는 시스템 배치(수직 레일 포함)를 따릅니다. |
HTML 크롬을 유지해야 한다면 모든 화면에서 좌우 env 패딩을 확인하세요. 상단/하단만으로는 부족합니다.

고정된 HTML 크롬은 레일을 따르지 않습니다. 상단 및 하단 안전 영역에 유지됩니다. UIKit 또는 @capgo/capacitor-native-navigation에 의해 그려지는 네이티브 바는 시스템과 함께 이동합니다.
iPhone Duo에서의 @capgo/capacitor-native-navigation
네이티브 내비게이션은 실제 UINavigationBar 및 UITabBar (또는 Android의 Material 바)를 하나의 Capacitor WebView 위에 그립니다. iPhone Duo를 사용하는 iOS 27에서 접힌 상태와 많은 가로 모드 포즈에서 시스템 탭 바가 측면 레일로 이동합니다. 플러그인의 곡선 curve 탭 바 모양은 하단 가장자리에 유지됩니다. 창이 넓어지는 동안 중앙 액션은 원형 크래들을 유지합니다.

왼쪽은 레일에 있는 시스템 탭 바입니다. 오른쪽은 하단에 유지되는 곡선 탭 바와 크래들을 유지하는 중앙 액션입니다.
한 번만 설정
import { NativeNavigation } from '@capgo/capacitor-native-navigation';
await NativeNavigation.configure({
contentInsetMode: 'css',
});
contentInsetMode: 'css' 바와 safe overlap 크기를 document root에 기록합니다:
--cap-native-navigation-top--cap-native-navigation-right--cap-native-navigation-bottom--cap-native-navigation-left--cap-native-navbar-height--cap-native-tabbar-height--cap-native-system-tabbar-height(기본 tab bar 높이는tabbarHeight이며0호스팅 시스템 바의 경우)
Native Navigation이 바를 관리할 경우, 아래 플러그인 변수로 패딩을 적용하세요. 바가 활성화되면 네이티브 바 오버랩과 UIKit safe-area 가장자리가 이미 포함되어 있습니다. 사용 max() env 폴백과 함께 사용하여 변수가 0 (예: 호스팅 시스템 tab bar)일 때에도 추가 플러그인과 env 패딩을 함께 사용하지 마세요.
main {
padding-top: max(var(--cap-native-navigation-top, 0px), env(safe-area-inset-top, 0px));
padding-right: max(var(--cap-native-navigation-right, 0px), env(safe-area-inset-right, 0px));
padding-bottom: max(var(--cap-native-navigation-bottom, 0px), env(safe-area-inset-bottom, 0px));
padding-left: max(var(--cap-native-navigation-left, 0px), env(safe-area-inset-left, 0px));
}
같은 main element.
iOS 26+에서 system-hosted 탭 바 tabbarHeight 플러그인 insets는 0 웹 콘텐츠가 두 번 들어올리지 않도록 합니다. 사용 --cap-native-system-tabbar-height 레일 위 플로팅 액션에 여백이 필요할 때.
safeAreaChanged
폴드, 레일 사이드, 또는 바 가시성이 변경될 때 구독:
await NativeNavigation.addListener('safeAreaChanged', ({ insets }) => {
console.log(insets.top, insets.right, insets.bottom, insets.left);
console.log(insets.navbarHeight, insets.tabbarHeight, insets.systemTabbarHeight);
});
플러그인은 또한 capNativeNavigation:safeAreaChanged 켜짐 window 동일한 insets payload.
네이티브 code는 작은 상단 간격이 safeInsets.top 0일 때 (레일에 있는 상태 바)로, 내비게이션 버튼이 둥근 디스플레이 모서리에 닿지 않도록 합니다. 웹 패딩은 여전히 레일을 위해 env left/right를 사용해야 합니다.
setNavbar, setTabbar, 그리고 configure 모두 반환합니다 { insets } Chrome을 업데이트한 후 일회성 읽기가 필요할 때.
전체 API 참고 자료: Native Navigation 문서.
테스트 체크리스트
iPhone Duo 시뮬레이터가 있는 Device Hub에서:
- 접힌 상태: 컨트롤이 레일 아래에 없고, 탭이 등록되며, 예상되는 위치에서 가로 env 인셋이 0이 아닙니다.
- 세로 모드 열기: 상단/하단 바가 iOS가 배치하는 위치에 유지되며, 레이아웃이 다른 정보 구조로 점프하지 않습니다.
- 가로 모드 / 레일 열기: 콘텐츠가 측면 레일을 피하고, 플로팅 액션은
insets.right또는insets.left을(를) 사용하며 하드코딩된16px. - 부분 접기: 힌지 분할 영역에 중요한 UI를 배치하지 않으며, 모달과 시트가 사용 가능합니다.
- 분할 보기: 앱을 좌우 창에서 실행하며, 레일 가장자리가 각 창을 따릅니다.
- 화면이 열린 상태에서 접기: 라우트 상태, 스크롤 위치, 폼 입력이 전환 후에도 유지됩니다.
- 클래식 iPhone 시뮬레이터: 회귀 오류 없음.
에이전트 프롬프트 (복사하여 붙여넣기)
Claude Code, Cursor, Codex 또는 다른 코딩 에이전트로 Capacitor 리포지토리를 작업할 때 이 블록을 사용하세요:
Task: Make this Capacitor app safe for iPhone Duo (iOS 27 foldable).
Context:
- Read https://capgo.app/blog/capacitor-iphone-duo-safe-area-guide/ and Expo's https://expo.dev/blog/supporting-iphone-duo for platform behavior.
- One WebView. Use viewport-fit=cover and per-edge env(safe-area-inset-*) padding for interactive content; backgrounds may ignore safe areas.
- Capacitor 8: configure plugins.SystemBars (insetsHandling css). Set viewport-fit=cover in index.html. Use var(--safe-area-inset-*, env(...)) fallbacks on Android.
- @capacitor/status-bar: only if the app already uses it; setOverlaysWebView must match CSS padding strategy.
- Do not double-apply Capacitor margin adjustment, full-page env padding, and native navigation CSS variables.
- On resize and visualViewport resize, recompute any cached inset or FAB positions.
- If the app uses @capgo/capacitor-native-navigation: contentInsetMode css, listen for safeAreaChanged, pad with max(var(--cap-native-navigation-*, 0px), env(safe-area-inset-*, 0px)) per edge (not sum); verify system tab bar rail poses in the iOS 27 Duo simulator.
- If the app uses custom HTML headers or tab bars, add left/right safe padding on every screen and document that bars will not move into the rail.
Deliverables:
1. List files changed and the inset strategy (single source of truth).
2. Replace fixed top-only safe padding with four-edge padding where needed.
3. Add or update capacitor.config and index.html viewport meta if missing.
4. Note manual test poses: folded, open portrait, open landscape rail, Split View left/right.
Constraints:
- Only use APIs that exist in @capacitor/core, @capacitor/status-bar, and @capgo/capacitor-native-navigation definitions.
- No em dashes or en dashes in user-facing strings you add.
설치 Capgo 스킬 에이전트가 Native Navigation을 어떻게 연결하는지 알 수 있도록: npx skills add Cap-go/capgo-skills --skill capacitor-plugins.