surf-uiby COLDSURF
Screens

AppScreen

화면 한 장의 바닥 판. 배경 · 위아래 여백 · 로딩 경계.

import { AppScreen, type AppScreenOffsetBottom } from '@coldsurfers/screens/AppScreen'import { useScheme } from '@coldsurfers/surf-ui/native/scheme'import { TabBar, useTabBarHeight } from '@coldsurfers/surf-ui/native/TabBar'import { Text } from '@coldsurfers/surf-ui/native/Text'import { House, Search, Tickets, UserRound } from 'lucide-react'import { useState } from 'react'import { Pressable, View } from 'react-native'const TABS = [  { key: 'feed', label: '피드', Icon: House },  { key: 'events', label: '이벤트', Icon: Tickets },  { key: 'search', label: '검색', Icon: Search },  { key: 'my', label: '나의 창꼬', Icon: UserRound },]const OFFSETS: AppScreenOffsetBottom[] = ['tabBar', 'safeArea', 'none']export default function Example() {  const [offsetBottom, setOffsetBottom] = useState<AppScreenOffsetBottom>('tabBar')  const scheme = useScheme()  const tabBarHeight = useTabBarHeight()  return (    <View style={{ flex: 1 }}>      <AppScreen offsetBottom={offsetBottom}>        {/* 테두리가 곧 AppScreen 이 비운 자리의 경계다. */}        <View          style={{            flex: 1,            borderWidth: 1,            borderColor: scheme.actionPrimary,            backgroundColor: scheme.panelFill,            gap: 8,            padding: 12,          }}        >          <Text textStyle="labelSm" tone="muted">            offsetBottom = {offsetBottom} · useTabBarHeight() = {tabBarHeight}          </Text>          <View style={{ flexDirection: 'row', gap: 6 }}>            {OFFSETS.map((candidate) => (              <Pressable                key={candidate}                onPress={() => setOffsetBottom(candidate)}                style={{                  borderWidth: 1,                  borderRadius: 8,                  paddingHorizontal: 10,                  paddingVertical: 6,                  borderColor:                    candidate === offsetBottom ? scheme.actionPrimary : scheme.lineDivider,                  backgroundColor: candidate === offsetBottom ? scheme.statePressed : scheme.bgBase,                }}              >                <Text textStyle="labelSm">{candidate}</Text>              </Pressable>            ))}          </View>          <View style={{ flex: 1, justifyContent: 'flex-end' }}>            <Text textStyle="labelSm" tone="muted">              콘텐츠 끝 — 이 선이 탭바에 닿으면 여백이 모자란 것이다            </Text>          </View>        </View>      </AppScreen>      {/* 흐름 밖이라 자리를 안 비워 준다 — offsetBottom 이 있는 이유. */}      <View style={{ position: 'absolute', left: 0, right: 0, bottom: 0 }}>        <TabBar>          {TABS.map(({ key, label, Icon }) => (            <TabBar.Item              key={key}              label={label}              active={key === 'feed'}              renderIcon={({ color, size, strokeWidth }) => (                <Icon color={color} size={size} strokeWidth={strokeWidth} aria-hidden />              )}            />          ))}        </TabBar>      </View>    </View>  )}

offsetBottom 을 none 으로 내려 보세요. 콘텐츠 테두리가 탭바 아래로 들어갑니다 — 이 컴포넌트가 존재하는 이유가 그 한 줄입니다.

무엇을 들고 무엇을 안 드는가

여기여기가 아닌 곳
배경색 · safe-area 여백 · 탭바 여백 · Suspense 경계헤더 → react-navigation options.header
에러 경계 → 소비처
탭바 자체 → TabBar

에러 경계를 안 드는 이유는 의존입니다. react-query 경계를 넣으면 이 패키지가 react-query 를 물고, 데이터를 안 읽는 화면까지 그 의존을 집니다. 소비처가 바깥에서 감쌉니다.

여백을 두 축으로 가른다

<AppScreen offsetTop="safeArea" offsetBottom="tabBar">
  <MyList />
</AppScreen>
값무엇이 되는가
offsetTopnone · safeArea0 · insets.top
offsetBottomnone · safeArea · tabBar0 · insets.bottom · useTabBarHeight()

left · right 는 받지 않습니다. 앞판 소비처 22곳에서 한 번도 안 썼습니다 — 있는데 안 먹는 prop 은 거짓말을 합니다.

왜 불리언 둘이 아니라 열거형인가

앞판은 edges: Edges 와 withBottomTab: boolean 을 따로 받았는데 둘 다 하단을 건드렸습니다. 조합 넷이 만드는 값이 이랬습니다.

앞판paddingBottom
edges=[], withBottomTabtabBarHeight - insets.bottom
edges=['bottom'], withBottomTabtabBarHeight
edges=['bottom']insets.bottom
edges=[]0

tabBarHeight - insets.bottom 은 앞판이 탭바 높이를 상수로 박아서 생긴 보정식입니다. useTabBarHeight() 는 인셋을 포함해 계산하므로 보정이 필요 없고, 넷이 세 값으로 접힙니다.

탭바는 absolute 로 얹힌다

화면이 자기 아래 여백을 직접 잡아야 하는 이유입니다. 탭바를 흐름 안에 넣으면 offsetBottom 이 할 일이 없습니다.

<View style={{ flex: 1 }}>
  <AppScreen offsetBottom="tabBar">{children}</AppScreen>
  <View style={{ position: 'absolute', left: 0, right: 0, bottom: 0 }}>
    <TabBar>{/* … */}</TabBar>
  </View>
</View>

Suspense

fallback 없이 쓰면 정중앙 design-system Spinner 가 섭니다.

기본 fallback 은 svg 와 reanimated 를 전이로 뭅니다

Spinner 가 react-native-svg · react-native-reanimated 를 씁니다. native 레인을 쓰는 소비처는 보통 이미 들고 있지만, 안 들고 싶으면 fallback 을 직접 넘기세요.

위 미리보기의 여백은 실기기 여백이 아닙니다

브라우저에는 안전 영역이 없어 인셋이 전부 0 입니다. safeArea 두 값이 여기서는 none 과 똑같이 보이고, useTabBarHeight() 도 Platform.OS === 'web' 이라 인셋을 안 탑니다. 실기기에서는 홈 인디케이터만큼 더 큽니다 — 자로 재서 시안과 맞추면 안 됩니다.

스피너도 안 돕니다. 문서 사이트가 reanimated 를 얇은 대역으로 바꿔 두기 때문입니다.

Props

Prop

Type

그 밖의 ViewProps 속성은 루트 엘리먼트로 그대로 통과한다.

On this page