메인 콘텐츠로 건너뛰기

마이그레이션 가이드

이 페이지는 두 가지를 다룹니다: Vue(lh-ui-plus / lh-ui-vue2) → React의 일반적인 개념 매핑과, 이 라이브러리 구현에서 Vue 버전과 의도적으로 다른 점.

개념 매핑 총정리​

Vue (lh-ui-plus)React (@lh-ui/react)
defineProps<T>()interface LhXxxProps + 구조 분해 기본 매개변수
withDefaults(...)구조 분해 기본값 (defaultProps 사용 안 함)
emits + emit('change', [val, e])onChange?: (value, event) => void
v-model="x"value={x} onChange={...}
v-model:visible="x"visible={x} onVisibleChange={...}
defineExpose({ ... })forwardRef + useImperativeHandle
<slot>children
<slot name="footer">footer?: ReactNode 프로퍼티
명명된 슬롯 #headerheader?: ReactNode 프로퍼티
provide(...) / inject(...)Context.Provider / useContext
provide는 반드시 computedContext value는 반드시 useMemo
computed(() => ...)useMemo(() => ..., [deps])
watch(src, cb)useEffect(() => cb(), [src])
watch(src, cb, { immediate: true })useEffect (effect는 본래 즉시 한 번 실행됨)
onMounted / onUnmounteduseEffect의 setup / cleanup
<Transition name="lh-x-fade">useTransition(visible, duration)가 동일 클래스명을 구동
<Teleport to="body">createPortal(..., document.body)
ref="<name>"useRef<HTMLDivElement>(null)
nextTick()flushSync() 또는 effect (보통은 필요 없음)
app.use(LhUI)없음 — 명명된 내보내기를 주문형으로 도입
<LhXxx v-bind="obj"><LhXxx {...obj}>

제어와 비제어​

Vue의 modelValue / update:modelValue는 React에서 두 가지 props로 나뉩니다:

<!-- Vue -->
<LhInput v-model="text" />
// React: 제어형
<LhInput value={text} onChange={(val) => setText(val)} />

// React: 비제어형
<LhInput defaultValue="초기값" />

혼용(동시에 value와 defaultValue를 전달)하면 개발 모드에서 경고가 출력되며, React 네이티브 form 의미론과 일치합니다.

이벤트 콜백​

모든 이벤트 props는 onXxx 명명으로 통일되고, 콜백 시그니처는 2-매개변수 (value, event)로 통일됩니다:

<!-- Vue -->
<LhSelect @change="onChange" />
// React
<LhSelect onChange={(value, option) => console.log(value, option)} />

전환 애니메이션​

Vue의 <Transition>은 선언형이고, React 버전은 자체 구현한 useTransition hook으로 동일한 클래스명 세트를 구동합니다:

import { useTransition } from '@lh-ui/react'

const { mounted, phase } = useTransition(visible, 300)
// 클래스명: `lh-dialog-fade-${phase}` — phase ∈ enter-from / enter-active / enter-to

클래스명이 share SCSS와 완전히 일치해, 시각적 표현이 Vue 버전과 프레임 단위로 같습니다.

오버레이와 텔레포트​

Vue의 <Teleport to="body">는 React의 createPortal에 대응하며, Dialog / Select 드롭다운 / Tooltip은 모두 document.body에 마운트됩니다. 위치 지오메트리는 useLhPopper가 계산합니다 (Vue 버전과 @lh-ui/share/core/shared/popper를 공유).

명령형 ref​

Vue의 defineExpose는 forwardRef + useImperativeHandle에 대응합니다:

<!-- Vue -->
<LhForm ref="formRef" />
// React
const formRef = useRef<LhFormInstance>(null)
<LhForm ref={formRef} />
<formRef.current?.validate() />

전역 설정​

<!-- Vue -->
<LhConfigProvider :size="size">
<App />
</LhConfigProvider>
// React
<LhConfigProvider size={size}>
<App />
</LhConfigProvider>

이 라이브러리와 Vue 버전의 구현 차이​

이하 차이점은 React 형태에서의 의도된 조정이며, 마이그레이션 시 주의가 필요합니다. 각 항목은 저장소 내에 등록되어 있습니다.

1. 스타일 주문형 주입​

lh-ui-plus(전체 주입)와 달리, 이 라이브러리는 lh-ui-vue2에 맞춥니다: 패키지 진입점에서 스타일을 주입하지 않고, 사용자가 theme.css + style/<name>을 명시적으로 가져옵니다.

2. 빌드 체인은 vue2와 동일​

vite lib mode + preserveModules + tsc가 d.ts를 그대로 생성합니다 (plus는 rollup 다중 진입점 스캔 사용). preserveModules는 주문형 도입을 자연스럽게 지원하고, tsx에는 SFC 가상 모듈이 없으며, tsc의 d.ts 직출이 가장 간단합니다.

3. ConfigProvider 소비 측 폴백​

Vue 버전은 inject(..., null)로 각 컴포넌트가 null 체크를 합니다. React 버전의 소비 hook은 모든 필드가 undefined인 폴백 객체를 반환해, 컴포넌트 내부에서 null 체크가 필요 없습니다 — useContext는 호출 지점에서 기본값을 줄 수 없기 때문에, 이는 React Context의 일반적인 관행입니다.

4. notification / loading의 마운트 차이​

셋 모두 명령형 서비스지만 마운트 방식이 다릅니다: message / notification은 싱글톤 컨테이너를 공유하고 (지연 createRoot + action 채널), loading은 아무 목표 요소(전체 화면 또는 일부)에 마운트되어야 하며, per-instance createRoot를 사용해 각 인스턴스가 자신의 root와 overflow / inert 복원 핸들을 갖습니다.

notification의 API는 객체 형태입니다 (notification.success({ title, content }), plus의 Notification(options)에 맞춤). 문자열 줄임 표기도 받습니다 (제목 줄만 렌더링). FLIP 이동 애니메이션은 미구현: Vue의 transition-group이 제공하는 -move 클래스와 동등한 원시 요소가 React 쪽에 없어, 큐의 추가 / 삭제 때 형제 카드가 순간적으로 위치를 바꿉니다.

5. Input의 onChange 2-매개변수​

Vue 버전은 @input / @change가 분리되어 있고, React 버전은 onChange(value, event)로 통일됩니다 (추가로 onInput이 네이티브 입력 이벤트를 전달).

6. Input autosize를 컴포넌트가 DOM 조작​

Vue 버전은 data-autosize 속성으로 디렉티브가 DOM 레벨에서 높이를 계산합니다. React 버전은 useEffect에서 scrollHeight를 읽고 textarea.style.height에 직접 기록합니다 (minRows / maxRows 제약). 추가적인 디렉티브 메커니즘 도입을 피하기 위해서입니다.

7. Select 위치 계산을 useLhPopper로 변경​

일부 Vue 버전의 Select는 직접 fixed 위치를 쓰고 뒤집지 않습니다. React 버전은 useLhPopper로 통일합니다 (Dialog / Tooltip과 동일한 지오메트리, 동일한 넘침 뒤집기와 스크롤 시 닫기).

8. Dialog 순수 제어​

plus의 Dialog는 내부에 isVisible 중간 상태를 갖고 watch로 modelValue와 양방향 동기화합니다. React 버전은 중간 상태를 제거했습니다: open을 외부가 소유하고, 모든 닫기 경로는 onClose만 호출합니다. 제어 의미가 더 순수하고 props와 내부 상태의 경쟁을 없앱니다.

9. Dialog 생명주기를 두 그룹의 effect로 분할​

Vue 버전은 watch(visible, cb, { immediate: true }) 콜백 하나로 열고 닫기를 처리합니다. React 버전은 "open effect"(오버레이 스택 등록, 스크롤 잠금, onOpen 트리거)와 "mounted effect"(focus trap, onOpened / onClosed 트리거)로 나눕니다. 전환 애니메이션의 마운트 시점과 open 시점이 React에서는 서로 다른 두 개의 독립적인 생명주기이기 때문입니다.

10. Form의 비동기 검증 수정​

plus의 validateField는 Promise.resolve(!message)를 사용해, 비동기 validator가 반환하는 Promise를 기다리지 않습니다 — 비동기 검증이 항상 true를 반환합니다. React 버전은 isThenable 판정을 추가해, 비동기 validator가 완료된 뒤에야 판정합니다:

if (isThenable(message)) return message.then((error) => !error)

이 결함은 패밀리 버그픽스 대장에 등록되었으며, Vue 버전으로의 수정은 아직 이식되지 않았습니다.

11. Tooltip 제어 단순화​

plus의 Tooltip은 manual 스위치로 visible 제어에 맞춥니다. React 버전은 visible 프로퍼티를 직접 봅니다: visible을 전달하면 제어형이고, onVisibleChange 콜백이 외부에 알리며, manual 중간 스위치는 더 이상 유지하지 않습니다.

12. React 측 개선 (plus에는 아직 없음)​

이하 차이점은 React 버전의 개선이며, 마이그레이션 시 새 기능으로 사용하면 됩니다:

  • Tag 닫기 즉시 언마운트: 닫기 버튼을 클릭하면 Tag가 즉시 DOM에서 언마운트된 뒤 onClose가 발생합니다 (plus는 이벤트만 던지고 외부 v-if가 노드를 제거).
  • Input 지우기 되돌아쓰기: clearable의 지우기 버튼이 onChange('')도 함께 던져, 제어 상태가 자동으로 동기화됩니다 (plus의 @clear는 이벤트만 던짐).
  • Form change 검증: rule.trigger === 'change'인 규칙이 콘텐츠 영역의 change 버블에 따라 발생합니다 (plus는 reactive watch로 구동).
  • Form 재설정 시 에러 초기화: resetFields()가 내부 버전 번호를 증가시키고, 각 FormItem이 감시해 에러 상태를 비웁니다 (plus는 reactive 체인으로 자동 비움).
  • Input 비밀번호 표시 / 숨김: showPassword 프로퍼티는 type="password"일 때 눈 버튼을 렌더링해 평문 전환. plus / vue2에는 이 프로퍼티가 없습니다.

13. Wave C 컴포넌트의 React 특화​

  • Drawer 마스크는 기본적으로 클릭 관통 안 됨: maskClosable 기본값 false (core의 canDrawerClose는 기본 거부). plus의 closeOnClickModal 기본값은 true이므로 마이그레이션 시 명시적으로 선언해야 합니다. 포커스 트랩은 lockScroll이 켜져 있을 때만 활성화되며 (shouldDrawerTrapFocus), 스크롤을 잠그지 않을 때는 배경 콘텐츠의 Tab 순서를 유지합니다.
  • Dropdown 슬롯을 프로퍼티로 변경: plus의 #dropdown 슬롯은 React 쪽에서 동명의 dropdown 프로퍼티가 됩니다. 메뉴 항목은 Context로 DOM을 등록하고, roving tabindex는 activeEl 앵커 상태로 구동됩니다 (Vue 버전은 getCurrentInstance로 자식 컴포넌트 인스턴스를 수집).
  • Popover / Popconfirm 제어화: Tooltip과 동일하게 visible을 전달하면 제어형, onVisibleChange가 외부로 나가며, plus의 manual 스위치는 없습니다.

타입 마이그레이션 치트시트​

// Vue: props는 withDefaults로 추론
const props = withDefaults(defineProps<LhButtonProps>(), { type: 'text' })

// React: 인터페이스 + 구조 분해 기본값
function LhButton(props: LhButtonProps) {
const { type = 'text' } = props
}
// Vue: emit 선언
const emit = defineEmits<{ change: [val: string, e: Event] }>()

// React: props의 콜백
interface LhXxxProps {
onChange?: (value: string, event: React.ChangeEvent<HTMLInputElement>) => void
}