feat(deepfield): 렌더 모드 판정(TDD) + useFieldMode 훅

This commit is contained in:
2026-06-13 00:05:17 +09:00
parent 5cfa124d38
commit bd13641f5e
3 changed files with 72 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
'use client';
import { useEffect, useState } from 'react';
import { decideFieldMode, type FieldMode } from '@/lib/deepfield-mode';
function detectWebGL(): boolean {
try {
const canvas = document.createElement('canvas');
return Boolean(canvas.getContext('webgl2') ?? canvas.getContext('webgl'));
} catch {
return false;
}
}
/** SSR/첫 페인트는 'static'으로 시작 — 클라이언트에서 승격 (hydration 불일치 방지) */
export function useFieldMode(): FieldMode {
const [mode, setMode] = useState<FieldMode>('static');
useEffect(() => {
setMode(
decideFieldMode({
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
webglSupported: detectWebGL(),
hardwareConcurrency: navigator.hardwareConcurrency ?? 0,
viewportWidth: window.innerWidth,
}),
);
}, []);
return mode;
}

View File

@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import { decideFieldMode } from '@/lib/deepfield-mode';
const base = { reducedMotion: false, webglSupported: true, hardwareConcurrency: 8, viewportWidth: 1440 };
describe('decideFieldMode', () => {
it('데스크톱 + WebGL = full', () => {
expect(decideFieldMode(base)).toBe('full');
});
it('reduced-motion이면 무조건 static', () => {
expect(decideFieldMode({ ...base, reducedMotion: true })).toBe('static');
expect(decideFieldMode({ ...base, reducedMotion: true, viewportWidth: 375 })).toBe('static');
});
it('WebGL 미지원이면 static', () => {
expect(decideFieldMode({ ...base, webglSupported: false })).toBe('static');
});
it('모바일 뷰포트(<768)는 lite', () => {
expect(decideFieldMode({ ...base, viewportWidth: 767 })).toBe('lite');
});
it('저성능 코어(<4)는 lite', () => {
expect(decideFieldMode({ ...base, hardwareConcurrency: 2 })).toBe('lite');
});
it('hardwareConcurrency 미보고(0/undefined)는 lite로 보수적 판정', () => {
expect(decideFieldMode({ ...base, hardwareConcurrency: 0 })).toBe('lite');
});
});

17
lib/deepfield-mode.ts Normal file
View File

@@ -0,0 +1,17 @@
export type FieldMode = 'full' | 'lite' | 'static';
export interface FieldEnv {
reducedMotion: boolean;
webglSupported: boolean;
hardwareConcurrency: number; // 미보고 시 0
viewportWidth: number;
}
/** Deep Field 렌더 모드 판정 — 우선순위: 접근성 > 지원 여부 > 성능 */
export function decideFieldMode(env: FieldEnv): FieldMode {
if (env.reducedMotion) return 'static';
if (!env.webglSupported) return 'static';
if (env.viewportWidth < 768) return 'lite';
if (!env.hardwareConcurrency || env.hardwareConcurrency < 4) return 'lite';
return 'full';
}