본문 바로가기

React

React에서 브라우저 풀스크린 API 제어하기

풀스크린(전체화면)은 미디어 감상, 프레젠테이션, 데이터 대시보드 등 몰입형 UX에 필수입니다. React에서 브라우저 Fullscreen API를 안전하게 제어하는 방법과 실무 팁을 정리합니다.

1. Fullscreen API 개요

핵심 메서드와 속성입니다.

- element.requestFullscreen(options?)
- document.exitFullscreen()
- document.fullscreenElement
- fullscreenchange, fullscreenerror 이벤트

주의: 사용자 제스처(클릭 등) 안에서만 진입이 가능합니다. iOS Safari는 일반 요소에 대해 Fullscreen API를 지원하지 않고, video 요소에 제한적으로 동작합니다.

2. 가장 빠른 토글 예제

특정 컨테이너를 전체화면으로 전환/해제하는 기본 React 컴포넌트입니다.

import React, { useRef, useState } from "react";

export default function FullscreenBox() {
  const boxRef = useRef(null);
  const [isFullscreen, setIsFullscreen] = useState(false);

  const enter = async () => {
    const el = boxRef.current;
    if (!el) return;
    // 벤더 프리픽스 대응
    const req = el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen;
    if (!req) {
      alert("이 브라우저는 Fullscreen API를 지원하지 않습니다.");
      return;
    }
    try {
      await req.call(el, { navigationUI: "hide" }); // 지원 브라우저에서 상단 UI 숨김
    } catch (e) {
      console.error("fullscreen enter error", e);
    }
  };

  const exit = async () => {
    const doc = document;
    const ex = doc.exitFullscreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
    if (!ex) return;
    try {
      await ex.call(doc);
    } catch (e) {
      console.error("fullscreen exit error", e);
    }
  };

  const toggle = () => {
    const doc = document;
    const cur = doc.fullscreenElement || doc.webkitFullscreenElement || doc.msFullscreenElement;
    if (cur) exit();
    else enter();
  };

  React.useEffect(() => {
    const onChange = () => {
      const cur = document.fullscreenElement || document.webkitFullscreenElement || document.msFullscreenElement;
      setIsFullscreen(Boolean(cur));
    };
    document.addEventListener("fullscreenchange", onChange);
    document.addEventListener("webkitfullscreenchange", onChange);
    document.addEventListener("MSFullscreenChange", onChange);
    return () => {
      document.removeEventListener("fullscreenchange", onChange);
      document.removeEventListener("webkitfullscreenchange", onChange);
      document.removeEventListener("MSFullscreenChange", onChange);
    };
  }, []);

  return (
    <div>
      <div
        ref={boxRef}
        role="region"
        aria-label="전체화면 영역"
        style={{
          width: 320,
          height: 180,
          background: "#111",
          color: "#fff",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          borderRadius: 8,
        }}
      >
        {isFullscreen ? "전체화면 모드" : "일반 모드"}
      </div>
      <button onClick={toggle} aria-pressed={isFullscreen} style={{ marginTop: 12 }}>
        {isFullscreen ? "전체화면 종료" : "전체화면 진입"}
      </button>
    </div>
  );
}

3. 재사용 가능한 커스텀 훅: useFullscreen

컴포넌트 복잡도를 줄이기 위해 훅으로 캡슐화합니다. onChange 콜백으로 상태 변화를 감지할 수 있습니다.

import { useCallback, useEffect, useState } from "react";

function getFullscreenElement(doc) {
  return doc.fullscreenElement || doc.webkitFullscreenElement || doc.msFullscreenElement || null;
}

function getRequest(el) {
  return el.requestFullscreen || el.webkitRequestFullscreen || el.msRequestFullscreen;
}

function getExit(doc) {
  return doc.exitFullscreen || doc.webkitExitFullscreen || doc.msExitFullscreen;
}

export function useFullscreen(targetRef, options = {}) {
  const { onChange } = options;
  const [isFullscreen, setIsFullscreen] = useState(false);

  const enter = useCallback(async (opts) => {
    if (typeof document === "undefined") return; // SSR 안전장치
    const el = (targetRef && targetRef.current) ? targetRef.current : document.documentElement;
    const req = el ? getRequest(el) : null;
    if (!req) {
      console.warn("Fullscreen API not supported");
      return;
    }
    try {
      await req.call(el, opts);
    } catch (e) {
      console.error("enter fullscreen failed", e);
    }
  }, [targetRef]);

  const exit = useCallback(async () => {
    if (typeof document === "undefined") return;
    const ex = getExit(document);
    if (!ex) return;
    try {
      await ex.call(document);
    } catch (e) {
      console.error("exit fullscreen failed", e);
    }
  }, []);

  const toggle = useCallback(() => {
    const cur = typeof document !== "undefined" ? getFullscreenElement(document) : null;
    if (cur) exit(); else enter({ navigationUI: "hide" });
  }, [enter, exit]);

  useEffect(() => {
    if (typeof document === "undefined") return;
    const handler = () => {
      const active = Boolean(getFullscreenElement(document));
      setIsFullscreen(active);
      if (typeof onChange === "function") onChange(active);
    };
    const errHandler = (e) => console.error("fullscreen error", e);

    document.addEventListener("fullscreenchange", handler);
    document.addEventListener("webkitfullscreenchange", handler);
    document.addEventListener("MSFullscreenChange", handler);
    document.addEventListener("fullscreenerror", errHandler);

    return () => {
      document.removeEventListener("fullscreenchange", handler);
      document.removeEventListener("webkitfullscreenchange", handler);
      document.removeEventListener("MSFullscreenChange", handler);
      document.removeEventListener("fullscreenerror", errHandler);
    };
  }, [onChange]);

  return { isFullscreen, enter, exit, toggle };
}

// 사용 예시
import React, { useRef } from "react";

export function GalleryFullscreen() {
  const ref = useRef(null);
  const { isFullscreen, toggle, exit } = useFullscreen(ref, {
    onChange: (active) => console.log("fullscreen:", active),
  });

  return (
    <section>
      <div ref={ref} style={{ width: 640, height: 360, background: "#222", color: "#fff" }}>
        <p style={{ textAlign: "center", paddingTop: 140 }}>이미지/그래프/지도 영역</p>
      </div>
      <div style={{ marginTop: 12 }}>
        <button onClick={toggle}>{isFullscreen ? "종료" : "전체화면"}</button>
        {isFullscreen && <button onClick={exit} style={{ marginLeft: 8 }}>나가기</button>}
      </div>
    </section>
  );
}

4. 오류 처리와 브라우저 제약

- 사용자 제스처 의존: 클릭/키보드 이벤트 핸들러 안에서 requestFullscreen을 호출해야 거절되지 않습니다.
- iOS Safari: video 요소 외 일반 요소는 미지원입니다. 필요 시 <video playsInline> 정책, 또는 대체 레이아웃을 제공하세요.
- 보안 컨텍스트: 일부 기능은 HTTPS에서만 동작합니다.
- 프레임 내 전체화면: iframe에 allow="fullscreen" 속성이 필요합니다.

// iframe 예시 (React)
<iframe src="https://example.com" allow="fullscreen" style={{ width: "100%", height: 400 }} />

5. 접근성/UX 가이드

- 명확한 토글 버튼 레이블: 전체화면 진입/종료를 상태 기반으로 바꿉니다.
- 키보드: 스크린리더 사용자도 인지할 수 있도록 aria-pressed, aria-label을 사용합니다.
- 포커스 관리: 전체화면 진입 후 주요 컨트롤에 포커스를 이동하면 접근성이 좋아집니다.
- ESC 안내: 대부분 브라우저는 ESC로 종료됩니다. 툴팁/토스트로 안내하세요.

6. Next.js/SSR 주의

서버 렌더링 환경에서는 document가 없습니다. 훅/핸들러에서 typeof document 체크를 넣어야 합니다. 또한 전체화면 관련 코드는 클라이언트 사이드에서만 실행되도록 보장하세요.

// 안전 가드 예시
if (typeof document === "undefined") {
  // SSR: 아무 것도 하지 않음
}

7. 테스트 체크리스트

- 데스크톱: Chrome, Edge, Firefox, Safari 동작 확인
- 모바일: Android Chrome(OK), iOS Safari(제약) 동작 확인
- ESC/제스처 종료 시 상태 동기화 여부
- iframe 내부/외부 권한 및 이벤트 동작

8. 자주 겪는 문제 해결

- 버튼 눌러도 안 됨: 클릭 핸들러 안에서 호출했는지 확인, 팝업 차단/자동재생 정책 영향 확인
- 상태가 꼬임: fullscreenchange 이벤트 기반으로 상태를 동기화하세요. DOM만 믿지 말고 이벤트를 청취합니다.
- 스타일 깨짐: 전체화면 시 크기 강제. 컨테이너에 width/height 100% 처리 및 스크롤 대응 필요

9. 확장 팁

- 옵션 사용: requestFullscreen({ navigationUI: 'hide' })로 상단 UI 숨김(지원 브라우저 한정).
- 비디오/지도: 미디어 컨트롤 옆에 전체화면 버튼 노출. iOS는 video 전용 전체화면을 고려.
- 가로모드 UX: 전체화면 진입 시 화면 회전 안내(Orientation API는 권한/지원 이슈 존재).

10. 마무리

Fullscreen API는 간단해 보이지만 브라우저/플랫폼 제약이 많습니다. 재사용 가능한 useFullscreen 훅으로 상태와 이벤트를 일관되게 관리하고, iOS 등 예외 케이스를 대비하면 안정적인 UX를 제공할 수 있습니다.