Skip to Content
Native App크로스플랫폼 UI (native 컴포넌트 · 스타일링)

크로스플랫폼 UI (native 컴포넌트 · 스타일링)

packages/ui(@howmuchhome-web/ui)는 web/dashboard 와 native(Expo) 앱이 공유하는 디자인 시스템입니다. web 컴포넌트는 DOM·Tailwind 를 전제로 하므로 RN 에서 그대로 동작하지 않아, 컴포넌트별로 native 구현(*.native.tsx)을 따로 두고 공유 로직(*.shared.ts)만 재사용합니다. 이 문서는 그 아키텍처와 NativeWind v5 스타일링 실전을 다룹니다.

관련 문서:

진입점: web 은 /v2, native 는 /native

// web / dashboard import {Button} from '@howmuchhome-web/ui/v2'; // native (Expo) import {Button, Form} from '@howmuchhome-web/ui/native';

native 진입점은 @howmuchhome-web/ui/native 단일 배럴(packages/ui/native/index.ts)이며, 여기에 native 구현만 모읍니다(native 는 전부 v2).

왜 같은 /v2 경로로 자동 분기하지 않나? 이상적으로는 import {Button} from '@howmuchhome-web/ui/v2' 한 줄이 web/native 에서 알아서 갈리는 것이지만, 그러려면 컴포넌트마다 package exportsreact-native 조건을 걸어야 하고 배럴(export *)에는 조건 분기를 적용할 수 없습니다. 그래서 native 전용 진입점을 명시적으로 분리했습니다.

@howmuchhome-web/ui/v2(web 컴포넌트)를 native 코드에서 직접 import 하지 마세요 — DOM/Tailwind 를 전제로 하므로 RN 에서 깨집니다. 반드시 /native 배럴을 사용하고, 없으면 native 구현을 추가합니다.

컴포넌트 분리 규약 (.tsx / .native.tsx / .shared.ts)

파일역할
Foo.tsxweb 구현 (DOM/Tailwind)
Foo.native.tsxnative 구현 (RN + NativeWind)
Foo.shared.tsweb/native 공유 로직·상수 (플랫폼 중립, RN/DOM import 금지)
  • *.shared.tsDOM 도 RN 도 import 하지 않습니다. 색·사이즈 매핑, 상태 계산 등 순수 로직만 담고 web Foo.tsx 와 native Foo.native.tsx 가 함께 import 합니다.
  • 예: Button.tsx(web) / Button.native.tsx(native) / Button.shared.ts(공유). web Button 도 공유 가능한 값(overlay 색, 아이콘 크기, 반경)은 Button.shared 를 참조합니다.
  • Metro 는 .native.tsx 를 자동 우선 해석하고, web 번들러는 .tsx 를 씁니다. 배럴에서는 확장자 없이 import 하면 각 플랫폼이 맞는 파일을 고릅니다.

packages/ui*.native.tsx 는 파일 맨 위에 /// <reference types="react-native-css/types" /> 가 필요합니다(className 타입). apps/native-app/src 화면 파일은 nativewind-env.d.ts 전역 참조로 커버되지만, packages/ui 는 자체 tsconfig 에 전역 참조가 없어 파일별로 붙여야 합니다. 자세한 배경: 컨벤션 §1.

현재 native 구현이 있는 컴포넌트: Button, IconButton, Form, Interaction.Overlay, Scaffold, AppBar, Divider, MenuList, CTAGroup, Alert/Confirm(+Modal/Backdrop), BottomSheet/SideDrawer(+useOverlay), TextField, PhoneTextField, OTPInput, Reveal/RevealContainer(+useReveal), SpinnerLoader, Tag, DataViewCard, useToast(+ToastHost).

컴포넌트별 설계 메모

  • PhoneTextFieldphoneFormatter(공유 순수 함수) 재사용. value/onChangeText 는 하이픈 제거 숫자(raw), 표시는 포맷.
  • OTPInput — web 과 동일하게 자리수만큼 TextField 셀 + ref 포커스 이동(TextField.native 가 ref 를 내부 TextInput 으로 forward). SMS 자동완성(iOS oneTimeCode / Android sms-otp)은 첫 칸 maxLength=numDigits 로 전체 코드를 받아 분배.
  • Reveal/RevealContainer/useRevealuseReveal/context 는 web 과 공유(순수), Reveal.native 만 web 의 max-height+ResizeObserver 대신 Reanimated FadeInDown 으로 순차 등장.
  • SpinnerLoader — web 의 conic-gradient 도넛을 react-native-svg 70% 호 + Reanimated 1초 회전으로 재현.
  • TextField — RN TextInput 기반이라 web 의 input 이벤트 API 대신 onChangeText/value 등 RN API 를 씁니다. web 처럼 공용 빌딩블록을 조합 — common/InputView.native(테두리·배경·높이·focus 외부 하이라이트 ring) + Text/FormLabel.native·Text/HelperText.native(라벨/도움말 색). focus ring: web 의 outline outline-2 outline-[rgba(40,117,250,0.12)](레이아웃 영향 없는 외곽선)이 RN 엔 없으므로, 항상 2px 테두리 바깥 wrapper(미포커스 투명)로 자리를 확보한 뒤 focus 시 색만 채워 시프트 없이 재현합니다.
  • DataViewCard/Tagtitle/children 등 텍스트는 RN 특성상 호출부에서 <Text> 로 감쌉니다.

Scaffold (native 전용, web PageLayout 통합)

Scaffold(native)는 web 의 PageLayout 을 흡수했습니다. px/py(body 패딩)·backgroundColor·statusBarColor/bottomBarColor(상/하단 안전영역 색) prop 을 직접 받습니다. SafeAreaView 대신 useSafeAreaInsets 로 상/하단 strip 을 직접 그려 바 색을 각각 지정합니다.

  • 색 기본값: 상태바는 appBar 가 있으면 그 아래 AppBar(normal-primary)와 끊김 없이 이어지도록 AppBar 색을 따르고, AppBar 가 없으면 backgroundColor 를 따릅니다. 하단바는 backgroundColor 를 따릅니다(둘 다 미지정이면 normal-primary). footer 가 있으면 하단 inset 은 footer(CTAGroup)가 처리하므로 Scaffold 는 중복 추가하지 않습니다. → 별도 PageLayout native 컴포넌트는 없습니다(web 은 그대로 유지).
  • Divider full-bleed: Scaffold 는 body 패딩(px)을 scaffoldPaddingContext 로 내려주고, Divider(native)는 이를 음수 마진(marginHorizontal: mx - px)으로 상쇄해 web 처럼 px 를 무시하고 화면 너비를 가득 채웁니다. Scaffold 밖(컨텍스트 미제공)에서는 px=0 이라 일반 마진처럼 동작.

Toast (전역 스토어 + 루트 <ToastHost/>)

web useToast(FloatingProvider 컨텍스트)와 달리 native 는 전역 모듈 스토어 + 루트 <ToastHost/> 방식이라 provider 가 불필요합니다. 앱 루트(_layout.tsx)에 <ToastHost/> 를 1회 렌더하면 어디서든 const toast = useToast(); toast.show({message}) 가능합니다. 다른 RN Modal(드로어/시트) 위에도 보이도록 ToastHost 가 자체 transparent RN Modal 로 띄웁니다.

press 틴트 (InteractionOverlay)

web 은 <button className="group">…<Interaction.Overlay/></button> 처럼 group-active 로 press 틴트를 표시합니다. native 도 동일하게 NativeWind 의 named group 으로 자동 반응합니다 — 부모 Pressable 에 group/interaction 을 주고 그 안에 <InteractionOverlay/> 를 두면, NativeWind 가 group-active/interaction:opacity-100 으로 press 시 틴트를 fade in 합니다(별도 pressed prop 불필요).

<Pressable className="group/interaction relative ..." onPress={...}> <InteractionOverlay intensity="normal" borderRadius={12} left={-8} right={-8} /> …내용… </Pressable>

부모에 relative(기본) + 필요 시 overflow-hidden. inset(left/right/top/bottomborderRadius 로 덮을 영역을 web 과 동일하게 조정합니다. (수동 제어가 필요하면 pressed prop 으로 직접 토글 가능.) Button/MenuList/DataViewCard 등은 이미 내부에서 이 방식으로 press 피드백을 적용합니다.

Alert / Modal / Backdrop (오버레이 체인)

web 의 오버레이는 Alert/ConfirmBaseAlertModalBackdropOnPortal(DOM 포털) + framer-motion + 브라우저 history-stack 으로 연결됩니다. native 는 이 체인을 다음으로 이식합니다.

애니메이션 책임은 web 과 동일하게 나눕니다(web: Backdrop 의 motion.div=dim 페이드, Modal 의 motion.div=카드 scale/opacity, AnimatePresence 가 둘을 조율):

  • Backdrop.native.tsx: RN 내장 Modal 로 구현(자체 포털 + 풀스크린 오버레이 + 안드로이드 하드웨어 백 → OnPortal/UIProvider 포털/usePopStateStack 불필요). dim 페이드 + 진행도(progress) 구동 + 생애주기를 담당. Reanimated SharedValue<number>(0=닫힘~1=열림)를 withTiming 으로 구동하고 Backdrop/context.ts 로 내려줍니다.
  • Modal.native.tsx: web Modal 과 동일하게 카드 애니메이션(scale 0.97→1 + opacity)을 제어. useBackdropProgress() 로 progress 를 읽어 useAnimatedStyle 로 카드에 매핑합니다.
  • BaseAlert.native.tsx / BaseAlertView.native.tsx: web 의 next/dynamic(SSR 회피) 없이 직접 렌더.
  • useAlert / useAsyncAlert / useAsyncConfirm / Alert/types: 순수 React/타입이라 플랫폼 중립 — web/native 가 그대로 공유합니다(.native 불필요). 단 Alert/types.ts 는 web 전용 Modal.tsx 대신 중립 타입 Overlay/typesOverlayController 를 참조합니다.

Reanimated 기반(react-native-reanimated)이라 마운트 레이스가 없습니다. RN Modal 은 visible=false 시 children 을 즉시 unmount 하므로, mounted(visible 을 늦게 따라감) state 로 exit 애니메이션이 끝난 뒤(progress withTiming(0) 완료 콜백) 닫습니다(web AnimatePresence + onClose 타이머 대응). enter 는 모달이 표시된 직후 onShow 에서 시작합니다.

className(NativeWind)과 Reanimated 애니메이션 스타일은 한 컴포넌트에서 섞지 않습니다. 정적 레이아웃 컨테이너만 react-native-css View(className), 애니메이션 대상(dim/카드)은 Animated.* + inline style 로 처리합니다.

사용: const c = useAlert({onOk}); ... <Confirm controller={c.controller} .../> + onPress={() => c.open()}. RN Modal 이 자체 포털이라 화면 트리 아무 곳에 <Confirm>/<Alert> 를 렌더해도 됩니다(별도 Provider 불필요).

전환 애니메이션 커스터마이즈

Modal/Backdroptransition prop({durationMs?, easing?})으로 조절합니다. duration·easing 은 Backdrop 이 소유해 dim·카드(progress)를 함께 구동합니다.

import {Easing} from 'react-native-reanimated'; <Modal transition={{ durationMs: 300, easing: Easing.out(Easing.cubic), }} ... />

BottomSheet / SideDrawer (Drawer)

slide + (BottomSheet)드래그-투-디스미스가 핵심이라 Reanimated + react-native-gesture-handler 로 구현합니다(Alert/Modal 의 NativeWind 와 다른 엔진). 컨트롤러는 native useOverlay(web 과 달리 closehistory.back 대신 setIsOpened(false)).

  • DrawerScaffold.native.tsx(내부 공용): RN Modal(포털/오버레이) + GestureHandlerRootView + dim 페이드 + 생애주기. progress(0~1 SharedValue)를 render-prop 으로 내려주고, 자식이 slide/드래그로 구동·매핑합니다. 자식의 exit 애니가 끝나면 requestExitDone() 으로 unmount.
  • BottomSheet.native.tsx: 하단에서 slide-up(스프링, web 과 동일 계수 stiffness 480/damping 48/mass 1.2) + grab 핸들 드래그-투-디스미스. props: header/expanded/disableUserClose/disableGrab/rounded/noBackdrop/backdropStyle/onOutsideClick. 측정 높이로 translateY 매핑(퍼센트 translate 회피). safe-area 하단 inset 반영.
  • SideDrawer.native.tsx: 좌/우 slide-in(timing, easeOut). position/defaultHeader. 드래그 없음(web 동일).

주의: RN Modal 은 앱 루트 GestureHandlerRootView 와 별개의 네이티브 루트라, Modal 내부 제스처를 위해 DrawerScaffold 가 모달 콘텐츠를 다시 GestureHandlerRootView 로 감쌉니다. 앱 루트(_layout.tsx)에도 GestureHandlerRootView 가 있어야 합니다.

미구현(web 대비): BottomSheetSnapPoint(사용 1곳) · preserveState children 리마운트. 필요 시 추가.

새 native 컴포넌트 추가 체크리스트

  1. v2/Foo/Foo.native.tsx 작성 (RN + NativeWind className). 파일 맨 위에 /// <reference types="react-native-css/types" />.
  2. 공유 가능한 분기/상수는 v2/Foo/Foo.shared.ts 로 추출 (web Foo.tsx 도 거기서 import).
  3. native/index.ts 배럴에 export 추가.
  4. className 을 쓰는 core RN 컴포넌트(View/Text/Pressable 등)는 react-native-css/components 에서 import (아래 NativeWind 참고).
  5. tsc(apps/native-app 에서 yarn typecheck)로 검증.

NativeWind v5 (Tailwind for RN)

native 는 NativeWind v5 로 className 스타일링을 합니다.

  • 왜 v5(preview)인가: v5 는 Tailwind v4 기반(web packages/ui 와 동일 메이저)이고 react-native >=0.81·react >=19 를 명시 지원합니다. v4 는 RN peer 가 * 라 RN 0.85 에서 미검증이었습니다.
  • 설치 세트: nativewind@5.0.0-preview.4, react-native-css@^3.0.7, tailwindcss@^4.2.1, @tailwindcss/postcss@^4.2.1, lightningcss@1.30.1(정확히 핀 — deserialization 오류 방지).
  • 설정 파일(apps/native-app/): global.css, postcss.config.mjs, tailwind.config.ts, nativewind-env.d.ts, metro.config.js(withNativewind 로 래핑), _layout.tsx 에서 import '../../global.css'.

⚠️ className 은 react-native-css/components 에서 import

// ✅ native import {View, Text, Pressable} from 'react-native-css/components'; <View className="rounded-lg bg-solid-brand-primary" /> // ❌ 'react-native' 의 View 에 className 을 주면 런타임에 조용히 무시됨 import {View} from 'react-native'; <View className="rounded-lg" /> // 타입은 통과, 스타일 안 먹음

NativeWind v5 의 전역 globalClassNamePolyfill(모든 react-native import 를 cssInterop 래퍼로 스왑)은 RN 0.85 에서 부팅 크래시를 일으켜 metro.config.js 에서 **비활성(globalClassNamePolyfill: false)**했습니다. 대신 className 이 필요한 컴포넌트는 cssInterop 이 적용된 react-native-css/components 에서 직접 import 합니다(opt-in 방식).

  • 흔한 함정: TextInput/Modal 등 RN 전용 컴포넌트를 쓰느라 import {TextInput, View} from 'react-native'View 까지 묶으면, 그 View 의 className 이 전부 무시돼 “그 파일/화면만 스타일 안 됨” 버그가 됩니다. RN 전용은 따로 import 하고, View/Textreact-native-css/components 에서 가져오세요. (css/components 의 View 는 style prop 도 지원 → inline style 병용 OK.)
  • 전역 className 타입 참조 위치 규칙은 컨벤션 §1 참고.

⚠️ Fast Refresh — 주입 CSS 모듈 패치 필요 (module.hot.accept)

react-native-css 가 컴파일한 global.css 주입 모듈은 export {}(사이드이펙트 전용) + module.hot.accept() 없음이라 Fast Refresh boundary 가 아닙니다. Tailwind JIT 라 모든 소스 파일이 global.css 의 content(의존성)이므로, 아무 파일이나 저장하면 이 비-boundary 모듈이 무효화되고 루트 import(_layout.tsx)까지 번져 매 저장이 full reload 가 됩니다(Fast Refresh 안 됨).

  • 해결: patches/react-native-css+3.0.7.patch 가 주입 모듈(dist/{commonjs,module}/metro/injection-code.js)에 module.hot.accept() 를 추가 → global.css 가 HMR 을 자체 수락해 루트로 번지지 않습니다.
  • 검증 시 transformer 출력이 캐시되므로 expo start --clear 로 리로드합니다.

이 패치(react-native-css+3.0.7.patch)는 여러 변경을 한 파일에 합쳐 두었습니다(아래 rem·폰트 항목 참고). 편집 후에는 npx patch-package react-native-css 로 재생성하며, 재생성 시 모든 변경이 함께 반영되는지 확인하세요.

rem 기준값 16px (web 픽셀 동일)

react-native-css(NativeWind v5) 기본 rem 은 14 라, 미지정 시 모든 rem 기반 유틸(text-[Nrem]·spacing·radius 등)이 native 에서 web(브라우저 기본 16px) 대비 ~12.5% 작게 렌더됩니다.

  • 실제 적용은 patches/react-native-css+3.0.7.patch 가 컴파일러 기본 rem 을 14→16 으로 바꿔 담당합니다(rem→px 변환 경로가 compiler.js firstPass + declarations.js parseLength + stylesheet.js + native-internal/root.js 로 여러 곳이라 컴파일러 기본값을 직접 16 으로 패치하는 게 가장 확실). withNativewindinlineRem 옵션은 이 Expo/Metro 버전에서 transform 까지 전달되지 않아 무효였습니다.
  • global.css:root { font-size: 16px } 는 보조 선언(컴파일러의 CSS 스캔 경로로도 16 을 보장하지만 직접 쓰인 rem 만 커버).

leading(줄간격)은 폰트 배수 — 소수 스케일은 leading-ext.css

native line-height 는 항상 폰트 배수입니다(고정 px 아님). NativeWind(nativewind/theme)는 leading-*calc(var(--spacing) / 1rem * --value(integer)) 로 재정의하는데, --value(integer) 라서 정수 스케일(leading-6)만 되고 leading-5.5 같은 소수는 드롭됩니다.

  • apps/native-app/leading-ext.css 가 같은 패턴을 --value(number) 로만 바꿔 0.25 단위 소수까지 허용합니다. 값: leading-N = N × 0.25 × font-size(예 leading-5.5 = 1.375×).
  • import 순서 주의: 같은 이름 @utility 는 “먼저 정의된” 게 이기므로, global.css 에서 leading-ext.css@import "nativewind/theme" 보다 먼저 import 해야 합니다.
  • ⚠️ 단위 붙은 arbitrary(leading-[Nrem]·leading-[Npx])는 react-native-css 가 드롭하므로 쓰지 마세요. 진짜 고정 px 줄간격이 필요하면 style={{lineHeight: N}} 으로 RN prop 을 직접 지정합니다.

전역 폰트(Pretendard)는 react-native-css Text 패치로 주입

RN 0.85 의 Text 는 순수 함수형 컴포넌트(.render 없음) + React 19 함수형에 defaultProps 가 없어 몽키패치가 불가합니다. 그래서 patches/react-native-css+3.0.7.patchreact-native-cssText/TextInput 컴포넌트의 useCssElement 호출 시 className 맨 앞에 font-pretendard 를 주입합니다.

  • global.css.font-pretendard { font-family: Pretendard } 는 소스에서 직접 쓰이지 않아 purge 되지 않도록 @layer 밖 일반 규칙으로 정의합니다.
  • 결과적으로 폰트 클래스를 안 붙여도 모든 react-native-css Text 가 Pretendard 로 렌더됩니다. raw RN Text(react-native 직접 import)만 style 에 fontFamily 를 직접 지정해야 합니다.

타이포그래피·shadow 유틸 (web 값 이식)

  • 타이포그래피: web packages/style/tailwind.csstext-* 유틸(text-body-1-medium, text-title-3-bold …)을 global.css@layer utilities 에 동일 값으로 이식했습니다(NativeWind v5 는 @apply 지원). className 그대로 사용.
  • elevation shadow: web tailwind config 의 다층 boxShadow.elevation* 를 native 용으로 대표 레이어 하나로 근사해 global.css 에 정의(shadow-elevation100~shadow-elevation400, shadow-elevation1/3/4). react-native-css 가 box-shadow 를 RN boxShadow 로 변환하며 New Architecture 에서 렌더됩니다. 그림자가 잘리지 않도록 대상 뷰에 overflow:hidden 을 주지 마세요.

불안정 유틸 — 런타임 CSS 변수 / disabled 변형

react-native-css 는 몇 가지 web Tailwind 기능의 런타임 적용이 불안정합니다. 아래는 값을 baked 하거나 JS 로 우회합니다.

  • 런타임 CSS 변수 해석 불안정 → 색은 컴파일 타임에 concrete 값으로 baked (아래 “색상” 참고).
  • group-disabled:/disabled: 변형 런타임 적용 미보장 → native 는 disabled 여부를 JS 로 판단해 common/colors 문자열의 group-disabled: 색을 직접 적용(Button.shared.tstoNativeDisabledClassName).

색상: web 과 동일한 semantic 단일 소스

web 의 색 시스템(packages/style/colors.css 의 CSS 변수 + packages/configs/tailwind/tailwind.config.js 의 semantic 토큰: bg-solid-brand-primary, text-normal-primary, text-status-negative …)을 native 도 그대로 사용합니다.

  • apps/native-app/tailwind.config.tsweb tailwind config 의 색 맵을 import 해, rgb(var(--color-X) / a) 형태를 colorV4 실제값(hex/rgba)으로 컴파일 타임에 해석해 native theme(backgroundColor/textColor/borderColor)에 주입합니다. (react-native-css 는 런타임 CSS 변수 해석이 불안정 → 값을 baked.) 알파가 없거나 <alpha-value> 면 hex 유지(/opacity 모디파이어도 동작), 리터럴 알파는 rgba() 로 변환.
  • 결과적으로 native 에서도 bg-solid-brand-primary 가 web 과 동일한 색으로 컴파일됩니다.
  • tailwind.config.tscontent./src/**/*.{ts,tsx} + ../../packages/ui/v2/**/*.{ts,tsx} 를 스캔합니다(native-app 소스 + 공유 *.native.tsx 의 className).
  • Button 의 색 결정 분기는 web v2/common/colors.ts(getButtonBackgroundClassName/getButtonTextColorClassname) 단일 소스에서만 일어납니다. native 의 disabled 색·아이콘 색도 그 className 에서 도출합니다(별도 분기 없음).

아이콘 (@howmuchhome-web/icon)

icon 패키지의 172개 아이콘(packages/icon/libs)은 web DOM SVG(<svg>/<path>)로 작성돼 RN 에서 그대로 못 그립니다(react-native-svg<Svg>/<Path> 필요).

  • 소스를 수정하지 않고, native 빌드에서만 동작하는 babel 플러그인(apps/native-app/plugins/babel-plugin-rn-svg-icons.js)이 소문자 SVG 엘리먼트를 react-native-svg 컴포넌트로 치환하고 import 를 주입합니다. web 소스는 DOM SVG 그대로 유지됩니다. → 상세: Metro / 의존성 §3.
  • react-native-svg@15.15.4(Expo bundled)가 필요하며, 네이티브 모듈이라 추가 시 prebuild + 재빌드가 필요합니다. 이 dep 는 아이콘을 소비하는 공유 패키지(packages/ui)의 package.json 에 선언돼 있습니다(RN 전용 dep 은 루트로 hoist 되지 않음).
  • Button 의 startIcon/endIcon 은 아이콘 color 를 지정하지 않으면 버튼 텍스트 색을 자동 적용합니다(web currentColor 대응). RN 에는 CSS currentColor 캐스케이드가 없어 Button 이 값을 주입하는 방식입니다.
  • 주의: 배럴(@howmuchhome-web/icon) import 는 Metro 트리셰이킹이 없어 172개가 모두 번들됩니다. 번들 크기가 문제가 되면 개별 subpath export 를 고려합니다.

리치 텍스트: editor/v2 를 webview 로 재사용 (뷰어 + 편집기)

RN 에 Tiptap 급 네이티브 리치 에디터가 없어, 카페 본문은 editor/v2(Tiptap)를 단일파일 webview 번들(apps/native-app/editor-web) 로 재사용합니다. react-native-webview + editor/v2 라 tentap·네이티브 재빌드 불필요.

  • 빌드: yarn editor-web:build(내부적으로 node editor-web/build.mjs → vite single-file, native-app 에 nested 된 vite@6 사용) → editorHtml(HTML 문자열). 본문 JSON 은 injectedJavaScriptBeforeContentLoadedwindow.__POST_CONTENT__ 주입.
  • 뷰어/편집기 한 번들: window.__EDITOR_MODE__ 로 분기 — viewer(mode="viewer", 상세 본문) / editor(mode="editor", 작성·수정).
  • 필수 함정:
    • source={{html, baseUrl:'https://localhost/'}} — baseUrl 없으면 origin 이 opaque → 인라인 <script type="module"> 차단돼 미실행(onLoadEnd 떠도 boot 안 됨).
    • 모드별 CSS 분리(body.viewer-mode/body.editor-mode): viewer 는 콘텐츠 자연 높이(ResizeObserver→postMessage('height') 로 WebView 높이 맞춤), editor 는 루트를 뷰포트로 채워 툴바 바닥 고정 + visualViewport 로 body 높이 동기화(키보드 위로 따라 올라감).
    • 편집 초기값 빈 doc(content:[])은 커서가 없어 입력 전 툴바 명령이 no-op → {type:'doc',content:[{type:'paragraph'}]} 로 초기화.
    • 툴바 버튼 텍스트 white-space:nowrap(좁은 폭에서 한글 글자 중간 줄바꿈 방지).
    • NodeView 의 Next 의존(FileNode/LinkCard@howmuchhome-web/app)은 vite resolve.aliaswebview-safe shim 대체(invariant expected app router to be mounted 크래시 회피).
  • 업로드 브릿지: 이미지/파일 버튼 → web file input → onImageUpload/onFileUpload(File→base64→postMessage) → RN 이 인증 업로드 → __resolveUpload__(id,url) injectJavaScript 로 에디터에 삽입. 이미지=Cloudinary(front-api 서명), 파일=CafeAPI.uploadPostFile(expo File). 카테고리/제목/제출은 네이티브(인증 필요).

관련 파일

경로내용
packages/ui/native/index.tsnative 진입점 배럴
packages/ui/v2/Button/Button.native.tsx · Button.shared.tsnative Button + 공유 로직
packages/ui/v2/Form/Form.native.tsxnative Form (DOM submit 대신 context.submit)
packages/ui/v2/Animation/Interaction/InteractionOverlay.native.tsxpress 틴트 오버레이
apps/native-app/global.cssTailwind v4 import·rem 보조·타이포그래피/shadow 유틸·.font-pretendard
apps/native-app/leading-ext.cssleading-* 소수 스케일 확장
apps/native-app/tailwind.config.tsweb 색 맵 → colorV4 concrete 값 해석 주입, content 스캔
apps/native-app/metro.config.jswithNativewind(globalClassNamePolyfill:false) + shim resolver
patches/react-native-css+3.0.7.patchrem 14→16 · font-pretendard 주입 · Fast Refresh module.hot.accept
apps/native-app/plugins/babel-plugin-rn-svg-icons.jsicon DOM SVG → react-native-svg 치환
apps/native-app/editor-web/editor/v2 webview 번들(뷰어/편집기), yarn editor-web:build
apps/native-app/src/lib/upload/toUploadFile.ts업로드용 expo File(Blob) 변환 헬퍼
Last updated on