Rating

Rating

Rating은 다른 사람들의 의견과 경험에 대한 통찰을 제공하고, 사용자가 자신의 평점을 직접 제출할 수 있게 해주는 컴포넌트예요. 제품이나 서비스에 대한 피드백을 별점으로 간단히 받고 싶을 때 아주 유용하답니다.

출처: 문서

본문

Rating은 다른 사람들의 의견과 경험에 대한 통찰을 제공하고, 사용자가 자신의 평점을 제출할 수 있게 해줘요.

기본 rating (Basic rating)

import * as React from 'react';
import Box from '@mui/material/Box';
import Rating from '@mui/material/Rating';
import Typography from '@mui/material/Typography';

export default function BasicRating() {
  const [value, setValue] = React.useState<number | null>(2);

  return (
    <Box sx={{ '& > legend': { mt: 2 } }}>
      <Typography component="legend">Controlled</Typography>
      <Rating
        name="simple-controlled"
        value={value}
        onChange={(event, newValue) => {
          setValue(newValue);
        }}
      />
      <Typography component="legend">Uncontrolled</Typography>
      <Rating
        name="simple-uncontrolled"
        onChange={(event, newValue) => {
          console.log(newValue);
        }}
        defaultValue={2}
      />
      <Typography component="legend">Read only</Typography>
      <Rating name="read-only" value={value} readOnly />
      <Typography component="legend">Disabled</Typography>
      <Rating name="disabled" value={value} disabled />
      <Typography component="legend">No rating given</Typography>
      <Rating name="no-value" value={null} />
    </Box>
  );
}

Rating 정밀도 (Rating precision)

rating은 value prop으로 어떤 소수(float) 숫자든 표시할 수 있어요. precision prop을 사용해서 허용되는 최소 단위 변경값(increment)을 정의하세요.

import Rating from '@mui/material/Rating';
import Stack from '@mui/material/Stack';

export default function HalfRating() {
  return (
    <Stack spacing={1}>
      <Rating name="half-rating" defaultValue={2.5} precision={0.5} />
      <Rating name="half-rating-read" defaultValue={2.5} precision={0.5} readOnly />
    </Stack>
  );
}

호버 피드백 (Hover feedback)

호버 시 라벨을 표시해서 사용자가 올바른 rating 값을 고르도록 도울 수 있어요. 이 데모는 onChangeActive prop을 사용합니다.

import * as React from 'react';
import Rating from '@mui/material/Rating';
import Box from '@mui/material/Box';
import StarIcon from '@mui/icons-material/Star';

const labels: { [index: string]: string } = {
  0.5: 'Useless',
  1: 'Useless+',
  1.5: 'Poor',
  2: 'Poor+',
  2.5: 'Ok',
  3: 'Ok+',
  3.5: 'Good',
  4: 'Good+',
  4.5: 'Excellent',
  5: 'Excellent+',
};

function getLabelText(value: number) {
  return `${value} Star${value !== 1 ? 's' : ''}, ${labels[value]}`;
}

export default function HoverRating() {
  const [value, setValue] = React.useState<number | null>(2);
  const [hover, setHover] = React.useState(-1);

  return (
    <Box sx={{ width: 200, display: 'flex', alignItems: 'center' }}>
      <Rating
        name="hover-feedback"
        value={value}
        precision={0.5}
        getLabelText={getLabelText}
        onChange={(event, newValue) => {
          setValue(newValue);
        }}
        onChangeActive={(event, newHover) => {
          setHover(newHover);
        }}
        emptyIcon={<StarIcon style={{ opacity: 0.55 }} fontSize="inherit" />}
      />
      {value !== null && (
        <Box sx={{ ml: 2 }}>{labels[hover !== -1 ? hover : value]}</Box>
      )}
    </Box>
  );
}

크기 (Sizes)

더 크거나 작은 rating이 필요하다면 size prop을 사용하세요.

import Rating from '@mui/material/Rating';
import Stack from '@mui/material/Stack';

export default function RatingSize() {
  return (
    <Stack spacing={1}>
      <Rating name="size-small" defaultValue={2} size="small" />
      <Rating name="size-medium" defaultValue={2} />
      <Rating name="size-large" defaultValue={2} size="large" />
    </Stack>
  );
}

커스터마이즈 (Customization)

여기 컴포넌트를 커스터마이즈하는 몇 가지 예시가 있어요. 이에 대해 더 자세히 알아보려면 오버라이드 문서 페이지를 참고하세요.

import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Rating from '@mui/material/Rating';
import FavoriteIcon from '@mui/icons-material/Favorite';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import Typography from '@mui/material/Typography';

const StyledRating = styled(Rating)({
  '& .MuiRating-iconFilled': {
    color: '#ff6d75',
  },
  '& .MuiRating-iconHover': {
    color: '#ff3d47',
  },
});

export default function CustomizedRating() {
  return (
    <Box sx={{ '& > legend': { mt: 2 } }}>
      <Typography component="legend">Custom icon and color</Typography>
      <StyledRating
        name="customized-color"
        defaultValue={2}
        getLabelText={(value: number) => `${value} Heart${value !== 1 ? 's' : ''}`}
        precision={0.5}
        icon={<FavoriteIcon fontSize="inherit" />}
        emptyIcon={<FavoriteBorderIcon fontSize="inherit" />}
      />
      <Typography component="legend">10 stars</Typography>
      <Rating name="customized-10" defaultValue={2} max={10} />
    </Box>
  );
}

라디오 그룹 (Radio group)

rating은 라디오 그룹으로 구현되어 있어요. highlightSelectedOnly를 설정하면 자연스러운 동작을 복원할 수 있어요.

import * as React from 'react';
import { styled } from '@mui/material/styles';
import Rating, { IconContainerProps } from '@mui/material/Rating';
import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied';
import SentimentDissatisfiedIcon from '@mui/icons-material/SentimentDissatisfied';
import SentimentSatisfiedIcon from '@mui/icons-material/SentimentSatisfied';
import SentimentSatisfiedAltIcon from '@mui/icons-material/SentimentSatisfiedAltOutlined';
import SentimentVerySatisfiedIcon from '@mui/icons-material/SentimentVerySatisfied';

const StyledRating = styled(Rating)(({ theme }) => ({
  '& .MuiRating-iconEmpty .MuiSvgIcon-root': {
    color: (theme.vars || theme).palette.action.disabled,
  },
}));

const customIcons: {
  [index: string]: {
    icon: React.ReactElement<unknown>;
    label: string;
  };
} = {
  1: {
    icon: <SentimentVeryDissatisfiedIcon color="error" />,
    label: 'Very Dissatisfied',
  },
  2: {
    icon: <SentimentDissatisfiedIcon color="error" />,
    label: 'Dissatisfied',
  },
  3: {
    icon: <SentimentSatisfiedIcon color="warning" />,
    label: 'Neutral',
  },
  4: {
    icon: <SentimentSatisfiedAltIcon color="success" />,
    label: 'Satisfied',
  },
  5: {
    icon: <SentimentVerySatisfiedIcon color="success" />,
    label: 'Very Satisfied',
  },
};

function IconContainer(props: IconContainerProps) {
  const { value, ...other } = props;
  return <span {...other}>{customIcons[value].icon}</span>;
}

export default function RadioGroupRating() {
  return (
    <StyledRating
      name="highlight-selected-only"
      defaultValue={2}
      getLabelText={(value: number) => customIcons[value].label}
      slotProps={{ icon: { component: IconContainer } }}
      highlightSelectedOnly
    />
  );
}

접근성 (Accessibility)

(WAI tutorial)

이 컴포넌트의 접근성은 다음에 의존해요:

  • 필드가 시각적으로 숨겨진 라디오 그룹. 여기에는 각 별에 하나씩, 그리고 기본으로 체크되어 있는 0점 별을 포함해 여섯 개의 라디오 버튼이 들어 있어요. 부모 폼에서 고유한 name prop 값을 반드시 제공하세요.
  • 실제 텍스트("1 Star", "2 Stars", …)를 가진 라디오 버튼의 라벨. 페이지가 영어가 아닌 다른 언어라면 getLabelText prop에 적절한 함수를 반드시 제공하세요. 포함된 로케일을 사용하거나, 직접 제공할 수 있어요.
  • rating 아이콘의 시각적으로 구별되는 외형. 기본적으로 rating 컴포넌트는 값을 나타내기 위해 색상과 모양(채워진/빈 아이콘)의 차이를 모두 사용해요. 색상을 값 표시의 유일한 수단으로 사용한다면, 이 데모처럼 정보를 텍스트로도 표시해야 해요. 이는 WCAG 2.2 성공 기준 1.4.1을 충족하는 데 중요해요.
import Box from '@mui/material/Box';
import Rating from '@mui/material/Rating';
import StarIcon from '@mui/icons-material/Star';

const labels: { [index: string]: string } = {
  0.5: 'Useless',
  1: 'Useless+',
  1.5: 'Poor',
  2: 'Poor+',
  2.5: 'Ok',
  3: 'Ok+',
  3.5: 'Good',
  4: 'Good+',
  4.5: 'Excellent',
  5: 'Excellent+',
};

export default function TextRating() {
  const value = 3.5;

  return (
    <Box sx={{ width: 200, display: 'flex', alignItems: 'center' }}>
      <Rating
        name="text-feedback"
        value={value}
        readOnly
        precision={0.5}
        emptyIcon={<StarIcon style={{ opacity: 0.55 }} fontSize="inherit" />}
      />
      <Box sx={{ ml: 2 }}>{labels[value]}</Box>
    </Box>
  );
}

ARIA

읽기 전용(read only) rating은 "img" 역할과, 표시된 rating을 설명하는 aria-label을 가져요.

키보드 (Keyboard)

rating 컴포넌트는 라디오 버튼을 사용하므로 키보드 상호작용은 네이티브 브라우저 동작을 따릅니다. Tab은 현재 rating에 포커스를 주고, 커서 키는 선택된 rating을 제어해요.

읽기 전용 rating은 포커스가 불가능해요.

테스팅 (Testing)

Jest와 jsdom 같은 환경에서 Rating 컴포넌트를 테스트할 때, 특히 호버 기반 상호작용 같은 일부 사용자 상호작용은 예상대로 동작하지 않을 수 있어요. 이것은 컴포넌트가 각 아이콘의 위치를 계산하고 현재 호버 중인 아이콘을 결정하기 위해 getBoundingClientRect()에 의존하기 때문이에요. jsdom에서 getBoundingClientRect()는 기본적으로 0 값을 반환하므로, onChange 핸들러에 NaN이 전달되는 것 같은 잘못된 동작이 발생할 수 있어요.

테스트 스위트에서 이 문제를 피하려면:

  • 클릭 이벤트를 시뮬레이션할 때 userEvent보다 fireEvent를 선호하세요.
  • 변경을 트리거하기 위해 호버 동작에 의존하지 마세요.
  • 필요하다면 더 고급 상호작용을 위해 getBoundingClientRect()를 수동으로 mock 하세요.
// @vitest-environment jsdom

import { Rating } from '@mui/material';
import { render, fireEvent, screen } from '@testing-library/react';

import { describe, test, vi } from 'vitest';

describe('Rating', () => {
  test('should update rating on click', () => {
    const handleChange = vi.fn();
    render(<Rating onChange={(_, newValue) => handleChange(newValue)} />);

    fireEvent.click(screen.getByLabelText('2 Stars'));

    expect(handleChange).toHaveBeenCalledWith(2);
  });
});

Rating API

데모 (Demos)

이 React 컴포넌트의 사용 예시와 세부 내용은 컴포넌트 데모 페이지에서 확인하세요:

Import

import Rating from '@mui/material/Rating';
// or
import { Rating } from '@mui/material';

Props

Name Type Default Required Description
classes object - No Override or extend the styles applied to the component.
component elementType - No
defaultValue number null No
disabled bool false No
emptyIcon node <StarBorder fontSize="inherit" /> No
emptyLabelText node 'Empty' No
getLabelText function(value: number) => string `function defaultLabelText(value) {
return `${value '0'} Star${value !== 1 ? 's' : ''}`;
}` No
highlightSelectedOnly bool false No
icon node <Star fontSize="inherit" /> No
max number 5 No
name string - No
onChange function(event: React.SyntheticEvent, value: number | null) => void - No
onChangeActive function(event: React.SyntheticEvent, value: number) => void - No
precision number 1 No
readOnly bool false No
size 'small' | 'medium' | 'large' | string 'medium' No
slotProps { decimal?: func | object, icon?: func | object, label?: func | object, root?: func | object } {} No
slots { decimal?: elementType, icon?: elementType, label?: elementType, root?: elementType } {} No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.
value number - No

Note: The ref is forwarded to the root element (HTMLSpanElement).

Any other props supplied will be provided to the root element (native element).

테마 기본 props (Theme default props)

테마와 함께 MuiRating을 사용하면 이 컴포넌트의 기본 props를 변경할 수 있어요.

슬롯 (Slots)

Name Default Class Description
root 'span' .MuiRating-root The component used for the root slot.
label 'label' .MuiRating-label The component used for the label slot.
icon 'span' .MuiRating-icon The component used for the icon slot.
decimal 'span' .MuiRating-decimal The component used for the decimal slot.

CSS

규칙 이름 (Rule name)

Global class Rule name Description
.Mui-disabled - State class applied to the root element if disabled={true}.
.Mui-focusVisible - State class applied to the root element if keyboard focused.
- iconActive Styles applied to the icon wrapping elements when active.
- iconEmpty Styles applied to the icon wrapping elements when empty.
- iconFilled Styles applied to the icon wrapping elements when filled.
- iconFocus Styles applied to the icon wrapping elements when focus.
- iconHover Styles applied to the icon wrapping elements when hover.
- labelEmptyValueActive Styles applied to the label of the "no value" input when it is active.
.Mui-readOnly - Styles applied to the root element if readOnly={true}.
- sizeLarge Styles applied to the root element if size="large".
- sizeMedium Styles applied to the root element if size="medium".
- sizeSmall Styles applied to the root element if size="small".
- visuallyHidden Visually hide an element.

소스 코드 (Source code)

이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용을 위해 컴포넌트 구현을 살펴보는 것을 고려해보세요.

더 알아보기 (Learn more)