Frequently Asked Questions

Frequently Asked Questions (자주 묻는 질문)

특정 문제에 막혀 있나요? FAQ에서 먼저 몇 가지 흔한 함정을 확인해 보세요. 여전히 원하는 것을 찾지 못했다면 지원 페이지를 참고할 수 있어요.

출처: 문서

본문

특정 문제에 막혀 있나요? FAQ에서 먼저 몇 가지 흔한 함정을 확인해 보세요.

여전히 원하는 것을 찾지 못했다면, 지원 페이지를 참고할 수 있어요.

MUI는 훌륭한 조직입니다. 어떻게 지원할 수 있나요?

우리를 지원하는 방법은 많습니다.

  • 소문을 퍼뜨리세요. 웹사이트에 mui.com을 링크해 MUI의 제품을 홍보하세요—모든 백링크가 중요하니까요. X에서 우리를 팔로우하고, 중요한 소식을 좋아요하고 리트윗해 주세요. 아니면 친구들과 우리 이야기를 나누기만 해도 됩니다.
  • 피드백을 주세요. 잘 되는 부분이나 개선할 여지가 있는 부분을 알려주세요. 가장 해결되길 바라는 이슈에 투표(👍)해 주시면 좋겠어요.
  • 새 사용자를 도와주세요. Stack Overflow에서 질문에 답할 수 있어요.
  • 변화를 만들어 내세요.
  • Open Collective에서 재정적으로 지원해 주세요. 상용 프로젝트에서 Material UI를 사용하면서 후원자가 되어 지속적인 개발을 지원하고 싶다면, 또는 사이드/취미 프로젝트에서 사용하면서 Backer가 되고 싶다면 Open Collective를 통해 할 수 있어요. 기부된 모든 자금은 투명하게 관리되며, 후원자는 README와 홈페이지에서 인정받습니다.

모달이 열리면 왜 고정 위치(fixed position) 요소들이 움직이나요?

모달이 열리는 즉시 스크롤이 차단됩니다. 모달이 유일한 인터랙티브 콘텐츠여야 할 때 배경과의 상호작용을 막기 위해서죠. 그런데 스크롤바를 제거하면 fixed positioned 요소들이 움직일 수 있습니다. 이런 상황에서는 전역 .mui-fixed 클래스 이름을 적용해 Material UI가 해당 요소들을 처리하도록 할 수 있어요.

ripple 효과를 전역으로 어떻게 끄나요?

ripple 효과는 오로지 BaseButton 컴포넌트에서 나옵니다. 테마에서 다음을 제공하면 ripple 효과를 전역으로 비활성화할 수 있어요.

import { createTheme } from '@mui/material';

const theme = createTheme({
  components: {
    // Name of the component ⚛️
    MuiButtonBase: {
      defaultProps: {
        // The props to apply
        disableRipple: true, // No more ripple, on the whole application 💣!
      },
    },
  },
});

전환(transition) 효과를 전역으로 어떻게 끄나요?

Material UI는 모든 전환을 만들 때 동일한 테마 헬퍼를 사용합니다. 따라서 테마에서 그 헬퍼를 오버라이드하면 모든 전환을 비활성화할 수 있어요.

import { createTheme } from '@mui/material';

const theme = createTheme({
  transitions: {
    // So `transition: none;` gets applied everywhere
    create: () => 'none',
  },
});

시각적 테스트 중에 전환을 끄는 것이 유용하거나, 저사양 기기에서 성능을 개선하는 데 도움이 될 수 있어요.

한 단계 더 나아가 모든 전환과 애니메이션 효과를 비활성화할 수도 있습니다.

import { createTheme } from '@mui/material';

const theme = createTheme({
  components: {
    // Name of the component ⚛️
    MuiCssBaseline: {
      styleOverrides: {
        '*, *::before, *::after': {
          transition: 'none !important',
          animation: 'none !important',
        },
      },
    },
  },
});

위 접근 방식이 작동하려면 CssBaseline을 사용해야 한다는 점에 유의하세요. 사용하지 않기로 했다면, 다음 CSS 규칙을 포함해 전환과 애니메이션을 여전히 비활성화할 수 있어요.

*,
*::before,
*::after {
  transition: 'none !important';
  animation: 'none !important';
}

앱을 스타일링하는 데 Emotion을 사용해야 하나요?

아니요, 필수는 아닙니다. 하지만 기본 styled engine(@mui/styled-engine)을 사용한다면 Emotion 의존성이 내장되어 있어 추가 번들 크기 부담이 없습니다.

혹시 이미 다른 스타일링 솔루션을 사용하는 앱에 Material UI 컴포넌트를 추가하거나, 다른 API에 이미 익숙해서 새 것을 배우고 싶지 않으신가요? 그렇다면 Style library interoperability 섹션으로 가서 대안 스타일 라이브러리로 Material UI 컴포넌트를 다시 스타일링하는 방법을 배워 보세요.

inline-style과 CSS는 언제 써야 하나요?

경험상, inline-style은 동적 스타일 속성에만 사용하세요. CSS 대안은 더 많은 장점을 제공합니다.

  • 자동 접두사 처리(auto-prefixing)
  • 더 나은 디버깅
  • 미디어 쿼리
  • 키프레임

react-router를 어떻게 사용하나요?

react-router나 Next.js 같은 서드파티 라우팅 라이브러리와의 통합 가이드를 방문해 자세한 내용을 확인하세요.

DOM 요소에 어떻게 접근하나요?

DOM에 무엇인가를 렌더링해야 하는 모든 Material UI 컴포넌트는 ref를 기본 DOM 컴포넌트에 전달합니다. 즉 Material UI 컴포넌트에 붙인 ref를 읽어 DOM 요소를 얻을 수 있다는 뜻이에요.

// or a ref setter function
const ref = React.createRef();
// render
<Button ref={ref} />;
// usage
const element = ref.current;

해당 Material UI 컴포넌트가 ref를 전달하는지 확실하지 않다면 "Props" 아래의 API 문서를 확인할 수 있어요. Button API처럼 아래 메시지를 찾을 수 있을 거예요.

ref는 루트 요소로 전달됩니다.

앱이 서버에서 올바르게 렌더링되지 않아요

작동하지 않는다면 99%의 경우 구성(설정) 문제입니다. 누락된 속성, 잘못된 호출 순서, 누락된 컴포넌트—서버 사이드 렌더링은 설정에 엄격하니까요.

무엇이 잘못됐는지 찾는 가장 좋은 방법은 프로젝트를 이미 잘 동작하는 설정과 비교하는 것입니다. reference implementations를 하나씩 살펴보세요.

제가 보는 색상이 여기서 보는 것과 다른 이유는?

문서 사이트는 커스텀 테마를 사용합니다. 따라서 컬러 팔레트가 Material UI가 기본 제공하는 기본 테마와 다릅니다. 테마 커스터마이즈에 대해 배우려면 이 페이지를 참고하세요.

컴포넌트 X는 왜 props에 ref 객체 대신 DOM 노드를 요구하나요?

Portal이나 Popper 같은 컴포넌트는 각각 container 또는 anchorEl prop에 DOM 노드를 요구합니다. 그런 props에 그냥 ref 객체를 넘기고 Material UI가 현재 값을 접근하게 하는 것이 편리해 보이죠.

이는 단순한 시나리오에서는 동작합니다.

function App() {
  const container = React.useRef(null);

  return (
    <div className="App">
      <Portal container={container}>
        <span>portaled children</span>
      </Portal>
      <div ref={container} />
    </div>
  );
}

여기서 Portal은 container.current를 사용할 수 있을 때만 children을 container에 마운트합니다. 다음은 Portal의 단순한 구현입니다.

function Portal({ children, container }) {
  const [node, setNode] = React.useState(null);

  React.useEffect(() => {
    setNode(container.current);
  }, [container]);

  if (node === null) {
    return null;
  }
  return ReactDOM.createPortal(children, node);
}

이 단순한 휴리스틱으로는, ref가 어떤 effects가 실행되기 전에 최신 상태이기 때문에 Portal이 마운트된 후 다시 렌더링될 수 있습니다. 하지만 ref가 최신이라고 해서 정의된 인스턴스를 가리킨다는 뜻은 아닙니다. ref가 ref 전달 컴포넌트에 붙어 있으면 DOM 노드를 언제 사용할 수 있을지 명확하지 않아요.

위 예시에서 Portal은 effect를 한 번 실행하지만 ref.current가 여전히 null이기 때문에 다시 렌더링되지 않을 수 있습니다. 이는 특히 Suspense에서 React.lazy 컴포넌트에서 두드러집니다. 위 구현은 DOM 노드의 변화를 처리하지도 못할 수 있어요.

그래서 React가 Portal이 언제 다시 렌더링해야 하는지 결정하게 하기 위해 실제 DOM 노드를 prop으로 요구하는 것입니다.

function App() {
  const [container, setContainer] = React.useState(null);
  const handleRef = React.useCallback(
    (instance) => setContainer(instance),
    [setContainer],
  );

  return (
    <div className="App">
      <Portal container={container}>
        <span>Portaled</span>
      </Portal>
      <div ref={handleRef} />
    </div>
  );
}

clsx dependency는 무엇을 위한 건가요?

clsx는 키가 클래스 문자열이고 값이 boolean인 객체에서 className 문자열을 조건부로 구성하는 아주 작은 유틸리티입니다.

이렇게 쓰는 대신:

// let disabled = false, selected = true;

return (
  <div
    className={`MuiButton-root ${disabled ? 'Mui-disabled' : ''} ${
      selected ? 'Mui-selected' : ''
    }`}
  />
);

이렇게 할 수 있어요:

import clsx from 'clsx';

return (
  <div
    className={clsx('MuiButton-root', {
      'Mui-disabled': disabled,
      'Mui-selected': selected,
    })}
  />
);

styled() 유틸리티에서 컴포넌트를 selector로 사용할 수 없어요. 어떻게 해야 하나요?

TypeError: Cannot convert a Symbol value to a string 오류가 발생한다면, styled() 문서 페이지에서 고치는 방법에 대한 지침을 확인하세요.

무료 템플릿에 어떻게 기여할 수 있나요?

템플릿은 shared theme를 사용해 만들어집니다. 아래는 새 템플릿을 만드는 구조입니다.

템플릿 페이지 (Template page)

docs/pages/material-ui/getting-started/templates/<name>.js 디렉토리에 다음 코드로 새 페이지를 만드세요.

import * as React from 'react';
import AppTheme from 'docs/src/modules/components/AppTheme';
import TemplateFrame from 'docs/src/modules/components/TemplateFrame';
import Template from 'docs/data/material/getting-started/templates/<name>/<Template>';

export default function Page() {
  return (
    <AppTheme>
      <TemplateFrame>
        <Template />
      </TemplateFrame>
    </AppTheme>
  );
}

그런 다음 docs/data/material/getting-started/templates/<name>/<Template>.tsx에 템플릿 파일을 만드세요(필요하면 파일을 더 추가하세요).

참고: <Template>은 <name> 폴더의 pascal case 문자열이어야 합니다.

공유 테마 (Shared theme)

템플릿은 모든 템플릿에서 일관된 모양과 느낌을 보장하기 위해 ../shared-theme/AppTheme의 AppTheme을 사용해야 합니다. 대시보드 템플릿의 MUI X 테마 컴포넌트처럼 커스텀 테마 컴포넌트가 템플릿에 포함된다면, 그것들을 AppTheme의 themedComponents prop에 넘기세요.

import AppTheme from '../shared-theme/AppTheme';

const xThemeComponents = {
  ...chartsCustomizations,
  ...dataGridCustomizations,
  ...datePickersCustomizations,
  ...treeViewCustomizations,
};

export default function Dashboard(props: { disableCustomTheme?: boolean }) {
  return (
    <AppTheme {...props} themeComponents={xThemeComponents}>...</AppTheme>
  )
}

색상 모드 토글 (Color mode toggle)

공유 테마는 색상 모드 토글의 2가지 모습인 ColorModeSelect와 ColorModeIconDropdown을 제공합니다. 둘 중 아무거나 템플릿에서 사용할 수 있으며, TemplateFrame 안에서는 숨겨지지만 CodeSandbox와 StackBlitz에서는 보입니다.

템플릿 프레임 (Template frame)

템플릿에 상단에 고정해야 하는 사이드바나 헤더가 있다면, CSS 변수 --template-frame-height를 참조해 조정하세요.

예를 들어 대시보드 템플릿에는 템플릿 프레임 높이를 고려해야 하는 고정 헤더가 있습니다.

<AppBar
  position="fixed"
  sx={{
    top: 'var(--template-frame-height, 0px)',
    // ...other styles
  }}
>

이렇게 하면 AppBar가 미리보기 모드에서는 TemplateFrame 아래에 머물지만, CodeSandbox와 StackBlitz에서는 상단에 고정됩니다.

[legacy] 페이지에 스타일 인스턴스가 여러 개 있습니다

아래와 같은 경고 메시지가 콘솔에서 보인다면, 페이지에 @mui/styles 인스턴스가 여러 개 초기화되어 있을 가능성이 높습니다.

:::warning It looks like there are several instances of @mui/styles initialized in this application. This may cause theme propagation issues, broken class names, specificity issues, and make your application bigger without a good reason.

이 애플리케이션에 @mui/styles 인스턴스가 여러 개 초기화된 것 같습니다. 이는 테마 전파 문제, 깨진 클래스 이름, specificity 문제를 일으키고, 이유 없이 애플리케이션을 더 크게 만들 수 있어요. :::

가능한 원인 (Possible reasons)

이런 일이 생기는 흔한 이유가 몇 가지 있습니다.

  • 의존성 어딘가에 또 다른 @mui/styles 라이브러리가 있습니다.
  • 프로젝트가 모노레포 구조(예: lerna나 yarn workspaces)이고 @mui/styles 모듈이 둘 이상의 패키지에서 의존성으로 있습니다(앞의 경우와 거의 동일합니다).
  • 같은 페이지에서 @mui/styles를 사용하는 여러 앱이 실행 중입니다(예: webpack의 여러 엔트리 포인트가 같은 페이지에 로드됨).

node_modules의 중복 모듈 (Duplicated module in node_modules)

문제가 의존성 어딘가에서 @mui/styles 모듈이 중복되는 것이라고 생각한다면, 확인할 방법이 몇 가지 있습니다. 앱 폴더에서 npm ls @mui/styles, yarn list @mui/styles 또는 find -L ./node_modules | grep /@mui/styles/package.json 명령을 사용할 수 있어요.

이 명령들 중 어느 것도 중복을 식별하지 못했다면, 번들에서 @mui/styles 인스턴스가 여러 개인지 분석해 보세요. 번들 소스를 확인하거나 source-map-explorer나 webpack-bundle-analyzer 같은 도구를 사용할 수 있어요.

중복이 문제라고 식별했다면 해결을 시도할 수 있는 몇 가지가 있습니다.

npm을 사용한다면 npm dedupe를 실행해 보세요. 이 명령은 로컬 의존성을 검색해 공통 의존성을 트리 위로 옮겨 구조를 단순화하려고 시도합니다.

webpack을 사용한다면 @mui/styles 모듈을 resolve하는 방식을 변경할 수 있습니다. webpack이 의존성을 찾는 기본 순서를 덮어써서, 기본 node module 해석 순서보다 앱의 node_modules를 더 우선시하게 만들 수 있어요.

 resolve: {
+  alias: {
+    '@mui/styles': path.resolve(appFolder, 'node_modules', '@mui/styles'),
+  },
 },

한 페이지에서 여러 앱 실행 (Running multiple applications on one page)

한 페이지에서 여러 앱을 실행한다면, 모두에 대해 하나의 @mui/styles 모듈을 사용하는 것을 고려하세요. webpack을 사용한다면, @mui/styles 모듈을 담을 공유 vendor chunk를 만들기 위해 splitChunks 구성을 사용할 수 있어요.

  module.exports = {
    entry: {
      app1: './src/app.1.js',
      app2: './src/app.2.js',
    },
+   optimization: {
+     splitChunks: {
+       cacheGroups: {
+         vendor: {
+           test: /[\\/]node_modules[\\/]@mui[\\/]styles[\\/]/,
+           name: 'vendor',
+           chunks: 'all',
+         },
+       },
+     },
+   },
  }

[legacy] 프로덕션 빌드에서 컴포넌트가 올바르게 렌더링되지 않는 이유는?

이런 일이 생기는 1순위 이유는 코드가 프로덕션 번들에 들어갈 때 발생하는 클래스 이름 충돌일 가능성이 높습니다. Material UI가 작동하려면, 페이지의 모든 컴포넌트의 className 값이 class name generator의 단일 인스턴스에서 생성되어야 합니다.

이 문제를 고치려면, 페이지의 모든 컴포넌트가 그들 사이에 단 하나의 class name generator만 있도록 초기화되어야 합니다.

여러 시나리오에서 실수로 두 개의 class name generator를 사용하게 될 수 있습니다.

  • 실수로 두 버전의 @mui/styles를 번들했을 수 있습니다. 어떤 의존성이 Material UI를 peer dependency로 올바르게 설정하지 않았을 수 있어요.
  • React 트리의 일부에 대해 StylesProvider를 사용하고 있습니다.
  • 번들러를 사용하고 있고, 코드를 분할하는 방식 때문에 여러 개의 class name generator 인스턴스가 만들어집니다.

:::success SplitChunksPlugin과 함께 webpack을 사용한다면, optimizations 아래의 runtimeChunk 설정을 구성해 보세요. :::

전반적으로 각 Material UI 앱을 컴포넌트 트리 상단에서 StylesProvider 컴포넌트로 감싸고, 그들 사이에 공유되는 단일 class name generator를 사용하면 이 문제에서 쉽게 회복할 수 있습니다.

[legacy] CSS가 첫 로드에만 작동하고 사라져요

CSS는 페이지의 첫 로드에만 생성됩니다. 그다음 연속되는 요청에서는 서버에서 CSS가 사라집니다.

취해야 할 조치 (Action to Take)

스타일링 솔루션은 캐시인 _sheets manager_에 의존해 컴포넌트 타입당 한 번만 CSS를 주입합니다(버튼 두 개를 사용해도 버튼의 CSS는 한 번만 필요하죠). 각 요청마다 새로운 sheets 인스턴스를 만들어야 합니다.

수정 예시:

-// Create a sheets instance.
-const sheets = new ServerStyleSheets();

 function handleRender(req, res) {
+  // Create a sheets instance.
+  const sheets = new ServerStyleSheets();

   //…

   // Render the component to a string.
   const html = ReactDOMServer.renderToString(

[legacy] React 클래스 이름 하이드레이션 불일치

:::warning Prop className did not match. :::

클라이언트와 서버 사이에 클래스 이름 불일치가 있습니다. 첫 요청에서는 작동할 수 있어요. 또 다른 증상은 초기 페이지 로드와 클라이언트 스크립트 다운로드 사이에 스타일링이 변하는 것입니다.

취해야 할 조치 (Action to Take)

클래스 이름 값은 class name generator의 개념에 의존합니다. 전체 페이지는 단일 generator로 렌더링되어야 합니다. 이 generator는 서버와 클라이언트에서 동일하게 동작해야 합니다. 예를 들어:

  • 각 요청에 대해 새 class name generator를 제공해야 합니다. 하지만 다른 요청 간에 createGenerateClassName()을 공유해서는 안 됩니다.

    수정 예시:

    -// Create a new class name generator.
    -const generateClassName = createGenerateClassName();
    
     function handleRender(req, res) {
    +  // Create a new class name generator.
    +  const generateClassName = createGenerateClassName();
    
       //…
    
       // Render the component to a string.
       const html = ReactDOMServer.renderToString(
    
  • 클라이언트와 서버가 정확히 동일한 버전의 Material UI를 실행하고 있는지 확인해야 합니다. 마이너 버전의 불일치만으로도 스타일링 문제가 생길 수 있습니다. 버전 번호를 확인하려면, 앱을 빌드하는 환경과 배포 환경에서 npm list @mui/styles를 실행하세요.

    package.json의 의존성에 특정 Material UI 버전을 지정해 여러 환경에서 동일한 버전을 보장할 수도 있습니다.

    수정 예시 (package.json):

      "dependencies": {
        ...
    -   "@mui/styles": "^5.0.0",
    +   "@mui/styles": "5.0.0",
        ...
      },
    
  • 서버와 클라이언트가 같은 process.env.NODE_ENV 값을 공유하는지 확인해야 합니다.

더 알아보기 (Learn more)