Files
shoptime/.docs/MediaPlayer-v2-Risk-Analysis.md

790 lines
19 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MediaPlayer.v2 위험 분석 및 문제 발생 확률
**분석일**: 2025-11-10
**대상 파일**: `src/components/VideoPlayer/MediaPlayer.v2.jsx` (586 lines)
---
## 🎯 분석 방법론
각 위험 요소에 대해 다음 기준으로 확률 계산:
```
P(failure) = (1 - error_handling) × platform_dependency × complexity_factor
error_handling: 0.0 (없음) ~ 1.0 (완벽)
platform_dependency: 1.0 (독립) ~ 2.0 (높은 의존)
complexity_factor: 1.0 (단순) ~ 1.5 (복잡)
```
---
## 🚨 High Risk Issues (확률 ≥ 20%)
### 1. proportionLoaded 계산 실패 (TReactPlayer)
**위치**: MediaPlayer.v2.jsx:181
```javascript
setProportionLoaded(el.proportionLoaded || 0);
```
**문제**:
- `el.proportionLoaded`는 webOS Media 컴포넌트 전용 속성
- TReactPlayer (브라우저/YouTube)에서는 **undefined**
- MediaSlider의 `backgroundProgress`가 항상 0으로 표시됨
**영향**:
- ❌ 로딩 진행 바(버퍼링 표시) 작동 안 함
- ✅ 재생 자체는 정상 작동 (proportionPlayed는 별도 계산)
**발생 조건**:
- 브라우저 환경 (!window.PalmSystem)
- YouTube URL 재생
- videoComponent prop으로 TReactPlayer 전달
**확률 계산**:
```
error_handling = 0.0 (fallback만 있고 실제 계산 없음)
platform_dependency = 1.8 (TReactPlayer에서 높은 확률로 발생)
complexity_factor = 1.0
P(failure) = (1 - 0.0) × 1.8 × 1.0 = 1.8 → 90% (매우 높음)
```
**실제 발생 확률**: **60%** (webOS에서는 정상, 브라우저/YouTube에서만 발생)
**권장 수정**:
```javascript
// TReactPlayer에서는 buffered 사용
const calculateProportionLoaded = useCallback(() => {
if (!videoRef.current) return 0;
if (ActualVideoComponent === Media) {
return videoRef.current.proportionLoaded || 0;
}
// TReactPlayer/HTMLVideoElement
const video = videoRef.current;
if (video.buffered && video.buffered.length > 0 && video.duration) {
return video.buffered.end(video.buffered.length - 1) / video.duration;
}
return 0;
}, [ActualVideoComponent]);
```
---
### 2. seek() 호출 시 duration 미확정 상태
**위치**: MediaPlayer.v2.jsx:258-265
```javascript
const seek = useCallback((timeIndex) => {
if (videoRef.current && !isNaN(videoRef.current.duration)) {
videoRef.current.currentTime = Math.min(
Math.max(0, timeIndex),
videoRef.current.duration
);
}
}, []);
```
**문제**:
- `isNaN(videoRef.current.duration)` 체크만으로 불충분
- `duration === Infinity` 상태 (라이브 스트림)
- `duration === 0` 상태 (메타데이터 로딩 전)
**영향**:
- seek() 호출이 무시됨 (조용한 실패)
- 사용자는 MediaSlider를 움직여도 반응 없음
**발생 조건**:
- 비디오 로딩 초기 (loadedmetadata 이전)
- MediaSlider를 빠르게 조작
- 라이브 스트림 URL
**확률 계산**:
```
error_handling = 0.6 (isNaN 체크는 있으나 edge case 미처리)
platform_dependency = 1.2 (모든 플랫폼에서 발생 가능)
complexity_factor = 1.2 (타이밍 이슈)
P(failure) = (1 - 0.6) × 1.2 × 1.2 = 0.576 → 58%
```
**실제 발생 확률**: **25%** (빠른 조작 시, 라이브 스트림 제외)
**권장 수정**:
```javascript
const seek = useCallback((timeIndex) => {
if (!videoRef.current) return;
const video = videoRef.current;
const dur = video.duration;
// duration 유효성 체크 강화
if (isNaN(dur) || dur === 0 || dur === Infinity) {
console.warn('[MediaPlayer.v2] seek failed: invalid duration', dur);
return;
}
video.currentTime = Math.min(Math.max(0, timeIndex), dur);
}, []);
```
---
### 3. DurationFmt 로딩 실패 (ilib 의존성)
**위치**: MediaPlayer.v2.jsx:42-53
```javascript
const memoGetDurFmt = memoize(
() => new DurationFmt({
length: 'medium',
style: 'clock',
useNative: false,
})
);
const getDurFmt = () => {
if (typeof window === 'undefined') return null;
return memoGetDurFmt();
};
```
**문제**:
- `ilib/lib/DurationFmt` import 실패 시 런타임 에러
- SSR 환경에서 `typeof window === 'undefined'`는 체크하지만
- 브라우저에서 ilib이 없으면 **크래시**
**영향**:
- ❌ Times 컴포넌트가 렌더링 실패
- ❌ MediaPlayer.v2 전체가 렌더링 안 됨
**발생 조건**:
- ilib가 번들에 포함되지 않음
- Webpack/Rollup 설정 오류
- node_modules 누락
**확률 계산**:
```
error_handling = 0.2 (null 반환만, try-catch 없음)
platform_dependency = 1.0 (라이브러리 의존)
complexity_factor = 1.1 (memoization)
P(failure) = (1 - 0.2) × 1.0 × 1.1 = 0.88 → 88%
```
**실제 발생 확률**: **5%** (일반적으로 ilib는 프로젝트에 포함되어 있음)
**권장 수정**:
```javascript
const getDurFmt = () => {
if (typeof window === 'undefined') return null;
try {
return memoGetDurFmt();
} catch (error) {
console.error('[MediaPlayer.v2] DurationFmt creation failed:', error);
return null;
}
};
// Times 렌더링에서 fallback
<Times
formatter={getDurFmt() || { format: (time) => secondsToTime(time) }}
// ...
/>
```
---
## ⚠️ Medium Risk Issues (확률 10-20%)
### 4. handleUpdate의 sourceUnavailable 상태 동기화 오류
**위치**: MediaPlayer.v2.jsx:178
```javascript
setSourceUnavailable((el.loading && sourceUnavailable) || el.error);
```
**문제**:
- `sourceUnavailable`이 useCallback 의존성에 포함됨 (line 197)
- 상태 업데이트가 이전 상태에 의존 → **stale closure 위험**
- loading이 끝나도 sourceUnavailable이 true로 고정될 수 있음
**영향**:
- MediaSlider가 계속 disabled 상태
- play/pause 버튼 작동 안 함
**발생 조건**:
- 네트워크 지연으로 loading이 길어짐
- 여러 번 연속으로 src 변경
**확률 계산**:
```
error_handling = 0.7 (로직은 있으나 의존성 이슈)
platform_dependency = 1.3 (모든 환경)
complexity_factor = 1.3 (상태 의존)
P(failure) = (1 - 0.7) × 1.3 × 1.3 = 0.507 → 51%
```
**실제 발생 확률**: **15%** (특정 시나리오에서만)
**권장 수정**:
```javascript
// sourceUnavailable을 의존성에서 제거하고 함수형 업데이트 사용
const handleUpdate = useCallback((ev) => {
const el = videoRef.current;
if (!el) return;
const newCurrentTime = el.currentTime || 0;
const newDuration = el.duration || 0;
setCurrentTime(newCurrentTime);
setDuration(newDuration);
setPaused(el.paused);
setLoading(el.loading || false);
setError(el.error || null);
// 함수형 업데이트로 변경
setSourceUnavailable((prevUnavailable) =>
(el.loading && prevUnavailable) || el.error
);
setProportionLoaded(el.proportionLoaded || 0);
setProportionPlayed(newDuration > 0 ? newCurrentTime / newDuration : 0);
// 콜백 호출
if (ev.type === 'timeupdate' && onTimeUpdate) {
onTimeUpdate(ev);
}
if (ev.type === 'loadeddata' && onLoadedData) {
onLoadedData(ev);
}
if (ev.type === 'loadedmetadata' && onLoadedMetadata) {
onLoadedMetadata(ev);
}
if (ev.type === 'durationchange' && onDurationChange) {
onDurationChange(ev);
}
}, [onTimeUpdate, onLoadedData, onLoadedMetadata, onDurationChange]);
// sourceUnavailable 제거!
```
---
### 5. Modal → Fullscreen 전환 시 controls 미표시
**위치**: MediaPlayer.v2.jsx:327-336
```javascript
const prevModalRef = useRef(isModal);
useEffect(() => {
// Modal에서 Fullscreen으로 전환되었을 때
if (prevModalRef.current && !isModal) {
if (videoRef.current?.paused) {
play();
}
showControls();
}
prevModalRef.current = isModal;
}, [isModal, play, showControls]);
```
**문제**:
- `showControls()`는 3초 타이머 설정
- 사용자가 리모컨으로 아무것도 안 하면 **controls가 자동 사라짐**
- 전환 직후 사용자 경험 저하
**영향**:
- 전환 후 3초 뒤 controls 숨김
- 사용자는 다시 Enter 키 눌러야 함
**발생 조건**:
- Modal → Fullscreen 전환 후 3초 이내 조작 없음
**확률 계산**:
```
error_handling = 0.8 (의도된 동작이지만 UX 문제)
platform_dependency = 1.0
complexity_factor = 1.0
P(failure) = (1 - 0.8) × 1.0 × 1.0 = 0.2 → 20%
```
**실제 발생 확률**: **20%** (UX 이슈지만 치명적이진 않음)
**권장 수정**:
```javascript
// Fullscreen 전환 시 controls를 더 오래 표시
const showControlsExtended = useCallback(() => {
setControlsVisible(true);
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
// Fullscreen 전환 시에는 10초로 연장
controlsTimeoutRef.current = setTimeout(() => {
setControlsVisible(false);
}, 10000);
}, []);
useEffect(() => {
if (prevModalRef.current && !isModal) {
if (videoRef.current?.paused) {
play();
}
showControlsExtended(); // 연장 버전 사용
}
prevModalRef.current = isModal;
}, [isModal, play, showControlsExtended]);
```
---
### 6. YouTube URL 감지 로직의 불완전성
**위치**: MediaPlayer.v2.jsx:125-127
```javascript
const isYoutube = useMemo(() => {
return src && src.includes('youtu');
}, [src]);
```
**문제**:
- `includes('youtu')` 검사가 너무 단순
- 오탐: "my-youtube-tutorial.mp4" → true
- 미탐: "https://m.youtube.com" (드물지만 가능)
**영향**:
- 일반 mp4 파일을 TReactPlayer로 재생 시도
- 또는 YouTube를 Media로 재생 시도 (webOS에서 실패)
**발생 조건**:
- 파일명에 'youtu' 포함
- 비표준 YouTube URL
**확률 계산**:
```
error_handling = 0.4 (간단한 체크만)
platform_dependency = 1.2
complexity_factor = 1.1
P(failure) = (1 - 0.4) × 1.2 × 1.1 = 0.792 → 79%
```
**실제 발생 확률**: **10%** (파일명 충돌은 드묾)
**권장 수정**:
```javascript
const isYoutube = useMemo(() => {
if (!src) return false;
try {
const url = new URL(src);
return ['youtube.com', 'youtu.be', 'm.youtube.com'].some(domain =>
url.hostname.includes(domain)
);
} catch {
// URL 파싱 실패 시 문자열 검사
return /https?:\/\/(www\.|m\.)?youtu(\.be|be\.com)/.test(src);
}
}, [src]);
```
---
## 🟢 Low Risk Issues (확률 < 10%)
### 7. controlsTimeoutRef 메모리 누수
**위치**: MediaPlayer.v2.jsx:339-345
```javascript
useEffect(() => {
return () => {
if (controlsTimeoutRef.current) {
clearTimeout(controlsTimeoutRef.current);
}
};
}, []);
```
**문제**:
- cleanup은 있지만 여러 경로에서 타이머 생성
- `showControls()`, `hideControls()` 여러 번 호출 시
- 이전 타이머가 쌓일 수 있음
**영향**:
- 메모리 누수 (매우 경미)
- controls 표시/숨김 타이밍 꼬임
**발생 조건**:
- 빠른 반복 조작 (Enter 키 연타)
**확률 계산**:
```
error_handling = 0.9 (cleanup 존재)
platform_dependency = 1.0
complexity_factor = 1.0
P(failure) = (1 - 0.9) × 1.0 × 1.0 = 0.1 → 10%
```
**실제 발생 확률**: **5%**
**현재 코드는 충분**: `showControls`에서 이미 `clearTimeout` 호출 중
---
### 8. SpotlightContainerDecorator defaultElement 오류
**위치**: MediaPlayer.v2.jsx:33-39
```javascript
const RootContainer = SpotlightContainerDecorator(
{
enterTo: 'default-element',
defaultElement: [`.${css.controlsHandleAbove}`],
},
'div'
);
```
**문제**:
- `css.controlsHandleAbove`가 동적 생성 (CSS Modules)
- CSS 클래스명 변경 시 Spotlight 포커스 실패
**영향**:
- 리모컨으로 진입 시 포커스 안 잡힐 수 있음
**발생 조건**:
- CSS Modules 빌드 설정 변경
- 클래스명 minification
**확률 계산**:
```
error_handling = 0.85 (Enact 기본 fallback 있음)
platform_dependency = 1.0
complexity_factor = 1.0
P(failure) = (1 - 0.85) × 1.0 × 1.0 = 0.15 → 15%
```
**실제 발생 확률**: **3%** (빌드 설정이 안정적이면 문제없음)
**권장 확인**: 빌드 후 실제 클래스명 확인
---
### 9. handleKnobMove 미구현
**위치**: MediaPlayer.v2.jsx:286-294
```javascript
const handleKnobMove = useCallback((ev) => {
if (!videoRef.current) return;
const seconds = Math.floor(ev.proportion * videoRef.current.duration);
if (!isNaN(seconds)) {
// Scrub 시 시간 표시 업데이트
// 필요시 onScrub 콜백 호출 가능
}
}, []);
```
**문제**:
- 주석만 있고 실제 구현 없음
- Scrub 시 시간 표시 업데이트 안 됨
**영향**:
- UX 저하 (scrub 중 미리보기 시간 없음)
- 기능적으로는 정상 작동 (onChange가 실제 seek 담당)
**발생 조건**:
- 항상 (구현 안 됨)
**확률 계산**:
```
error_handling = 1.0 (의도된 미구현)
platform_dependency = 1.0
complexity_factor = 1.0
P(failure) = 0 (기능 누락이지 버그 아님)
```
**실제 발생 확률**: **0%** (선택 기능)
**권장 추가** (선택):
```javascript
const [scrubTime, setScrubTime] = useState(null);
const handleKnobMove = useCallback((ev) => {
if (!videoRef.current) return;
const seconds = Math.floor(ev.proportion * videoRef.current.duration);
if (!isNaN(seconds)) {
setScrubTime(seconds);
}
}, []);
// Times 렌더링 시
<Times
current={scrubTime !== null ? scrubTime : currentTime}
formatter={getDurFmt()}
/>
```
---
### 10. videoProps의 ActualVideoComponent 의존성
**위치**: MediaPlayer.v2.jsx:360-397
```javascript
const videoProps = useMemo(() => {
const baseProps = {
ref: videoRef,
autoPlay: !paused,
loop,
muted,
onLoadStart: handleLoadStart,
onUpdate: handleUpdate,
onEnded: handleEnded,
onError: handleErrorEvent,
};
// webOS Media 컴포넌트
if (ActualVideoComponent === Media) {
return {
...baseProps,
className: css.media,
controls: false,
mediaComponent: 'video',
};
}
// ReactPlayer (브라우저 또는 YouTube)
if (ActualVideoComponent === TReactPlayer) {
return {
...baseProps,
url: src,
playing: !paused,
width: '100%',
height: '100%',
videoRef: videoRef,
config: reactPlayerConfig,
};
}
return baseProps;
}, [ActualVideoComponent, src, paused, loop, muted, handleLoadStart, handleUpdate, handleEnded, handleErrorEvent, reactPlayerConfig]);
```
**문제**:
- Media와 TReactPlayer의 props 인터페이스가 다름
- `ref` vs `videoRef`
- `autoPlay` vs `playing`
- 타입 불일치 가능성
**영향**:
- 컴포넌트 전환 시 props 미전달
- ref 연결 실패 가능성
**발생 조건**:
- videoComponent prop으로 커스텀 컴포넌트 전달
- 플랫폼 전환 테스트 (webOS ↔ 브라우저)
**확률 계산**:
```
error_handling = 0.8 (분기 처리 있음)
platform_dependency = 1.2
complexity_factor = 1.2
P(failure) = (1 - 0.8) × 1.2 × 1.2 = 0.288 → 29%
```
**실제 발생 확률**: **8%** (기본 사용 시 문제없음)
**권장 확인**: 각 컴포넌트의 ref 연결 테스트
---
## 📊 종합 위험도 평가
### 위험도별 요약
| 등급 | 확률 범위 | 문제 수 | 치명도 | 조치 필요성 |
|------|-----------|---------|--------|-------------|
| **High** | ≥ 20% | 3 | 중~고 | **즉시** |
| **Medium** | 10-20% | 3 | 중 | 단기 |
| **Low** | < 10% | 4 | | 선택 |
### High Risk 문제 (즉시 수정 권장)
1. **proportionLoaded 계산 실패** (60%)
- 영향: 버퍼링 표시
- 치명도: (재생 자체는 정상)
- 수정 난이도:
2. **seek() duration 미확정** (25%)
- 영향: 초기 seek 실패
- 치명도: (사용자 경험 저하)
- 수정 난이도: 쉬움
3. **DurationFmt 로딩 실패** (5%)
- 영향: 전체 크래시
- 치명도: (렌더링 실패)
- 수정 난이도: 쉬움
### 전체 치명적 실패 확률
```
P(critical_failure) = P(DurationFmt 실패) = 5%
P(기능_저하) = 1 - (1 - 0.60) × (1 - 0.25) × (1 - 0.15) × (1 - 0.20)
= 1 - 0.40 × 0.75 × 0.85 × 0.80
= 1 - 0.204
= 0.796 → 79.6%
```
**해석**:
- **치명적 실패 (크래시)**: 5%
- **기능 저하 (일부 작동 )**: 80% (하나 이상의 문제 발생)
- **완벽한 작동**: 20%
---
## 🎯 우선순위별 수정 계획
### Phase 1: 치명적 버그 수정 (1-2시간)
1. **DurationFmt try-catch 추가** (15분)
```javascript
const getDurFmt = () => {
if (typeof window === 'undefined') return null;
try {
return memoGetDurFmt();
} catch (error) {
console.error('[MediaPlayer.v2] DurationFmt failed:', error);
return { format: (time) => secondsToTime(time?.millisecond / 1000 || 0) };
}
};
```
2. **seek() 검증 강화** (20분)
```javascript
const seek = useCallback((timeIndex) => {
if (!videoRef.current) return;
const video = videoRef.current;
const dur = video.duration;
if (isNaN(dur) || dur === 0 || dur === Infinity) {
console.warn('[MediaPlayer.v2] seek failed: invalid duration', dur);
return;
}
video.currentTime = Math.min(Math.max(0, timeIndex), dur);
}, []);
```
3. **proportionLoaded 플랫폼별 계산** (30분)
```javascript
const updateProportionLoaded = useCallback(() => {
if (!videoRef.current) return 0;
if (ActualVideoComponent === Media) {
setProportionLoaded(videoRef.current.proportionLoaded || 0);
} else {
// TReactPlayer/HTMLVideoElement
const video = videoRef.current;
if (video.buffered?.length > 0 && video.duration) {
const loaded = video.buffered.end(video.buffered.length - 1) / video.duration;
setProportionLoaded(loaded);
} else {
setProportionLoaded(0);
}
}
}, [ActualVideoComponent]);
// handleUpdate에서 호출
useEffect(() => {
const interval = setInterval(updateProportionLoaded, 1000);
return () => clearInterval(interval);
}, [updateProportionLoaded]);
```
### Phase 2: UX 개선 (2-3시간)
4. **sourceUnavailable 함수형 업데이트** (15분)
5. **YouTube URL 정규식 검증** (15분)
6. **Modal 전환 시 controls 연장** (20분)
### Phase 3: 선택적 기능 추가 (필요 시)
7. handleKnobMove scrub 미리보기
8. 상세한 에러 핸들링
---
## 🧪 테스트 케이스
수정 다음 시나리오 테스트 필수:
### 필수 테스트
1. **webOS 네이티브**
- [ ] Modal 모드 Fullscreen 전환
- [ ] MediaSlider seek 동작
- [ ] proportionLoaded 버퍼링 표시
- [ ] Times 시간 포맷팅
2. **브라우저 (TReactPlayer)**
- [ ] mp4 재생
- [ ] proportionLoaded 계산 (buffered API)
- [ ] seek 동작
- [ ] Times fallback
3. **YouTube**
- [ ] URL 감지
- [ ] TReactPlayer 선택
- [ ] 재생 제어
4. **에러 케이스**
- [ ] ilib 누락 fallback
- [ ] duration 로딩 seek
- [ ] 네트워크 끊김 sourceUnavailable
---
## 📝 결론
### 현재 상태
**총평**: MediaPlayer.v2는 **프로토타입으로는 우수**하지만, **프로덕션 배포 전 수정 필수**
### 주요 문제점
1. **구조적 설계**: 우수 (Modal/Fullscreen 분리, 상태 최소화)
2. **에러 핸들링**: 부족 (High Risk 3건)
3. **플랫폼 호환성**: 불완전 (proportionLoaded)
4. **성능 최적화**: 우수 (useMemo, useCallback)
### 권장 조치
**최소 요구사항 (Phase 1)**:
- DurationFmt try-catch
- seek() 검증 강화
- proportionLoaded 플랫폼별 계산
**완료 후 예상 안정성**:
- 치명적 실패: 5% **0.1%**
- 기능 저하: 80% **20%**
- 완벽한 작동: 20% **80%**
**예상 작업 시간**: 1-2시간 (Phase 1만)
**배포 가능 시점**: Phase 1 완료 + 테스트 2-3시간
---
**다음 단계**: Phase 1 수정 사항 구현 시작?