useMediaQuery

useMediaQuery (미디어 쿼리 훅)

CSS 미디어 쿼리가 매칭되는지 감지해서 반응형 디자인을 구현할 수 있게 해주는 React 훅이에요. 화면 크기나 사용자 환경에 따라 컴포넌트를 조건부로 렌더링하고 싶다면 이 훅이 정답이에요.

출처: 문서

본문

이 React 훅은 CSS 미디어 쿼리의 매칭 여부를 듣고, 쿼리가 매칭되는지 여부에 따라 컴포넌트를 렌더링할 수 있게 해줘요.

주요 기능은 다음과 같아요:

  • ⚛️ 관용적인 React API를 제공해요.
  • 🚀 성능이 좋아요 — 값을 주기적으로 폴링하는 대신, 문서를 관찰해서 미디어 쿼리가 언제 변경되는지 감지해요.
  • 📦 1.1 kB gzipped.
  • 🤖 서버 사이드 렌더링을 지원해요.

기본 미디어 쿼리 (Basic media query)

훅의 첫 번째 인자에 미디어 쿼리를 제공해야 해요. 미디어 쿼리 문자열은 예를 들어 '(prefers-color-scheme: dark)' 같은 유효한 CSS 미디어 쿼리면 뭐든 될 수 있어요.

import useMediaQuery from '@mui/material/useMediaQuery';

export default function SimpleMediaQuery() {
  const matches = useMediaQuery('(min-width:600px)');

  return <span>{`(min-width:600px) matches: ${matches}`}</span>;
}

:::warning 인쇄를 위해 문서를 수정하는 'print' 쿼리는 지원되지 않아요, 재렌더링에서 변경 사항이 정확하게 반영되지 않을 수 있기 때문이에요. 이 목적에는 sx prop의 displayPrint 필드를 대신 사용할 수 있어요. 자세한 내용은 MUI System—Display in print를 참고하세요. :::

브레이크포인트 헬퍼 사용하기 (Using breakpoint helpers)

Material UI의 브레이크포인트 헬퍼를 다음과 같이 사용할 수 있어요:

import { useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';

function MyComponent() {
  const theme = useTheme();
  const matches = useMediaQuery(theme.breakpoints.up('sm'));

  return <span>{`theme.breakpoints.up('sm') matches: ${matches}`}</span>;
}
import { createTheme, ThemeProvider, useTheme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';

function MyComponent() {
  const theme = useTheme();
  const matches = useMediaQuery(theme.breakpoints.up('sm'));

  return <span>{`theme.breakpoints.up('sm') matches: ${matches}`}</span>;
}

const theme = createTheme();

export default function ThemeHelper() {
  return (
    <ThemeProvider theme={theme}>
      <MyComponent />
    </ThemeProvider>
  );
}

다른 방법으로, 테마를 첫 번째 인자로 받는 콜백 함수를 사용할 수도 있어요:

import useMediaQuery from '@mui/material/useMediaQuery';

function MyComponent() {
  const matches = useMediaQuery((theme) => theme.breakpoints.up('sm'));

  return <span>{`theme.breakpoints.up('sm') matches: ${matches}`}</span>;
}

⚠️ 기본 테마 지원은 없어요, 부모 테마 프로바이더에서 주입해야 해요.

JavaScript 문법 사용하기 (Using JavaScript syntax)

json2mq를 사용해서 JavaScript 객체에서 미디어 쿼리 문자열을 생성할 수 있어요.

import json2mq from 'json2mq';
import useMediaQuery from '@mui/material/useMediaQuery';

export default function JavaScriptMedia() {
  const matches = useMediaQuery(
    json2mq({
      minWidth: 600,
    }),
  );

  return <span>{`{ minWidth: 600 } matches: ${matches}`}</span>;
}

테스팅 (Testing)

테스트 환경에 matchMedia 구현이 필요해요.

예를 들어, jsdom은 아직 지원하지 않아요. polyfill을 해야 해요. css-mediaquery로 이를 에뮬레이트하는 것을 권장해요.

import mediaQuery from 'css-mediaquery';

function createMatchMedia(width) {
  return (query) => ({
    matches: mediaQuery.match(query, {
      width,
    }),
    addEventListener: () => {},
    removeEventListener: () => {},
  });
}

describe('MyTests', () => {
  beforeAll(() => {
    window.matchMedia = createMatchMedia(window.innerWidth);
  });
});

클라이언트 사이드 전용 렌더링 (Client-side only rendering)

서버 사이드 하이드레이션을 수행하려면 훅이 두 번 렌더링되어야 해요. 첫 번째는 서버의 값인 defaultMatches로, 두 번째는 해석된 값으로 렌더링해요. 이 이중 렌더링 사이클에는 느리다는 단점이 있어요. 반환된 값을 클라이언트 사이드에서만 사용한다면 noSsr 옵션을 true로 설정할 수 있어요.

const matches = useMediaQuery('(min-width:600px)', { noSsr: true });

또는 테마로 전역적으로 켤 수 있어요:

const theme = createTheme({
  components: {
    MuiUseMediaQuery: {
      defaultProps: {
        noSsr: true,
      },
    },
  },
});

:::info noSsr는 createRoot() API(React 18에서 도입된 클라이언트 사이드 전용 API)를 사용할 때는 효과가 없다는 점을 참고하세요. :::

서버 사이드 렌더링 (Server-side rendering)

:::warning 서버 사이드 렌더링과 클라이언트 사이드 미디어 쿼리는 근본적으로 상충돼요. 트레이드오프를 인지하세요. 지원은 부분적일 수밖에 없어요. :::

먼저 클라이언트 사이드 CSS 미디어 쿼리에 의존해 보세요. 예를 들어 다음을 사용할 수 있어요:

위 대안 중 어느 것도 선택할 수 없다면, 이 문서 섹션을 계속 읽어볼 수 있어요.

먼저, 서버에서 클라이언트 요청의 특징을 추측해야 해요. 다음 중에서 선택할 수 있어요:

  • 사용자 에이전트(User agent). 클라이언트의 사용자 에이전트 문자열을 파싱해서 정보를 추출해요. 사용자 에이전트 파싱에는 ua-parser-js를 사용하는 것이 권장돼요.
  • 클라이언트 힌트(Client hints). 클라이언트가 서버로 보내는 힌트를 읽어요. 이 기능은 모든 곳에서 지원되지는 않는다는 점을 인지하세요.

마지막으로, 앞서 추측한 특징과 함께 matchMedia 구현을 useMediaQuery에 제공해야 해요. matchMedia를 에뮬레이트하려면 css-mediaquery를 사용하는 것이 권장돼요.

예를 들어 서버 사이드에서는:

import * as ReactDOMServer from 'react-dom/server';
import parser from 'ua-parser-js';
import mediaQuery from 'css-mediaquery';
import { createTheme, ThemeProvider } from '@mui/material/styles';

function handleRender(req, res) {
  const deviceType = parser(req.headers['user-agent']).device.type || 'desktop';
  const ssrMatchMedia = (query) => ({
    matches: mediaQuery.match(query, {
      // The estimated CSS width of the browser.
      width: deviceType === 'mobile' ? '0px' : '1024px',
    }),
  });

  const theme = createTheme({
    components: {
      // Change the default options of useMediaQuery
      MuiUseMediaQuery: {
        defaultProps: {
          ssrMatchMedia,
        },
      },
    },
  });

  const html = ReactDOMServer.renderToString(
    <ThemeProvider theme={theme}>
      <App />
    </ThemeProvider>,
  );

  // …
}
import mediaQuery from 'css-mediaquery';
import { ThemeProvider, Theme } from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';

function MyComponent() {
  const matches = useMediaQuery('(min-width:600px)');

  return <span>{`(min-width:600px) matches: ${matches}`}</span>;
}

export default function ServerSide() {
  const ssrMatchMedia = (query: string) => ({
    matches: mediaQuery.match(query, {
      // The estimated CSS width of the browser.
      width: 800,
    }),
  });

  return (
    <ThemeProvider<Theme>
      theme={{
        components: {
          MuiUseMediaQuery: {
            // Change the default options of useMediaQuery
            defaultProps: { ssrMatchMedia },
          },
        },
      }}
    >
      <MyComponent />
    </ThemeProvider>
  );
}

하이드레이션 일치를 보장하려면 클라이언트 사이드에도 동일한 커스텀 match media 구현을 제공해야 해요.

withWidth()에서 마이그레이션하기 (Migrating from withWidth())

withWidth() 고차 컴포넌트는 페이지의 화면 너비를 주입해요. useWidth 훅으로 동일한 동작을 재현할 수 있어요:

import {
  Breakpoint,
  Theme,
  ThemeProvider,
  useTheme,
  createTheme,
} from '@mui/material/styles';
import useMediaQuery from '@mui/material/useMediaQuery';

type BreakpointOrNull = Breakpoint | null;

/**
 * Be careful using this hook. It only works because the number of
 * breakpoints in theme is static. It will break once you change the number of
 * breakpoints. See https://legacy.reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level
 */
function useWidth() {
  const theme: Theme = useTheme();
  const keys: readonly Breakpoint[] = [...theme.breakpoints.keys].reverse();
  return (
    keys.reduce((output: BreakpointOrNull, key: Breakpoint) => {
      // TODO: uncomment once we enable eslint-plugin-react-compiler // eslint-disable-next-line react-compiler/react-compiler -- useMediaQuery is called inside callback
      // eslint-disable-next-line react-hooks/rules-of-hooks
      const matches = useMediaQuery(theme.breakpoints.up(key));
      return !output && matches ? key : output;
    }, null) || 'xs'
  );
}

function MyComponent() {
  const width = useWidth();
  return <span>{`width: ${width}`}</span>;
}

const theme = createTheme();

export default function UseWidth() {
  return (
    <ThemeProvider theme={theme}>
      <MyComponent />
    </ThemeProvider>
  );
}

API

useMediaQuery(query, [options]) => matches

Arguments (인자)

  1. query (string | func): 처리할 미디어 쿼리를 나타내는 문자열 또는 (컨텍스트의) 테마를 받아 문자열을 반환하는 콜백 함수.
  2. options (object [optional]):
  • options.defaultMatches (bool [optional]): 서버에서는 window.matchMedia()를 사용할 수 없기 때문에, 첫 번째 마운트 동안 기본 matches를 반환해요. 기본값은 false예요.
  • options.matchMedia (func [optional]): 자신만의 matchMedia 구현을 제공할 수 있어요. iframe 콘텐츠 창을 처리하는 데 사용할 수 있어요.
  • options.noSsr (bool [optional]): 기본값은 false예요. 서버 사이드 하이드레이션을 수행하려면 훅이 두 번 렌더링되어야 해요. 첫 번째는 defaultMatches, 즉 서버의 값으로, 두 번째는 해석된 값으로 렌더링해요. 이 이중 렌더링 사이클에는 느리다는 단점이 있어요. 반환된 값을 클라이언트 사이드에서만 사용한다면 이 옵션을 true로 설정할 수 있어요.
  • options.ssrMatchMedia (func [optional]): 자신만의 matchMedia 구현을 제공할 수 있어요, 서버 사이드 렌더링에 사용돼요.

참고: MuiUseMediaQuery 키로 테마의 default props 기능을 사용해서 기본 옵션을 변경할 수 있어요.

Returns (반환값)

matches: 문서가 현재 미디어 쿼리와 일치하면 true, 그렇지 않으면 false예요.

Examples (예제)

import * as React from 'react';
import useMediaQuery from '@mui/material/useMediaQuery';

export default function SimpleMediaQuery() {
  const matches = useMediaQuery('(min-width:600px)');

  return <span>{`(min-width:600px) matches: ${matches}`}</span>;
}

더 알아보기 (Learn more)