Skip to main content
민트라

브라우저 호환성 지옥 - WebGL이 안 되는 환경들

"제 폰에서는 안 보여요." QA 중에 받은 메시지였습니다. 저는 크롬, 사파리, 파이어폭스에서 테스트했고 모두 잘 작동했습니다. 하지만 세상에는 더 많은 환경이 있었죠.

첫 번째 사건: 삼성 인터넷 브라우저 #

삼성 갤럭시의 기본 브라우저인 삼성 인터넷에서 3D 씬이 전혀 보이지 않았습니다.

원인 파악 #

콘솔에 에러가 있었습니다.

THREE.WebGLRenderer: Error creating WebGL context.

WebGL 컨텍스트 생성에 실패했습니다. 하지만 같은 폰의 크롬에서는 잘 됐습니다.

해결 #

삼성 인터넷의 특정 버전에서 WebGL2가 불안정했습니다. WebGL1으로 폴백하도록 수정했습니다.

const renderer = new THREE.WebGLRenderer({
  canvas: canvas,
  context: canvas.getContext('webgl') || canvas.getContext('experimental-webgl'),
  antialias: true
});

또한 삼성 인터넷의 "절전 모드"가 WebGL을 비활성화한다는 것을 알게 되었습니다. 사용자에게 안내 메시지를 추가했습니다.

두 번째 사건: iOS 사파리 메모리 제한 #

아이폰 SE 사용자에게서 신고가 들어왔습니다. 사이트가 열리다가 갑자기 새로고침된다고요.

원인 파악 #

iOS 사파리는 메모리 사용량이 일정 수준을 넘으면 탭을 강제로 리로드합니다. 제 사이트가 그 한계를 넘고 있었습니다.

// 텍스처 메모리 확인
console.log(renderer.info.memory);
// { geometries: 45, textures: 23 }

텍스처 23개가 각각 4K 해상도였습니다. 총 텍스처 메모리만 약 800MB.

해결 #

텍스처 해상도를 디바이스에 따라 동적으로 조절했습니다.

function getTextureSize() {
  const isLowMemory = navigator.deviceMemory && navigator.deviceMemory < 4;
  const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent);

  if (isLowMemory || isIOS) {
    return 512;  // 저해상도
  }
  return 2048;  // 고해상도
}

const textureSize = getTextureSize();
textureLoader.load(`/textures/wood-${textureSize}.jpg`, (texture) => {
  // ...
});

세 번째 사건: 회사 PC의 인텔 내장 그래픽 #

기업용 PC에서 테스트해달라는 요청이 있었습니다. 결과는 처참했습니다. 2fps.

원인 파악 #

const gl = renderer.getContext();
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
console.log(gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL));
// "Intel(R) UHD Graphics 620"

인텔 내장 그래픽은 3D 성능이 제한적입니다. 특히 후처리 효과(Bloom, SSAO 등)가 치명적이었습니다.

해결 #

GPU 벤더를 감지해서 품질을 자동 조절하도록 했습니다.

function detectGPUTier() {
  const gl = renderer.getContext();
  const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');

  if (!debugInfo) return 'medium';

  const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);

  // 고성능 GPU
  if (/NVIDIA|AMD|Radeon|GeForce|RTX|GTX/i.test(renderer)) {
    return 'high';
  }

  // 저성능 GPU
  if (/Intel|Mali|Adreno [0-4]/i.test(renderer)) {
    return 'low';
  }

  return 'medium';
}

const gpuTier = detectGPUTier();

if (gpuTier === 'low') {
  // 후처리 효과 비활성화
  composer.passes = [renderPass];  // Bloom, SSAO 제거

  // 그림자 비활성화
  renderer.shadowMap.enabled = false;

  // 안티앨리어싱 비활성화
  renderer.setPixelRatio(1);
}

네 번째 사건: 프라이버시 브라우저 #

DuckDuckGo 브라우저와 Brave의 Shields 기능이 켜진 상태에서 일부 리소스가 로드되지 않았습니다.

원인 파악 #

추적 방지 기능이 CDN 요청을 차단하고 있었습니다.

// 이 요청이 차단됨
loader.load('https://cdn.example.com/model.glb', ...);

해결 #

중요한 에셋은 같은 도메인에서 서빙하도록 변경했습니다.

// 같은 도메인 사용
loader.load('/assets/model.glb', ...);

다섯 번째 사건: 기업 방화벽 #

특정 기업 네트워크에서 WebSocket 연결이 차단되어 일부 기능이 작동하지 않았습니다. 이건 제 문제가 아니었지만, 사용자에게는 "안 돼요"였습니다.

해결 #

네트워크 의존성을 최소화하고, 오프라인 폴백을 준비했습니다.

// 연결 실패 시 정적 데이터 사용
fetch('/api/data')
  .then(res => res.json())
  .catch(() => {
    console.warn('API 연결 실패, 로컬 데이터 사용');
    return import('./fallback-data.json');
  });

방어적 초기화 패턴 #

이 경험들을 바탕으로 방어적 초기화 패턴을 만들었습니다.

class SafeRenderer {
  constructor(container) {
    this.container = container;
    this.initialized = false;
    this.fallbackMode = false;
  }

  async init() {
    // 1. WebGL 지원 확인
    if (!this.checkWebGLSupport()) {
      this.showFallback('WebGL을 지원하지 않는 브라우저입니다.');
      return;
    }

    // 2. 컨텍스트 생성 시도
    try {
      this.renderer = new THREE.WebGLRenderer({
        canvas: this.createCanvas(),
        antialias: this.shouldEnableAntialias(),
        powerPreference: this.getPowerPreference()
      });
    } catch (e) {
      this.showFallback('3D 렌더러 초기화에 실패했습니다.');
      return;
    }

    // 3. 에셋 로딩
    try {
      await this.loadAssets();
    } catch (e) {
      this.showFallback('리소스를 불러오는 데 실패했습니다.');
      return;
    }

    // 4. 성능 테스트
    const fps = await this.measurePerformance();
    if (fps < 20) {
      this.enableLowQualityMode();
    }

    this.initialized = true;
  }

  checkWebGLSupport() {
    try {
      const canvas = document.createElement('canvas');
      return !!(
        window.WebGLRenderingContext &&
        (canvas.getContext('webgl') ||
         canvas.getContext('experimental-webgl'))
      );
    } catch (e) {
      return false;
    }
  }

  showFallback(message) {
    this.fallbackMode = true;
    this.container.innerHTML = `
      <div class="fallback">
        <img src="/fallback-image.jpg" alt="3D 미리보기">
        <p>${message}</p>
      </div>
    `;
  }

  async measurePerformance() {
    return new Promise(resolve => {
      let frames = 0;
      const start = performance.now();

      const measure = () => {
        frames++;
        if (performance.now() - start < 1000) {
          requestAnimationFrame(measure);
        } else {
          resolve(frames);
        }
      };

      requestAnimationFrame(measure);
    });
  }
}

테스트 체크리스트 #

프로젝트마다 확인하는 체크리스트입니다.

브라우저:
☐ Chrome (Windows, Mac, Android)
☐ Safari (Mac, iOS)
☐ Firefox (Windows, Mac)
☐ Edge (Windows)
☐ Samsung Internet (Android)

하드웨어:
☐ 외장 GPU (NVIDIA, AMD)
☐ 내장 GPU (Intel, Apple Silicon)
☐ 모바일 (고사양, 저사양)

네트워크:
☐ 고속 (유선)
☐ 저속 (3G 시뮬레이션)
☐ 기업 네트워크 (프록시)

설정:
☐ 절전 모드
☐ 프라이버시 모드
☐ 광고 차단기

배운 점 #

1. "내 컴퓨터에서는 됩니다"는 변명

다양한 환경에서 테스트하세요. BrowserStack 같은 서비스가 있습니다.

2. 우아한 실패

3D가 안 되면 이미지라도 보여주세요. 하얀 화면보다 낫습니다.

3. 점진적 향상

저사양을 기본으로 만들고, 고사양에서 기능을 추가하세요.

4. 에러 추적

Sentry 같은 도구로 실제 사용자 에러를 수집하세요. 생각보다 다양한 환경이 있습니다.

웹의 장점은 어디서든 접근 가능하다는 것입니다. 3D 웹도 마찬가지여야 합니다.