~1.5 kB min+brotli의존성 0개React 17–19MIT

CSR에서도, SSR에서도 정확한 기기 판별

React CSR 앱에서도 Next.js SSR에서도, 사용자가 폰·태블릿·데스크톱 중 무엇으로, 어떤 OS에서 접근했는지 정확하게 알 수 있습니다. 의존성 0개에 iPad 위장 해제와 동결된 User-Agent까지 처리하며, hydration 에러는 단 한 건도 발생하지 않습니다.

At a glance

기기는 속여도, 답은 정직합니다

각 화면은 그 기기에서 useDevice()가 반환하는 값입니다 — 자신을 속이는 기기까지 포함해서요.

type: 'mobile'
os: 'ios'
isTouchPrimary: true
orientation: 'portrait'

iPhone 15

기기의 주장Macintosh; Intel Mac OS X · screen: 393px
훅의 판별'mobile''ios'

Safari의 “데스크톱 웹사이트 요청”을 켜면 iPhone조차 자신을 Mac이라고 주장합니다. 멀티터치와 화면 크기 교차검증이 그래도 mobile임을 밝혀냅니다.

type: 'mobile'
os: 'android'
isTouchPrimary: true
orientation: 'portrait'

Galaxy S24

기기의 주장Linux; Android 14; K
훅의 판별'mobile''android'

Chrome은 UA를 동결해 모든 안드로이드가 모델명 “K”로 보고됩니다. Client Hints가 정확히 판별합니다.

type: 'tablet'
os: 'ios'
isTouchPrimary: true
orientation: 'landscape'

iPad Pro

기기의 주장Macintosh; Intel Mac OS X 10_15_7
훅의 판별'tablet''ios'

iPadOS 13부터 Mac으로 위장하지만, 멀티터치 언마스킹이 태블릿임을 밝혀냅니다.

type: 'desktop'
os: 'macos'
isTouchPrimary: false
orientation: 'landscape'

iMac

기기의 주장Macintosh; Intel Mac OS X 10_15_7 · maxTouchPoints: 0
훅의 판별'desktop''macos'

위의 iPad와 완전히 같은 UA를 보내는 진짜 Mac입니다. 멀티터치 교차검증(maxTouchPoints: 0)이 둘을 구분합니다.

type: 'desktop'
os: 'windows'
isTouchPrimary: false
orientation: 'landscape'

Windows 터치 노트북

기기의 주장Windows NT 10.0 · maxTouchPoints: 10
훅의 판별'desktop''windows'

터치스크린에 속지 않습니다 — 터치 신호는 Apple 위장 분기에서만 참조됩니다. desktop을 유지합니다.

type: 'desktop'
os: 'android'
isTouchPrimary: false
orientation: 'landscape'

Android TV

기기의 주장Linux; Android 11; SHIELD Android TV
훅의 판별'desktop''android'

Mobile 토큰이 없는 Android UA라 원래는 태블릿으로 분류될 신호지만, TV 마커를 먼저 확인합니다. 10-foot UI에는 desktop이 가장 맞는 답입니다.

Live demo

SSR 계약이 동작하는 모습

이 페이지는 Next.js로 서버 렌더됩니다. 왼쪽 패널은 hydration 첫 페인트 시점에 동결된 값 — 서버가 보낸 그대로입니다. 오른쪽 패널은 훅이 지금 알고 있는 값입니다.

첫 페인트 (서버가 렌더한 값)

server default
type
desktop
os
unknown
isTouchPrimary
false
orientation
landscape
isHydrated
false

어떤 기기에서든 항상 desktop / unknown — 그래서 서버 HTML과 클라이언트 HTML이 어긋날 수 없습니다.

라이브 값

server default
type
desktop
os
unknown
isTouchPrimary
false
orientation
landscape
isHydrated
false

hydration 직후 단 한 번의 렌더로 교정됩니다. isTouchPrimary와 orientation은 계속 실시간 갱신됩니다.

이 페이지를 폰에서 열거나 DevTools 기기 에뮬레이션으로 새로고침해 보세요. 왼쪽 패널은 desktop에 머물고 오른쪽이 진실을 말합니다 — 콘솔의 hydration 에러는 0건입니다.

Usage

세 가지 사용 방법

불리언 한 줄부터 프레임워크 없는 순수 엔진까지 — 각 import는 실제로 필요한 만큼만 번들에 담습니다.

01

전체 스냅샷 읽기

useDevice()는 type, os, 불리언 슈가와 실시간 필드를 반환합니다. 서버 렌더와 hydration 첫 페인트는 구조적으로 항상 일치하므로 typeof window 가드를 쓸 일이 없고, 교정 렌더 한 번을 감추고 싶을 때만 isHydrated로 분기하면 됩니다.

app/page.tsx
import { useDevice } from 'react-device-check';

export default function Page() {
  const { type, os, isMobile, isHydrated } = useDevice();

  // Server render & hydration first paint: type = 'desktop', isHydrated = false.
  // One render later the real device shows up — no hydration mismatch, ever.
  if (!isHydrated) return <Skeleton />;

  if (isMobile && os === 'ios') return <AppStoreBanner />;
  return <p>You are on a {type} running {os}.</p>;
}
02

쓰는 것만 import

정적 훅은 리스너가 없고 최대한으로 tree-shaking됩니다. useIsMobile과 useOS만 import하면 반응형 스토어가 통째로 빠져 ~1.1 kB만 배송됩니다. OS별 앱스토어 버튼 같은 곳에 딱 맞습니다.

components/DownloadButton.tsx
import { useIsMobile, useOS } from 'react-device-check';

export default function DownloadButton() {
  const isMobile = useIsMobile(); // boolean only — ~1.1 kB total
  const os = useOS(); // 'ios' | 'android' | ...

  if (isMobile && os === 'ios') return <AppStoreButton />;
  if (isMobile && os === 'android') return <PlayStoreButton />;
  return <DesktopDownloadButton />;
}
03

React 밖에서도 사용

detectDevice()는 훅 뒤에서 동작하는 순수 판별 트리입니다 — React도 전역 객체도 필요 없습니다. UA 문자열(또는 Client Hints)을 주입하면 미들웨어, 서버, 테스트 어디서든 같은 결정론적 결과를 얻습니다.

middleware.ts
import { detectDevice } from 'react-device-check';

export function middleware(request: Request) {
  // No React, no globals — inject any signals you have
  const { type } = detectDevice({
    ua: request.headers.get('user-agent') ?? '',
  });

  if (type === 'mobile') {
    // e.g. rewrite to the lightweight variant
  }
}

Why

2026년, 기기가 거짓말하는 방식에 맞춘 설계

다른 라이브러리가 틀리는 곳에서 정확

iPadOS 13+가 macOS 데스크톱 UA를 보내도 iPad를 태블릿으로 판별합니다(MacIntel + 멀티터치 언마스킹). 안드로이드 태블릿은 공식 Mobile 토큰 규칙으로 구분하고, Samsung DeX와 인앱 웹뷰도 처리합니다.

Client Hints 우선

Chromium에서는 navigator.userAgentData를 신뢰해 UA 동결에 면역이고, 그 외 환경에서는 UA 파싱으로 폴백합니다.

구조적으로 SSR-safe

서버 렌더와 hydration 첫 페인트가 항상 일치해 React 18/19에서 hydration mismatch가 기록되지 않습니다. hydration 직후 렌더 한 번으로 실제 값으로 교정됩니다.

작고 tree-shakeable

런타임 의존성 0개, ESM/CJS 듀얼. useIsMobile만 import하면 ~1.1 kB이고 반응형 스토어 전체가 번들에서 제거됩니다 — 예산은 CI에서 강제됩니다.

하이브리드 반응성

type과 os는 세션 동안 고정되고, isTouchPrimary와 orientation은 matchMedia로 실시간 갱신됩니다 — 폴더블, DeX 도킹, iPad Stage Manager까지 커버합니다.

실브라우저 검증

76개 단위 테스트에 더해 Playwright 매트릭스 — iPhone 15, iPad Pro, Galaxy S24, Galaxy Tab S9, 데스크톱 Chrome/Safari — 가 판별 결과와 hydration 에러 0건을 검증합니다.

Comparison

react-device-detect는요?

react-device-detect는 import 시점에 UA로 상수를 계산해 SSR에서 크래시하거나 mismatch가 나고, iPad를 데스크톱으로 오판하며, 값이 갱신되지 않고, tree-shaking이 불가능한 ~13 kB gzip을 항상 배송합니다. 2023년 이후 유지보수가 중단됐고 파서 의존성은 AGPL로 전환됐습니다. react-device-check는 오늘의 플랫폼 현실에 맞춰 설계된, 유지보수되는 MIT 대안입니다.

API

작은 표면적, 빠짐없는 커버리지

APIDescription
useDevice()전체 스냅샷: type, os, 불리언 슈가, 실시간 isTouchPrimary / orientation, isHydrated.
useDeviceType()'mobile' | 'tablet' | 'desktop' — 세션 동안 정적, 리스너 없음.
useIsMobile() · useIsTablet() · useIsDesktop()불리언 슈가 — 최대 tree-shaking. 이것만 import하면 반응형 스토어가 번들에서 빠집니다.
useOS()'ios' | 'android' | 'windows' | 'macos' | 'linux' | 'unknown'.
detectDevice(input?, options?)React 없이 쓰는 순수 엔진 — 모든 신호를 주입할 수 있어 서버와 테스트에 적합합니다.