본문 바로가기

React

React 앱에서 브라우저 Battery Status API 활용하기

Battery Status API는 디바이스 배터리 잔량과 충전 여부를 제공하는 브라우저 API입니다. 다만 보안/프라이버시 이슈로 표준에서 사실상 폐기(Obsolete)되었고, 최신 브라우저 대부분에서 비활성화/제거되었습니다. 따라서 핵심 기능이 아닌 보조 UX에만 프로그레시브 인핸스먼트 관점으로 사용해야 합니다. 본 글은 지원되는 환경에서만 가볍게 활용하는 React 실전 패턴을 설명합니다.

1. 지원 현황과 전략

- 현재 주요 데스크톱/모바일 브라우저에서 동작하지 않거나 제한적입니다. 구형/특정 브라우저에서만 작동할 수 있습니다.
- 반드시 기능 감지를 하고, 실패 시 완전한 폴백 UX를 제공합니다.
- SEO/AEO 관점에서도 핵심 콘텐츠/기능은 배터리 정보에 의존하지 않도록 설계합니다.
- 참고: MDN 가이드(영문) https://developer.mozilla.org/docs/Web/API/Battery_Status_API

2. React 훅으로 감싸기: useBatteryStatus

프로그레시브 인핸스먼트를 위해 훅으로 캡슐화합니다. SSR 안전성, 이벤트 구독/해제, 에러 처리까지 포함합니다.

import { useEffect, useState } from "react";

// 타입 힌트를 원하면 JSDoc을 활용하세요.
// /** @typedef {{ supported: boolean, loading: boolean, level: number, charging: boolean, chargingTime: number, dischargingTime: number, error: any, lowPowerMode: boolean, levelPercent: number }} BatteryState */

export function useBatteryStatus() {
  const [state, setState] = useState({
    supported: false,
    loading: true,
    level: 1,
    charging: true,
    chargingTime: Infinity,
    dischargingTime: Infinity,
    error: null,
    lowPowerMode: false,
    levelPercent: 100,
  });

  useEffect(() => {
    let isMounted = true;
    let batteryRef = null;
    let update = null;

    const setup = async () => {
      // SSR/기능 감지
      if (
        typeof navigator === "undefined" ||
        typeof navigator.getBattery !== "function"
      ) {
        setState((s) => ({ ...s, supported: false, loading: false }));
        return;
      }

      try {
        const b = await navigator.getBattery();
        batteryRef = b;

        update = () => {
          if (!isMounted) return;
          const level = typeof b.level === "number" ? b.level : 1;
          const charging = Boolean(b.charging);
          const levelPercent = Math.round(level * 100);
          const lowPowerMode = !charging && level <= 0.2; // 필요에 따라 임계값 조정

          setState({
            supported: true,
            loading: false,
            level,
            charging,
            chargingTime: b.chargingTime,
            dischargingTime: b.dischargingTime,
            error: null,
            lowPowerMode,
            levelPercent,
          });
        };

        update();
        [
          "levelchange",
          "chargingchange",
          "chargingtimechange",
          "dischargingtimechange",
        ].forEach((evt) => b.addEventListener(evt, update));
      } catch (err) {
        if (isMounted) {
          setState((s) => ({ ...s, supported: false, loading: false, error: err }));
        }
      }
    };

    setup();

    return () => {
      isMounted = false;
      if (batteryRef && update) {
        [
          "levelchange",
          "chargingchange",
          "chargingtimechange",
          "dischargingtimechange",
        ].forEach((evt) => batteryRef.removeEventListener(evt, update));
      }
    };
  }, []);

  return state;
}

3. 사용 예시: 저전력 모드에서 UI/작업 줄이기

배터리 지원이 안 되면 기본 UX로 동작하고, 지원되면 저전력일 때 애니메이션을 줄이고 폴링 간격을 늘리는 식으로 처리합니다.

import React, { useEffect, useRef } from "react";
import { useBatteryStatus } from "./useBatteryStatus";

export default function BatteryAwareWidget() {
  const {
    supported,
    loading,
    levelPercent,
    charging,
    lowPowerMode,
  } = useBatteryStatus();

  const intervalRef = useRef(null);

  // 폴링(또는 무거운 작업) 빈도를 배터리 상태에 따라 조절
  useEffect(() => {
    const baseMs = 5000;
    const slowMs = 20000; // 저전력 시 느리게
    const delay = lowPowerMode ? slowMs : baseMs;

    function tick() {
      // TODO: 가벼운 데이터 동기화 등
      // console.log("tick");
    }

    tick();
    intervalRef.current = setInterval(tick, delay);

    return () => clearInterval(intervalRef.current);
  }, [lowPowerMode]);

  return (
    <div>
      <h4>상태 요약</h4>
      {loading ? (
        <p>배터리 정보를 확인하는 중입니다...</p>
      ) : supported ? (
        <p>
          배터리: {levelPercent}% / {charging ? "충전 중" : "배터리 사용 중"}
          {lowPowerMode ? " (저전력 최적화 적용)" : ""}
        </p>
      ) : (
        <p>배터리 API 미지원 환경입니다. 기본 성능 프로파일로 동작합니다.</p>
      )}

      <div style={{ animation: lowPowerMode ? "none" : "pulse 1.5s infinite" }}>
        애니메이션(저전력 시 비활성화)
      </div>
    </div>
  );
}

4. SSR/Next.js 안전 처리

API는 클라이언트 전용입니다. 훅 내부에서 typeof navigator 가드를 했기 때문에 일반 CSR에서는 충분합니다. Next.js에서 훅을 사용하는 컴포넌트는 "use client" 파일에서만 사용하세요. 또한 서버 렌더 단계에서는 절대 Battery API 접근을 시도하지 않도록 주의합니다.

5. 폴백과 대안 신호

- 폴백: API가 없을 때는 보수적 기본값(애니메이션 on, 폴링 보통)으로 동작합니다.
- 대안 신호: 네트워크 상태 API(navigator.connection.effectiveType)로 대역폭이 낮은 환경에서 작업을 줄이는 전략을 병행할 수 있습니다. 단, 이 API도 브라우저 지원이 제한적이므로 동일하게 기능 감지를 적용합니다.
- UX: 배터리/네트워크 같은 신호는 어디까지나 힌트로만 사용하고, 사용자 설정(예: "절전 모드" 스위치)을 제공해 수동 제어를 허용합니다.

6. 테스트 팁

- DevTools에서 배터리 에뮬레이션이 제공되지 않는 경우가 많습니다. 실제 지원 기기에서 테스트하세요.
- 실서비스에선 이 신호에 의존하는 로직을 반드시 옵셔널 처리하고, 관측 로그는 개인정보/식별 가능 정보 없이 집계 수준으로만 수집합니다.

7. 보안/프라이버시

Battery Status API는 과거 핑거프린팅 우려가 있었습니다. 사용 시에는 꼭 비필수 기능에만 적용하고, 상태 값을 서버로 전송하지 않는 것을 기본값으로 권장합니다.

8. FAQ

- 왜 작동하지 않나요? 대부분의 최신 브라우저에서 API가 제거/비활성화되었기 때문입니다. 기능 감지 결과 supported=false가 정상입니다.
- 대안은 무엇인가요? 명시적 사용자 설정(절전 토글), 네트워크 상태 힌트, 작업 스케줄링(백오프/디바운스), 애니메이션 감소(prefers-reduced-motion) 등으로 간접 최적화를 적용하세요.
- PWA에서 유용한가요? 제한적입니다. 오프라인 동기화/백그라운드 작업의 주기를 배터리 힌트로 완화하는 정도로만 고려하세요.

결론: Battery Status API는 오늘날 신뢰 가능한 기반 기술이 아닙니다. 다만 기능 감지를 전제로, 지원되는 일부 환경에서 UX/성능을 미세 조정하는 가벼운 최적화 레벨로는 의미가 있습니다. 실제 제품에서는 폴백을 우선 설계하고, 사용자 제어권을 제공합니다.