본문으로 건너뛰기

ZSScreen

화면 하나의 루트 컨테이너입니다. safe area·스크롤·키보드 대응·넓은 창 폭을 한 번에 처리하므로 SafeAreaView·KeyboardAvoidingView·ScrollView를 따로 조합하지 않습니다. 같은 화면 안 2-pane은 ZSPaneLayout, 키보드 바로 위에 붙는 버튼·툴바는 ZSKeyboardStickyView를 씁니다.

기본 사용법​

import { ZSButton, ZSScreen, ZSText } from '@0610studio/zs-ui';

function EditScreen() {
return (
<ZSScreen bottomComponent={<ZSButton title="저장" fullWidth onPress={save} />}>
<ZSText variant="h2" emphasized>제목</ZSText>
<ZSText>본문 내용</ZSText>
</ZSScreen>
);
}

LayoutProvider 아래에서 동작합니다(창 크기 클래스·가독 폭).

Props — ZSScreenProps​

PropTypeDefault설명
backgroundColorColorName | string콘텐츠 style의 배경 → 'background'화면 표면색 — 색 역할 이름 또는 직접 지정한 색. safe area 영역까지 같은 색으로 칠합니다
edgesArray<'top' | 'right' | 'bottom' | 'left'>영역별 자동지정하면 그 방향만 safe area 패딩으로 처리합니다 — Safe area
maxContentWidth'readable' | number | 'none''none'넓은 창에서 콘텐츠·topComponent·bottomComponent의 최대 폭 — 넓은 창 폭
scrollEnabledbooleantruefalse면 ScrollView 없이 고정 View로 그립니다(키보드 자동 스크롤·refreshControl·bounces도 꺼짐)
refreshControlReactElement<RefreshControlProps>undefined당겨서 새로고침 — 아래
bouncesbooleanrefreshControl이 있으면 true, 없으면 false스크롤 끝에서 튕김(iOS). Android는 오버스크롤 효과(overScrollMode)를 같은 값으로 켜고 끕니다
topComponentReactNodeundefined스크롤 밖 상단에 고정하는 영역
bottomComponentReactNodeundefined스크롤 밖 하단에 고정하는 영역. 배경은 backgroundColor와 같게 둡니다 — Safe area
bottomComponentPlacement'full' | 'start' | 'end''full'넓은 창에서 bottomComponent를 놓을 자리 — 전폭, 시작 쪽 절반, 끝 쪽 절반. compact 창은 늘 전폭입니다 — 아래 영역 위치
showsVerticalScrollIndicatorbooleantrue세로 스크롤 인디케이터 표시
scrollToFocusedInputbooleantrue키보드가 포커스 입력을 가리면 가린 만큼 스크롤 — 키보드 자동 스크롤
keyboardScrollExtraOffsetnumber30포커스 입력 하단과 키보드 상단 사이 여유(pt·dp). 키보드 위 CTA가 있으면 그 높이를 더합니다
onScroll(event: NativeSyntheticEvent<NativeScrollEvent>) => voidundefined스크롤 이벤트 핸들러
scrollEventThrottlenumber16스크롤 이벤트 스로틀(ms)
...propsViewPropsundefined콘텐츠 뷰에 적용하는 View 속성(style·testID·접근성 등)

ZSScreenProps·ZSScreenRef·ZSScreenBottomPlacement 타입을 내보냅니다. ref(ZSScreenRef)는 안쪽 ScrollView입니다 — ref.current?.scrollTo({ y: 0, animated: true }). scrollEnabled={false}면 ScrollView를 그리지 않아 null입니다.

Safe area​

edges를 지정하지 않으면 영역마다 알맞게 처리합니다.

영역iOSAndroid
좌·우safe area 패딩(가로 회전 시 측면 Dynamic Island·cutout 회피)safe area 패딩
스크롤 상·하UIKit 자동 조정 — 네이티브 헤더·탭바 아래로 콘텐츠가 흐름상·하 inset 패딩
고정 영역(bottomComponent·scrollEnabled={false})하단 inset하단 inset
투명 헤더 아래(Stack 기본)topComponent·고정 영역은 safe area(내비게이션 바 포함)만큼 비킴, 스크롤은 위와 같음헤더 높이만큼 콘텐츠를 내리고, 스크롤되면 헤더 영역에 짙은 흐림 막(96%)과 아래 0.5 선을 깜(ZSHeaderBackground)
  • inset은 창이 아니라 컨테이너 위치 기준입니다. 헤더 아래 컨테이너는 top이 0, 탭바 위 컨테이너는 bottom이 그만큼 줄어듭니다. 컨테이너 안의 useSafeAreaInsets()도 같은 값을 받습니다.
  • 콘텐츠의 flex: 1 자식은 inset 안쪽 남은 높이를 채웁니다.
  • 헤더가 없는 화면은 top inset이 0일 수 있으니(아이폰 듀오, iPhone 가로) 맨 위에 디자인 여백을 두세요.
  • 하단 inset(홈 인디케이터)과 좌·우 inset(아이폰 듀오 레일, 가로 Dynamic Island)은 표면색(backgroundColor)으로 칠합니다. bottomComponent에 다른 배경색을 주면 그 둘레에 띠가 생기므로 표면색과 같게 둡니다.
<ZSScreen edges={['top', 'bottom']}>{/* 상·하만 직접 지정 */}</ZSScreen>
<ZSScreen edges={[]}>{/* safe area 없음(엣지 투 엣지) */}</ZSScreen>

넓은 창 폭​

기본은 전폭입니다 — 창 폭은 기기마다 다르므로 여백만 고정합니다. 글이 길게 이어지거나 이름·값이 한 줄에 나란한 화면은 아주 넓은 창(iPad 가로 등)에서 "readable"이나 숫자를 주세요.

<ZSScreen>{/* 기본: 전폭 */}</ZSScreen>
<ZSScreen maxContentWidth="readable">{/* LayoutProvider readableMaxWidth — iOS readableContentGuide, Android 640 */}</ZSScreen>
<ZSScreen maxContentWidth={600}>{/* 두 플랫폼 모두 600 */}</ZSScreen>
  • 넓은 창(medium·expanded)에만 적용하고 compact 창은 전폭입니다.
  • topComponent·bottomComponent도 본문과 같은 열에 놓입니다.
  • 스크롤 뷰 자체는 전폭이라 측면 여백에서도 스크롤되고, 인디케이터는 창 가장자리에 붙습니다.
  • iOS readableContentGuide는 기기에 따라 창 전체 폭이 되기도 합니다. 두 플랫폼을 같게 하려면 숫자를 주세요.

아래 영역 위치​

넓은 창을 두 칸(목록 | 상세)으로 나눈 화면에서 CTA가 한 칸에만 해당하면 bottomComponentPlacement로 그 칸 아래에만 둡니다. 다른 칸은 화면 아래 끝까지 씁니다.

<ZSScreen scrollEnabled={false} bottomComponent={<WriteButton />} bottomComponentPlacement="start">
<View style={{ flex: 1, flexDirection: 'row' }}>
<ListPane /> {/* 아래에 CTA가 겹친다 — useScreenBottomInset() 만큼 끝 여백 */}
<DetailPane /> {/* 아래 끝까지 */}
</View>
</ZSScreen>
값넓은 창(medium·expanded)compact 창
'full'본문 아래 전폭본문 아래 전폭
'start'본문 위 바닥, 시작 쪽 절반 폭(RTL이면 오른쪽)본문 아래 전폭
'end'본문 위 바닥, 끝 쪽 절반 폭본문 아래 전폭
  • 절반 폭은 화면(또는 maxContentWidth 열)의 50%입니다. 칸 사이 간격·화면 여백은 bottomComponent 안의 패딩으로 칸 가장자리에 맞춥니다.
  • start·end면 본문이 CTA 아래로 흐릅니다. 스크롤 본문은 CTA 높이만큼 아래 여백을 자동으로 더하고, 칸마다 따로 스크롤하는 화면은 겹치는 칸에서 useScreenBottomInset()(CTA 높이, 전폭이면 0)을 아래 여백으로 씁니다. 훅은 ZSScreen 자식 안에서 부릅니다.
  • 아래 safe area(홈 인디케이터)는 두 칸 모두 비웁니다. 스낵바는 CTA 윗변 위에 뜹니다.

키보드 자동 스크롤​

scrollToFocusedInput(기본 켬)이면 포커스한 입력이 키보드에 가릴 때만 가린 만큼 스크롤합니다. 탭·다음 키·ref.focus() 모두 같은 규칙입니다. Android에서 키보드가 떠 있는 동안 포커스를 옮길 때는 ZSTextField만 따라 스크롤합니다. 이 컨테이너 안의 입력만 대상이고, 시트·모달 위 입력은 스크롤하지 않습니다.

<ZSScreen keyboardScrollExtraOffset={50}>
<ZSTextField label="이름" value={name} onChangeText={setName} />
<ZSTextField label="이메일" value={email} onChangeText={setEmail} />
</ZSScreen>

키보드 위에 CTA를 붙이는 화면은 ZSKeyboardStickyView를 보세요.

당겨서 새로고침​

React Native RefreshControl을 refreshControl로 넘깁니다. refreshing은 제어 값이라 onRefresh에서 켜고, 불러오기가 끝나면 끕니다.

const { colors } = useTheme();
const [refreshing, setRefreshing] = useState(false);
const refresh = async () => {
setRefreshing(true);
await reload();
setRefreshing(false);
};

<ZSScreen
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={refresh}
tintColor={colors.primary}
colors={[colors.primary]}
progressBackgroundColor={colors.surface3}
/>
}
>
<ZSText>목록</ZSText>
</ZSScreen>
  • 스피너는 시스템이 그려 테마를 따르지 않습니다. 위 예제처럼 iOS tintColor, Android colors·progressBackgroundColor에 테마 색을 넘깁니다.
  • 투명 헤더 아래에서는 스피너가 헤더 아래에서 나옵니다. edges를 직접 지정한 화면은 Android에서 useHeaderInset() 값을 progressViewOffset으로 넘깁니다.

상태바​

상태바 아이콘은 테마를 따릅니다. 시스템과 다른 수동 테마도 같습니다. 방법과 화면별로 바꾸는 법은 플랫폼과 iOS Info.plist UIViewControllerBasedStatusBarAppearance 값에 따라 다릅니다.

플랫폼·설정테마를 따르는 방법화면마다 바꾸기
iOS true (Expo SDK 58.0.23 이상 템플릿 기본값)useStackTheme()이 테마 모드의 statusBarStyle을 넣습니다Stack 화면 옵션 statusBarStyle: 'light' 등
iOS false (그 전 템플릿)ThemeProvider의 Appearance 동기화Xcode 27(iOS 27 SDK)로 빌드하면 바꿀 수 없습니다. Stack statusBarStyle은 레드 박스가 납니다
AndroidThemeProviderexpo-status-bar의 <StatusBar style="light" /> 등
  • iOS true에서는 Stack screenOptions에 useStackTheme()을 넣어야 상태바가 테마를 따릅니다. 넣지 않으면 앱 테마가 시스템과 다를 때 상태바가 시스템 모드로 남습니다(다크 테마에 검은 글자).
  • iOS에서 expo-status-bar·React Native StatusBar는 expo-router Stack 화면에서 효과가 없습니다. Stack 화면이 있으면 react-native-screens가 화면별 statusBarStyle로 상태바를 정하기 때문입니다.
  • 값은 prebuild 때 Info.plist에 들어갑니다. 바꾸려면 app.json의 ios.infoPlist.UIViewControllerBasedStatusBarAppearance를 정하고 다시 prebuild합니다. 그 전 템플릿으로 만든 ios 폴더는 false로 남아 있습니다.

iOS에서 상태바를 탭하면 스크롤이 맨 위로 갑니다. 시트·진행 표시가 화면을 덮고 있는 동안에는 가려진 화면이 반응하지 않습니다 — 오버레이 위쪽 버튼을 누를 때 가려진 화면까지 맨 위로 올라가지 않게 하려는 것입니다. 컨테이너 밖에서 직접 쓰는 ScrollView·FlatList에는 적용되지 않습니다.

관련 문서​