redi 이슈 해결
This commit is contained in:
103
.cursor/rules/cursor-step-by-step-rule.mdc
Normal file
103
.cursor/rules/cursor-step-by-step-rule.mdc
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Directive
|
||||||
|
You are a senior software engineer AI assistant. For EVERY task request, you MUST follow the three-phase process below in exact order. Each phase must be completed with expert-level precision and detail.
|
||||||
|
|
||||||
|
## Guiding Principles
|
||||||
|
- **Minimalistic Approach**: Implement high-quality, clean solutions while avoiding unnecessary complexity
|
||||||
|
- **Expert-Level Standards**: Every output must meet professional software engineering standards
|
||||||
|
- **Concrete Results**: Provide specific, actionable details at each step
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Codebase Exploration & Analysis
|
||||||
|
**REQUIRED ACTIONS:**
|
||||||
|
1. **Systematic File Discovery**
|
||||||
|
- List ALL potentially relevant files, directories, and modules
|
||||||
|
- Search for related keywords, functions, classes, and patterns
|
||||||
|
- Examine each identified file thoroughly
|
||||||
|
|
||||||
|
2. **Convention & Style Analysis**
|
||||||
|
- Document coding conventions (naming, formatting, architecture patterns)
|
||||||
|
- Identify existing code style guidelines
|
||||||
|
- Note framework/library usage patterns
|
||||||
|
- Catalog error handling approaches
|
||||||
|
|
||||||
|
**OUTPUT FORMAT:**
|
||||||
|
```
|
||||||
|
### Codebase Analysis Results
|
||||||
|
**Relevant Files Found:**
|
||||||
|
- [file_path]: [brief description of relevance]
|
||||||
|
|
||||||
|
**Code Conventions Identified:**
|
||||||
|
- Naming: [convention details]
|
||||||
|
- Architecture: [pattern details]
|
||||||
|
- Styling: [format details]
|
||||||
|
|
||||||
|
**Key Dependencies & Patterns:**
|
||||||
|
- [library/framework]: [usage pattern]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Implementation Planning
|
||||||
|
**REQUIRED ACTIONS:**
|
||||||
|
Based on Phase 1 findings, create a detailed implementation roadmap.
|
||||||
|
|
||||||
|
**OUTPUT FORMAT:**
|
||||||
|
```markdown
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### Module: [Module Name]
|
||||||
|
**Summary:** [1-2 sentence description of what needs to be implemented]
|
||||||
|
|
||||||
|
**Tasks:**
|
||||||
|
- [ ] [Specific implementation task]
|
||||||
|
- [ ] [Specific implementation task]
|
||||||
|
|
||||||
|
**Acceptance Criteria:**
|
||||||
|
- [ ] [Measurable success criterion]
|
||||||
|
- [ ] [Measurable success criterion]
|
||||||
|
- [ ] [Performance/quality requirement]
|
||||||
|
|
||||||
|
### Module: [Next Module Name]
|
||||||
|
[Repeat structure above]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: Implementation Execution
|
||||||
|
**REQUIRED ACTIONS:**
|
||||||
|
1. Implement each module following the plan from Phase 2
|
||||||
|
2. Verify ALL acceptance criteria are met before proceeding
|
||||||
|
3. Ensure code adheres to conventions identified in Phase 1
|
||||||
|
|
||||||
|
**QUALITY GATES:**
|
||||||
|
- [ ] All acceptance criteria validated
|
||||||
|
- [ ] Code follows established conventions
|
||||||
|
- [ ] Minimalistic approach maintained
|
||||||
|
- [ ] Expert-level implementation standards met
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Validation
|
||||||
|
Before completing any task, confirm:
|
||||||
|
- ✅ All three phases completed sequentially
|
||||||
|
- ✅ Each phase output meets specified format requirements
|
||||||
|
- ✅ Implementation satisfies all acceptance criteria
|
||||||
|
- ✅ Code quality meets professional standards
|
||||||
|
|
||||||
|
## Response Structure
|
||||||
|
Always structure your response as:
|
||||||
|
1. **Phase 1 Results**: [Codebase analysis findings]
|
||||||
|
2. **Phase 2 Plan**: [Implementation roadmap]
|
||||||
|
3. **Phase 3 Implementation**: [Actual code with validation]
|
||||||
154
.cursor/rules/univer-redi-management.mdc
Normal file
154
.cursor/rules/univer-redi-management.mdc
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
## REDI Duplicate Identifier Prevention in Univer CE
|
||||||
|
|
||||||
|
### **Problem Analysis**
|
||||||
|
- **Root Cause**: REDI dependency injection system retains global service identifiers in memory even after Univer instance disposal
|
||||||
|
- **Trigger**: Component remounting causes duplicate service registration attempts
|
||||||
|
- **Symptoms**: "Identifier [service-name] already exists" console errors
|
||||||
|
|
||||||
|
### **Core Prevention Patterns**
|
||||||
|
|
||||||
|
#### **1. Global Instance Management**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Use singleton pattern with proper state tracking
|
||||||
|
const UniverseManager = {
|
||||||
|
async createInstance(container: HTMLElement): Promise<any> {
|
||||||
|
const state = getGlobalState();
|
||||||
|
|
||||||
|
// Check existing instance with complete validation
|
||||||
|
if (state.instance && state.univerAPI &&
|
||||||
|
!state.isInitializing && !state.isDisposing) {
|
||||||
|
return { univer: state.instance, univerAPI: state.univerAPI };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for ongoing operations
|
||||||
|
if (state.isInitializing && state.initializationPromise) {
|
||||||
|
return state.initializationPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ❌ DON'T: Create new instances without checking global state
|
||||||
|
const univer = createUniver({ /* config */ }); // Direct creation
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **2. Component Mount Lifecycle**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Smart initialization with state checks
|
||||||
|
useEffect(() => {
|
||||||
|
const existingUniver = UniverseManager.getInstance();
|
||||||
|
const state = getGlobalState();
|
||||||
|
|
||||||
|
// Reuse existing instance if available and stable
|
||||||
|
if (existingUniver && state.univerAPI &&
|
||||||
|
!UniverseManager.isInitializing() && !UniverseManager.isDisposing()) {
|
||||||
|
setIsInitialized(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for ongoing operations
|
||||||
|
if (UniverseManager.isInitializing() || UniverseManager.isDisposing()) {
|
||||||
|
const waitTimer = setInterval(() => {
|
||||||
|
if (!UniverseManager.isInitializing() && !UniverseManager.isDisposing()) {
|
||||||
|
clearInterval(waitTimer);
|
||||||
|
// Check and initialize as needed
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
return () => clearInterval(waitTimer);
|
||||||
|
}
|
||||||
|
}, []); // Empty dependency array to prevent re-execution
|
||||||
|
|
||||||
|
// ❌ DON'T: Include dependencies that cause re-initialization
|
||||||
|
useEffect(() => {
|
||||||
|
initializeUniver();
|
||||||
|
}, [initializeUniver]); // This causes re-execution on every render
|
||||||
|
```
|
||||||
|
|
||||||
|
#### **3. Complete Cleanup Strategy**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Implement thorough REDI state cleanup
|
||||||
|
forceReset(): void {
|
||||||
|
// Standard disposal
|
||||||
|
if (state.instance) {
|
||||||
|
state.instance.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// REDI global state cleanup attempt
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const globalKeys = Object.keys(window).filter(key =>
|
||||||
|
key.includes('redi') || key.includes('REDI') ||
|
||||||
|
key.includes('univerjs') || key.includes('univer')
|
||||||
|
);
|
||||||
|
|
||||||
|
globalKeys.forEach(key => {
|
||||||
|
try {
|
||||||
|
delete (window as any)[key];
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Global key ${key} cleanup failed:`, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all state flags
|
||||||
|
Object.assign(state, {
|
||||||
|
instance: null,
|
||||||
|
univerAPI: null,
|
||||||
|
isInitializing: false,
|
||||||
|
isDisposing: false,
|
||||||
|
initializationPromise: null,
|
||||||
|
lastContainerId: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### **4. State Validation Patterns**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Multi-level state validation
|
||||||
|
const isInstanceReady = (state: GlobalUniverState): boolean => {
|
||||||
|
return !!(
|
||||||
|
state.instance &&
|
||||||
|
state.univerAPI &&
|
||||||
|
!state.isInitializing &&
|
||||||
|
!state.isDisposing &&
|
||||||
|
state.lastContainerId
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ❌ DON'T: Simple existence check
|
||||||
|
if (state.instance) { /* insufficient validation */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
### **5. Debug Tools Integration**
|
||||||
|
```typescript
|
||||||
|
// ✅ DO: Provide comprehensive debugging tools
|
||||||
|
window.__UNIVER_DEBUG__ = {
|
||||||
|
getGlobalUniver: () => UniverseManager.getInstance(),
|
||||||
|
getGlobalState: () => getGlobalState(),
|
||||||
|
forceReset: () => UniverseManager.forceReset(),
|
||||||
|
completeCleanup: () => UniverseManager.completeCleanup(),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### **Error Resolution Steps**
|
||||||
|
1. **Immediate**: Call `window.__UNIVER_DEBUG__.completeCleanup()` in console
|
||||||
|
2. **Prevention**: Use empty dependency arrays in useEffect for initialization
|
||||||
|
3. **Validation**: Always check both `instance` and `univerAPI` existence
|
||||||
|
4. **Cleanup**: Implement thorough REDI global state cleanup in disposal methods
|
||||||
|
|
||||||
|
### **Common Anti-Patterns to Avoid**
|
||||||
|
- ❌ Re-initializing on every component render
|
||||||
|
- ❌ Not waiting for ongoing initialization/disposal operations
|
||||||
|
- ❌ Incomplete state validation before reuse
|
||||||
|
- ❌ Missing REDI global state cleanup
|
||||||
|
- ❌ Using complex dependency arrays in initialization useEffect
|
||||||
|
|
||||||
|
### **Best Practices**
|
||||||
|
- ✅ Implement singleton pattern with proper state management
|
||||||
|
- ✅ Use polling-based waiting for state transitions
|
||||||
|
- ✅ Provide debug tools for runtime state inspection
|
||||||
|
- ✅ Separate initialization concerns from component lifecycle
|
||||||
|
- ✅ Implement both standard and emergency cleanup methods
|
||||||
20
src/App.tsx
20
src/App.tsx
@@ -1,7 +1,12 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "./components/ui/button";
|
import { Button } from "./components/ui/button";
|
||||||
import HomeButton from "./components/ui/homeButton";
|
import HomeButton from "./components/ui/homeButton";
|
||||||
import EditSheetViewer from "./components/sheet/EditSheetViewer";
|
import { lazy, Suspense } from "react";
|
||||||
|
|
||||||
|
// 동적 import로 EditSheetViewer 로드 (필요할 때만)
|
||||||
|
const EditSheetViewer = lazy(
|
||||||
|
() => import("./components/sheet/EditSheetViewer"),
|
||||||
|
);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [showTestViewer, setShowTestViewer] = useState(false);
|
const [showTestViewer, setShowTestViewer] = useState(false);
|
||||||
@@ -52,9 +57,20 @@ function App() {
|
|||||||
{/* 메인 콘텐츠 */}
|
{/* 메인 콘텐츠 */}
|
||||||
<main className="h-[calc(100vh-4rem)]">
|
<main className="h-[calc(100vh-4rem)]">
|
||||||
{showTestViewer ? (
|
{showTestViewer ? (
|
||||||
// 테스트 뷰어 표시
|
// 동적 로딩된 테스트 뷰어 표시
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="h-full flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
||||||
|
<p className="text-gray-600">🚀 그리드 그리는 중...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<EditSheetViewer />
|
<EditSheetViewer />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// 메인 페이지
|
// 메인 페이지
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ declare global {
|
|||||||
getGlobalState: () => GlobalUniverState;
|
getGlobalState: () => GlobalUniverState;
|
||||||
clearGlobalState: () => void;
|
clearGlobalState: () => void;
|
||||||
forceReset: () => void;
|
forceReset: () => void;
|
||||||
|
completeCleanup: () => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,6 +214,33 @@ const UniverseManager = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// REDI 전역 상태 완전 정리 시도
|
||||||
|
try {
|
||||||
|
// 브라우저의 전역 REDI 상태 정리 (가능한 경우)
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
// REDI 관련 전역 객체들 정리
|
||||||
|
const globalKeys = Object.keys(window).filter(
|
||||||
|
(key) =>
|
||||||
|
key.includes("redi") ||
|
||||||
|
key.includes("REDI") ||
|
||||||
|
key.includes("univerjs") ||
|
||||||
|
key.includes("univer"),
|
||||||
|
);
|
||||||
|
|
||||||
|
globalKeys.forEach((key) => {
|
||||||
|
try {
|
||||||
|
delete (window as any)[key];
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`전역 키 ${key} 정리 실패:`, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🧹 REDI 전역 상태 정리 시도 완료");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("⚠️ REDI 전역 상태 정리 중 오류:", error);
|
||||||
|
}
|
||||||
|
|
||||||
state.instance = null;
|
state.instance = null;
|
||||||
state.univerAPI = null;
|
state.univerAPI = null;
|
||||||
state.isInitializing = false;
|
state.isInitializing = false;
|
||||||
@@ -221,13 +249,46 @@ const UniverseManager = {
|
|||||||
state.lastContainerId = null;
|
state.lastContainerId = null;
|
||||||
window[GLOBAL_UNIVER_KEY] = null;
|
window[GLOBAL_UNIVER_KEY] = null;
|
||||||
|
|
||||||
console.log("🔄 전역 Univer 상태 강제 리셋 완료");
|
console.log("🔄 전역 Univer 상태 강제 리셋 완료 (REDI 정리 포함)");
|
||||||
|
},
|
||||||
|
|
||||||
|
// 완전한 정리 메서드 (REDI 포함)
|
||||||
|
async completeCleanup(): Promise<void> {
|
||||||
|
const state = getGlobalState();
|
||||||
|
|
||||||
|
if (state.isDisposing) {
|
||||||
|
console.log("🔄 이미 정리 진행 중...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.isDisposing = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log("🗑️ 완전한 정리 시작 (REDI 포함)");
|
||||||
|
|
||||||
|
if (state.instance) {
|
||||||
|
await state.instance.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 약간의 대기 후 REDI 상태 정리
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// REDI 전역 상태 정리 시도
|
||||||
|
this.forceReset();
|
||||||
|
|
||||||
|
console.log("✅ 완전한 정리 완료");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ 완전한 정리 실패:", error);
|
||||||
|
} finally {
|
||||||
|
state.isDisposing = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 전역 디버그 객체 설정
|
// 전역 디버그 객체 설정을 위한 헬퍼 함수 (모듈 레벨 실행 방지)
|
||||||
if (typeof window !== "undefined") {
|
const setupDebugTools = (): void => {
|
||||||
// 전역 상태 초기화
|
if (typeof window !== "undefined" && !window.__UNIVER_DEBUG__) {
|
||||||
|
// 전역 상태 초기화 (필요한 경우에만)
|
||||||
initializeGlobalState();
|
initializeGlobalState();
|
||||||
|
|
||||||
// 디버그 객체 설정
|
// 디버그 객체 설정
|
||||||
@@ -248,10 +309,12 @@ if (typeof window !== "undefined") {
|
|||||||
console.log("🧹 전역 상태 정리 완료");
|
console.log("🧹 전역 상태 정리 완료");
|
||||||
},
|
},
|
||||||
forceReset: () => UniverseManager.forceReset(),
|
forceReset: () => UniverseManager.forceReset(),
|
||||||
|
completeCleanup: () => UniverseManager.completeCleanup(),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("🐛 디버그 객체 설정 완료: window.__UNIVER_DEBUG__");
|
console.log("🐛 디버그 객체 설정 완료: window.__UNIVER_DEBUG__");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Univer CE + 파일 업로드 오버레이
|
* Univer CE + 파일 업로드 오버레이
|
||||||
@@ -520,28 +583,76 @@ const TestSheetViewer: React.FC = () => {
|
|||||||
mountedRef.current = true;
|
mountedRef.current = true;
|
||||||
console.log("🎯 컴포넌트 마운트됨");
|
console.log("🎯 컴포넌트 마운트됨");
|
||||||
|
|
||||||
// 기존 전역 인스턴스 확인 및 재사용
|
// 디버그 도구 설정 (컴포넌트 마운트 시에만)
|
||||||
|
setupDebugTools();
|
||||||
|
|
||||||
|
// 강화된 기존 인스턴스 확인 및 재사용
|
||||||
const existingUniver = UniverseManager.getInstance();
|
const existingUniver = UniverseManager.getInstance();
|
||||||
if (existingUniver && !UniverseManager.isInitializing()) {
|
const state = getGlobalState();
|
||||||
console.log("♻️ 기존 전역 Univer 인스턴스 재사용");
|
|
||||||
|
// 기존 인스턴스가 있고 정상 상태면 재사용
|
||||||
|
if (
|
||||||
|
existingUniver &&
|
||||||
|
state.univerAPI &&
|
||||||
|
!UniverseManager.isInitializing() &&
|
||||||
|
!UniverseManager.isDisposing()
|
||||||
|
) {
|
||||||
|
console.log("♻️ 기존 전역 Univer 인스턴스와 univerAPI 재사용");
|
||||||
setIsInitialized(true);
|
setIsInitialized(true);
|
||||||
|
|
||||||
|
// 기존 인스턴스의 컨테이너가 현재 컨테이너와 다른 경우 갱신
|
||||||
|
if (
|
||||||
|
containerRef.current &&
|
||||||
|
state.lastContainerId !== containerRef.current.id
|
||||||
|
) {
|
||||||
|
console.log("🔄 컨테이너 정보 갱신");
|
||||||
|
state.lastContainerId = containerRef.current.id;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 컨테이너 준비 후 초기화
|
// 초기화 중이거나 정리 중인 경우 대기
|
||||||
const initTimer = setTimeout(() => {
|
if (UniverseManager.isInitializing() || UniverseManager.isDisposing()) {
|
||||||
if (containerRef.current && !UniverseManager.isInitializing()) {
|
console.log("⏳ 초기화/정리 중이므로 대기");
|
||||||
console.log("🚀 컴포넌트 마운트 시 Univer 초기화");
|
const waitTimer = setInterval(() => {
|
||||||
|
if (
|
||||||
|
!UniverseManager.isInitializing() &&
|
||||||
|
!UniverseManager.isDisposing()
|
||||||
|
) {
|
||||||
|
clearInterval(waitTimer);
|
||||||
|
const currentUniver = UniverseManager.getInstance();
|
||||||
|
if (currentUniver && getGlobalState().univerAPI) {
|
||||||
|
console.log("✅ 대기 후 기존 인스턴스 재사용");
|
||||||
|
setIsInitialized(true);
|
||||||
|
} else if (containerRef.current && mountedRef.current) {
|
||||||
|
console.log("🚀 대기 후 새 인스턴스 초기화");
|
||||||
initializeUniver();
|
initializeUniver();
|
||||||
}
|
}
|
||||||
}, 100); // 짧은 지연으로 DOM 완전 준비 보장
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
return () => clearInterval(waitTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 완전히 새로운 초기화가 필요한 경우만 진행
|
||||||
|
const initTimer = setTimeout(() => {
|
||||||
|
// 마운트 상태와 컨테이너 재확인
|
||||||
|
if (
|
||||||
|
containerRef.current &&
|
||||||
|
mountedRef.current &&
|
||||||
|
!UniverseManager.isInitializing()
|
||||||
|
) {
|
||||||
|
console.log("🚀 컴포넌트 마운트 시 새 Univer 초기화");
|
||||||
|
initializeUniver();
|
||||||
|
}
|
||||||
|
}, 100); // DOM 완전 준비 보장
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(initTimer);
|
clearTimeout(initTimer);
|
||||||
mountedRef.current = false;
|
mountedRef.current = false;
|
||||||
console.log("👋 컴포넌트 언마운트됨");
|
console.log("👋 컴포넌트 언마운트됨");
|
||||||
};
|
};
|
||||||
}, [initializeUniver]);
|
}, []); // 의존성 배열을 빈 배열로 변경하여 한 번만 실행
|
||||||
|
|
||||||
// 컴포넌트 언마운트 시 정리 (전역 인스턴스는 유지)
|
// 컴포넌트 언마운트 시 정리 (전역 인스턴스는 유지)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user