타이포그래피

타이포그래피 (Typography)

테마는 서로 잘 어울리도록, 그리고 레이아웃 그리드와도 잘 맞도록 설계된 일련의 타입 크기 세트를 제공해요. 폰트 패밀리부터 크기, 변형(variant)까지 타이포그래피를 어떻게 커스터마이징하는지 함께 살펴볼게요.

출처: 문서

본문

폰트 패밀리 (Font family)

폰트 패밀리는 theme.typography.fontFamily 속성으로 변경할 수 있어요.

예를 들어, 다음 예시는 기본 Roboto 폰트 대신 시스템 폰트를 사용해요:

const theme = createTheme({
  typography: {
    fontFamily: [
      '-apple-system',
      'BlinkMacSystemFont',
      '"Segoe UI"',
      'Roboto',
      '"Helvetica Neue"',
      'Arial',
      'sans-serif',
      '"Apple Color Emoji"',
      '"Segoe UI Emoji"',
      '"Segoe UI Symbol"',
    ].join(','),
  },
});

자체 호스팅 폰트 (Self-hosted fonts)

폰트를 자체 호스팅하려면 woff2 형식의 폰트 파일을 다운로드해서 코드에 import 하세요.

:::warning 빌드 과정에서 woff2 파일을 처리할 수 있는 플러그인이나 로더가 필요해요. 폰트는 번들 안에 내장되지 않을 것입니다. CDN 대신 여러분의 웹서버에서 로드되게 됩니다. :::

import RalewayWoff2 from './fonts/Raleway-Regular.woff2';

이제 테마를 변경해서 이 새 폰트를 사용해야 해요. Raleway를 전역적으로 font face로 정의하려면 CssBaseline 컴포넌트를 사용할 수 있습니다 (또는 원하는 다른 CSS 방식을 사용해도 돼요).

import RalewayWoff2 from './fonts/Raleway-Regular.woff2';

const theme = createTheme({
  typography: {
    fontFamily: 'Raleway, Arial',
  },
  components: {
    MuiCssBaseline: {
      styleOverrides: `
        @font-face {
          font-family: 'Raleway';
          font-style: normal;
          font-display: swap;
          font-weight: 400;
          src: local('Raleway'), local('Raleway-Regular'), url(${RalewayWoff2}) format('woff2');
          unicodeRange: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF;
        }
      `,
    },
  },
});

// ...
return (
  <ThemeProvider theme={theme}>
    <CssBaseline />
    <Box sx={{ fontFamily: 'Raleway' }}>Raleway</Box>
  </ThemeProvider>
);

추가 @font-face 선언을 추가하고 싶다면, 스타일 오버라이드 추가 시 문자열 CSS 템플릿 구문을 사용해야 한다는 점에 유의하세요. 그래야 이전에 정의된 @font-face 선언이 대체되지 않아요.

폰트 크기 (Font size)

Material UI는 폰트 크기에 rem 단위를 사용해요. 브라우저 <html> 요소의 기본 폰트 크기는 16px이지만, 브라우저에는 이 값을 변경할 수 있는 옵션이 있습니다. 따라서 rem 단위는 사용자의 설정을 수용할 수 있게 해 주고, 결과적으로 더 나은 접근성(accessibility) 지원을 가능하게 해요. 사용자들은 시력이 좋지 않아서부터 크기와 시청 거리가 크게 다른 기기에 최적 설정을 고르는 것까지, 다양한 이유로 폰트 크기 설정을 변경합니다.

Material UI의 폰트 크기를 변경하려면 fontSize 속성을 제공하면 돼요. 기본값은 14px이에요.

const theme = createTheme({
  typography: {
    // In Chinese and Japanese the characters are usually larger,
    // so a smaller fontsize may be appropriate.
    fontSize: 12,
  },
});

브라우저가 계산하는 폰트 크기는 다음 수학 공식을 따릅니다:

font size calculation
font size calculation

반응형 폰트 크기 (Responsive font sizes)

theme.typography.* 변형(variant) 속성은 생성된 CSS에 직접 매핑돼요. 그 안에서 미디어 쿼리를 사용할 수 있답니다:

const baseTheme = createTheme();

const theme = createTheme({
  typography: {
    h3: {
      fontSize: '1.2rem',
      '@media (min-width:600px)': {
        fontSize: '1.5rem',
      },
      [baseTheme.breakpoints.up('md')]: {
        fontSize: '2.4rem',
      },
    },
  },
});
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Typography from '@mui/material/Typography';

const theme = createTheme();

theme.typography.h3 = {
  fontSize: '1.2rem',
  '@media (min-width:600px)': {
    fontSize: '1.5rem',
  },
  [theme.breakpoints.up('md')]: {
    fontSize: '2rem',
  },
};

export default function CustomResponsiveFontSizes() {
  return (
    <ThemeProvider theme={theme}>
      <Typography variant="h3">Responsive h3</Typography>
    </ThemeProvider>
  );
}

이 설정을 자동화하려면 responsiveFontSizes() 헬퍼를 사용해서 테마 안의 Typography 폰트 크기를 반응형으로 만들 수 있어요.

// import of a small, pure module in a private demo
// bundle size and module duplication is negligible

// eslint-disable-next-line no-restricted-imports
import { convertLength } from '@mui/material/styles/cssUtils';
import { createTheme, responsiveFontSizes } from '@mui/material/styles';
import Box from '@mui/material/Box';
import { LineChart } from '@mui/x-charts';

let theme = createTheme();
theme = responsiveFontSizes(theme);

const colors = [
  '#443dc2',
  '#2060df',
  '#277e91',
  '#378153',
  '#4d811d',
  '#63780d',
  '#996600',
];
const variants = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'subtitle1'];

export default function ResponsiveFontSizesChart() {
  const convert = convertLength(theme.typography.htmlFontSize);
  const toPx = (rem) => parseFloat(convert(rem, 'px'));

  let dataset = theme.breakpoints.keys.map((key) => {
    const viewport = theme.breakpoints.values[key];
    const value = theme.breakpoints.up(key);

    const rep = { viewport };
    variants.forEach((variantName) => {
      const variant = theme.typography[variantName];
      if (variant[value]) {
        const fontSize = toPx(variant[value].fontSize);
        rep[variantName] = fontSize;
      } else if (viewport === 0) {
        const fontSize = toPx(variant.fontSize);
        rep[variantName] = fontSize;
      }
    });
    return rep;
  });

  dataset = [
    ...dataset.slice(0, dataset.length - 1),
    {
      ...dataset[dataset.length - 2],
      viewport: dataset[dataset.length - 1].viewport,

      subtitle1: dataset[0].subtitle1,
    },
  ];

  return (
    <Box sx={{ height: 380, width: '100%', color: 'black' }}>
      <LineChart
        dataset={dataset}
        series={variants.map((variantName) => ({
          curve: 'stepAfter',
          dataKey: variantName,
          label: variantName,
          connectNulls: true,
        }))}
        xAxis={[
          {
            scaleType: 'linear',
            dataKey: 'viewport',
            valueFormatter: (value) => value.toString(),
            tickNumber: 10,
            max: 1600,
            tickLabelStyle: { fontSize: 15 },
            label: 'viewport (px)',
          },
        ]}
        yAxis={[
          {
            valueFormatter: (value) => value.toString(),
            tickNumber: 5,
            min: 0,
            max: 100,
            tickLabelStyle: { fontSize: 15 },
            labelStyle: {
              fontSize: 15,
            },
            label: 'font-size (px)',
          },
        ]}
        colors={colors}
        margin={{ left: 70 }}
        sx={{
          [`.MuiChartsAxis-left .MuiChartsAxis-label`]: {
            transform: 'translateX(-5px)',
          },
          [`.MuiChartsAxis-bottom .MuiChartsAxis-label`]: {
            transform: 'translateY(5px)',
          },
          [`.MuiChartsAxis-root text`]: {
            fill: '#808080',
          },
          [`.MuiChartsAxis-root line`]: {
            stroke: '#808080',
          },
        }}
      />
    </Box>
  );
}

이 동작은 아래 예시에서 직접 확인할 수 있어요. 브라우저 창 크기를 조절하면서, 너비가 각기 다른 breakpoints를 넘어갈 때 폰트 크기가 어떻게 바뀌는지 주목해 보세요:

import { createTheme, responsiveFontSizes } from '@mui/material/styles';

let theme = createTheme();
theme = responsiveFontSizes(theme);
import {
  createTheme,
  responsiveFontSizes,
  ThemeProvider,
} from '@mui/material/styles';
import Typography from '@mui/material/Typography';

let theme = createTheme();
theme = responsiveFontSizes(theme);

export default function ResponsiveFontSizes() {
  return (
    <div>
      <ThemeProvider theme={theme}>
        <Typography variant="h3">Responsive h3</Typography>
        <Typography variant="h4">Responsive h4</Typography>
        <Typography variant="h5">Responsive h5</Typography>
      </ThemeProvider>
    </div>
  );
}

유동 폰트 크기 (Fluid font sizes)

작업 예정입니다: #15251.

HTML 폰트 크기 (HTML font size)

<html> 요소의 기본 폰트 크기를 바꾸고 싶을 수도 있어요. 예를 들어 10px 단순화 기법을 사용할 때 그렇죠.

:::warning 폰트 크기를 바꾸면 접근성에 해가 될 수 있어요 ♿️. 대부분의 브라우저는 기본 크기 16px에 동의하지만, 사용자가 이를 변경할 수 있어요. 예를 들어 시력이 좋지 않은 사람은 브라우저 기본 폰트 크기를 더 크게 설정할 수 있답니다. :::

theme.typography.htmlFontSize 속성이 이런 경우를 위해 제공돼요. 이 속성은 <html> 요소의 폰트 크기가 무엇인지 Material UI에 알려 줍니다. rem 값을 조정해서 계산된 폰트 크기가 항상 스펙과 일치하도록 하는 데 사용돼요.

const theme = createTheme({
  typography: {
    // Tell Material UI what the font-size on the html element is.
    htmlFontSize: 10,
  },
});
html {
  font-size: 62.5%; /* 62.5% of 16px = 10px */
}

아래 데모를 올바르게 렌더링하려면 위 CSS를 이 페이지의 HTML 요소에 적용해야 해요.

import { createTheme, ThemeProvider } from '@mui/material/styles';
import Typography from '@mui/material/Typography';

const theme = createTheme({
  typography: {
    // Tell MUI what the font-size on the html element is.
    htmlFontSize: 10,
  },
});

export default function FontSizeTheme() {
  return (
    <ThemeProvider theme={theme}>
      <Typography>body1</Typography>
    </ThemeProvider>
  );
}

변형 (Variants)

typography 객체는 기본적으로 13개의 변형(variants)을 제공해요:

  • h1
  • h2
  • h3
  • h4
  • h5
  • h6
  • subtitle1
  • subtitle2
  • body1
  • body2
  • button
  • caption
  • overline

이 변형들은 각각 개별적으로 커스터마이징할 수 있어요:

const theme = createTheme({
  typography: {
    subtitle1: {
      fontSize: 12,
    },
    body1: {
      fontWeight: 500,
    },
    button: {
      fontStyle: 'italic',
    },
  },
});
import { createTheme, ThemeProvider } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';

const theme = createTheme({
  typography: {
    subtitle1: {
      fontSize: 12,
    },
    body1: {
      fontWeight: 500,
    },
    button: {
      fontStyle: 'italic',
    },
  },
});

export default function TypographyVariants() {
  return (
    <div>
      <ThemeProvider theme={theme}>
        <Typography variant="subtitle1">subtitle</Typography>
        <Typography>body1</Typography>
        <Button>Button</Button>
      </ThemeProvider>
    </div>
  );
}

변형 추가 및 비활성화 (Adding & disabling variants)

기본 타이포그래피 변형을 사용하는 것 외에도, 커스텀 변형을 추가하거나 필요하지 않은 변형을 비활성화할 수 있어요. 그 방법은 다음과 같습니다:

1단계. 테마의 typography 객체 업데이트

아래 코드 스니펫은 poster라는 커스텀 변형을 테마에 추가하고, 기본 h3 변형을 제거해요:

const theme = createTheme({
  typography: {
    poster: {
      fontSize: '4rem',
      color: 'red',
    },
    // Disable h3 variant
    h3: undefined,
  },
});

2단계. (선택) 새 변형의 기본 시맨틱 요소 설정

이 시점에서 이미 새 poster 변형을 사용할 수 있어요. 기본적으로 커스텀 스타일이 적용된 <span>을 렌더링합니다. 때로는 시맨틱 목적을 위해 다른 HTML 요소를 기본으로 사용하고 싶을 수도 있고, 스타일링 목적으로 인라인 <span>을 블록 레벨 요소로 바꾸고 싶을 수도 있어요.

이를 위해 테마 레벨에서 Typography 컴포넌트의 variantMapping prop을 전역적으로 업데이트하세요:

const theme = createTheme({
  typography: {
    poster: {
      fontSize: 64,
      color: 'red',
    },
    // Disable h3 variant
    h3: undefined,
  },
  components: {
    MuiTypography: {
      defaultProps: {
        variantMapping: {
          // Map the new variant to render a <h1> by default
          poster: 'h1',
        },
      },
    },
  },
});

3단계. 필요한 타입 정의 업데이트 (TypeScript를 사용하는 경우)

:::info TypeScript를 사용하지 않는다면 이 단계는 건너뛰어도 돼요. :::

테마의 typography 변형과 Typography의 variant prop에 대한 타입 정의가 새 변형 세트를 반영하도록 해야 해요.

declare module '@mui/material/styles' {
  interface TypographyVariants {
    poster: React.CSSProperties;
  }

  // allow configuration using `createTheme()`
  interface TypographyVariantsOptions {
    poster?: React.CSSProperties;
  }
}

// Update the Typography's variant prop options
declare module '@mui/material/Typography' {
  interface TypographyPropsVariantOverrides {
    poster: true;
    h3: false;
  }
}

4단계. 이제 새 변형을 사용할 수 있어요

import { createTheme, ThemeProvider } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';

const theme = createTheme({
  typography: {
    // @ts-ignore
    poster: {
      fontSize: '4rem',
      color: 'indianred',
    },
    // Disable v3 variant
    h3: undefined,
  },
  components: {
    MuiTypography: {
      defaultProps: {
        variantMapping: {
          // @ts-ignore
          poster: 'h1', // map our new variant to render an <h1> by default
        },
      },
    },
  },
});

export default function TypographyCustomVariant() {
  return (
    <ThemeProvider theme={theme}>
      <Box sx={{ '& > *': { display: 'block' } }}>
        {/* @ts-ignore */}
        <Typography variant="poster">poster</Typography>
        <Typography variant="h3">h3</Typography>
      </Box>
    </ThemeProvider>
  );
}

<Typography variant="poster">poster</Typography>;

/* This variant is no longer supported. If you are using TypeScript it will give an error */
<Typography variant="h3">h3</Typography>;

기본값 (Default values)

typography의 기본값은 테마 탐색기 (theme explorer)를 사용하거나, 이 페이지에서 개발자 도구 콘솔을 열어 확인할 수 있어요 (window.theme.typography).

더 알아보기 (Learn more)