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 (인자)
query(string | func): 처리할 미디어 쿼리를 나타내는 문자열 또는 (컨텍스트의) 테마를 받아 문자열을 반환하는 콜백 함수.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)
- Breakpoints — Material UI 브레이크포인트
- sx prop — 반응형 스타일링
- css-mediaquery — matchMedia 에뮬레이션