크로스플랫폼 UI (native 컴포넌트 · 스타일링)
packages/ui(@howmuchhome-web/ui)는 web/dashboard 와 native(Expo) 앱이 공유하는 디자인 시스템입니다. web 컴포넌트는 DOM·Tailwind 를 전제로 하므로 RN 에서 그대로 동작하지 않아, 컴포넌트별로 native 구현(*.native.tsx)을 따로 두고 공유 로직(*.shared.ts)만 재사용합니다. 이 문서는 그 아키텍처와 NativeWind v5 스타일링 실전을 다룹니다.
관련 문서:
- 패키지 자체 구조·의존 규칙: Metro / 의존성
- 스타일·import 규칙 체크리스트: 컨벤션
- 앱 전반: Expo native 앱
진입점: 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 exports 에 react-native 조건을 걸어야 하고 배럴(export *)에는 조건 분기를 적용할 수 없습니다. 그래서 native 전용 진입점을 명시적으로 분리했습니다.
@howmuchhome-web/ui/v2(web 컴포넌트)를 native 코드에서 직접 import 하지 마세요 — DOM/Tailwind 를 전제로 하므로 RN 에서 깨집니다. 반드시/native배럴을 사용하고, 없으면 native 구현을 추가합니다.
컴포넌트 분리 규약 (.tsx / .native.tsx / .shared.ts)
| 파일 | 역할 |
|---|---|
Foo.tsx | web 구현 (DOM/Tailwind) |
Foo.native.tsx | native 구현 (RN + NativeWind) |
Foo.shared.ts | web/native 공유 로직·상수 (플랫폼 중립, RN/DOM import 금지) |
*.shared.ts는 DOM 도 RN 도 import 하지 않습니다. 색·사이즈 매핑, 상태 계산 등 순수 로직만 담고 webFoo.tsx와 nativeFoo.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).
컴포넌트별 설계 메모
PhoneTextField—phoneFormatter(공유 순수 함수) 재사용. value/onChangeText 는 하이픈 제거 숫자(raw), 표시는 포맷.OTPInput— web 과 동일하게 자리수만큼TextField셀 + ref 포커스 이동(TextField.native가 ref 를 내부TextInput으로 forward). SMS 자동완성(iOSoneTimeCode/ Androidsms-otp)은 첫 칸maxLength=numDigits로 전체 코드를 받아 분배.Reveal/RevealContainer/useReveal—useReveal/context 는 web 과 공유(순수),Reveal.native만 web 의 max-height+ResizeObserver 대신 ReanimatedFadeInDown으로 순차 등장.SpinnerLoader— web 의 conic-gradient 도넛을 react-native-svg 70% 호 + Reanimated 1초 회전으로 재현.TextField— RNTextInput기반이라 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/Tag—title/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 는 중복 추가하지 않습니다. → 별도PageLayoutnative 컴포넌트는 없습니다(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/bottom)·borderRadius 로 덮을 영역을 web 과 동일하게 조정합니다. (수동 제어가 필요하면 pressed prop 으로 직접 토글 가능.) Button/MenuList/DataViewCard 등은 이미 내부에서 이 방식으로 press 피드백을 적용합니다.
Alert / Modal / Backdrop (오버레이 체인)
web 의 오버레이는 Alert/Confirm → BaseAlert → Modal → Backdrop → OnPortal(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) 구동 + 생애주기를 담당. ReanimatedSharedValue<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/types의OverlayController를 참조합니다.
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-cssView(className), 애니메이션 대상(dim/카드)은Animated.*+ inline style 로 처리합니다.
사용: const c = useAlert({onOk}); ... <Confirm controller={c.controller} .../> + onPress={() => c.open()}. RN Modal 이 자체 포털이라 화면 트리 아무 곳에 <Confirm>/<Alert> 를 렌더해도 됩니다(별도 Provider 불필요).
전환 애니메이션 커스터마이즈
Modal/Backdrop 의 transition 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 과 달리 close 가 history.back 대신 setIsOpened(false)).
DrawerScaffold.native.tsx(내부 공용): RN Modal(포털/오버레이) +GestureHandlerRootView+ dim 페이드 + 생애주기.progress(0~1SharedValue)를 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곳) ·preserveStatechildren 리마운트. 필요 시 추가.
새 native 컴포넌트 추가 체크리스트
v2/Foo/Foo.native.tsx작성 (RN + NativeWind className). 파일 맨 위에/// <reference types="react-native-css/types" />.- 공유 가능한 분기/상수는
v2/Foo/Foo.shared.ts로 추출 (webFoo.tsx도 거기서 import). native/index.ts배럴에 export 추가.- className 을 쓰는 core RN 컴포넌트(
View/Text/Pressable등)는react-native-css/components에서 import (아래 NativeWind 참고). - 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/Text는react-native-css/components에서 가져오세요. (css/components 의 View 는styleprop 도 지원 → 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.jsfirstPass +declarations.jsparseLength +stylesheet.js+native-internal/root.js로 여러 곳이라 컴파일러 기본값을 직접 16 으로 패치하는 게 가장 확실).withNativewind의inlineRem옵션은 이 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.patch 가 react-native-css 의 Text/TextInput 컴포넌트의 useCssElement 호출 시 className 맨 앞에 font-pretendard 를 주입합니다.
global.css의.font-pretendard { font-family: Pretendard }는 소스에서 직접 쓰이지 않아 purge 되지 않도록@layer밖 일반 규칙으로 정의합니다.- 결과적으로 폰트 클래스를 안 붙여도 모든
react-native-cssText 가 Pretendard 로 렌더됩니다. raw RN Text(react-native직접 import)만 style 에fontFamily를 직접 지정해야 합니다.
타이포그래피·shadow 유틸 (web 값 이식)
- 타이포그래피: web
packages/style/tailwind.css의text-*유틸(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를 RNboxShadow로 변환하며 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.ts의toNativeDisabledClassName).
색상: 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.ts가 web 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.ts의content는./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 를 지정하지 않으면 버튼 텍스트 색을 자동 적용합니다(webcurrentColor대응). 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 은injectedJavaScriptBeforeContentLoaded로window.__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)은 viteresolve.alias로 webview-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(expoFile). 카테고리/제목/제출은 네이티브(인증 필요).
관련 파일
| 경로 | 내용 |
|---|---|
packages/ui/native/index.ts | native 진입점 배럴 |
packages/ui/v2/Button/Button.native.tsx · Button.shared.ts | native Button + 공유 로직 |
packages/ui/v2/Form/Form.native.tsx | native Form (DOM submit 대신 context.submit) |
packages/ui/v2/Animation/Interaction/InteractionOverlay.native.tsx | press 틴트 오버레이 |
apps/native-app/global.css | Tailwind v4 import·rem 보조·타이포그래피/shadow 유틸·.font-pretendard |
apps/native-app/leading-ext.css | leading-* 소수 스케일 확장 |
apps/native-app/tailwind.config.ts | web 색 맵 → colorV4 concrete 값 해석 주입, content 스캔 |
apps/native-app/metro.config.js | withNativewind(globalClassNamePolyfill:false) + shim resolver |
patches/react-native-css+3.0.7.patch | rem 14→16 · font-pretendard 주입 · Fast Refresh module.hot.accept |
apps/native-app/plugins/babel-plugin-rn-svg-icons.js | icon 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) 변환 헬퍼 |