akjfal

Material UI 코드 따라가기 - Button 본문

카테고리 없음

Material UI 코드 따라가기 - Button

akjfal 2021. 11. 11. 01:32

우선 사용하는 방법을 보자

<Button variant="text">Text</Button>

컴포넌트안에 variant="text" 같이 변수를 인자로 넘겨준다.

인자로 들어가는 것들은 Button 에서 살펴볼 수 있으니 확인해보도록 하자.

Button 컴포넌트를 까보도록 하자

const Button = React.forwardRef(function Button(inProps, ref) {
  const props = useThemeProps({ props: inProps, name: "MuiButton" });
  const {
    className: classNameContext,
    color: colorContext,
    disabled: disabledContext,
    disableElevation: disableElevationContext,
    disableFocusRipple: disableFocusRippleContext,
    disableRipple: disableRippleContext,
    fullWidth: fullWidthContext,
    size: sizeContext,
    variant: variantContext,
  } = React.useContext(ButtonGroupContext);
  const {
    children,
    className,
    color: colorProp,
    component = "button",
    disabled: disabledProp,
    disableElevation: disableElevationProp,
    disableFocusRipple: disableFocusRippleProp,
    disableRipple: disableRippleProp,
    endIcon: endIconProp,
    focusVisibleClassName,
    fullWidth: fullWidthProp,
    size: sizeProp,
    startIcon: startIconProp,
    type,
    variant: variantProp,
    ...other
  } = props;

  const color = colorProp || colorContext || "primary";
  // TODO v6: Use nullish coalescing (??) instead of OR operator for these boolean props so that these boolean props for Button with ButtonGroup context take priority. See conversation from https://github.com/mui-org/material-ui/pull/28645#discussion_r738380902.
  const disabled = disabledProp || disabledContext || false;
  const disableElevation =
    disableElevationProp || disableElevationContext || false;
  const disableFocusRipple =
    disableFocusRippleProp || disableFocusRippleContext || false;
  const fullWidth = fullWidthProp || fullWidthContext || false;
  const size = sizeProp || sizeContext || "medium";
  const variant = variantProp || variantContext || "text";
  const disableRipple = disableRippleProp || disableRippleContext || false;

  const ownerState = {
    ...props,
    color,
    component,
    disabled,
    disableElevation,
    disableFocusRipple,
    fullWidth,
    size,
    type,
    variant,
  };

  const classes = useUtilityClasses(ownerState);

  const startIcon = startIconProp && (
    <ButtonStartIcon className={classes.startIcon} ownerState={ownerState}>
      {startIconProp}
    </ButtonStartIcon>
  );

  const endIcon = endIconProp && (
    <ButtonEndIcon className={classes.endIcon} ownerState={ownerState}>
      {endIconProp}
    </ButtonEndIcon>
  );

  return (
    <ButtonRoot
      ownerState={ownerState}
      className={clsx(className, classNameContext)}
      component={component}
      disabled={disabled}
      disableRipple={disableRipple}
      focusRipple={!disableFocusRipple}
      focusVisibleClassName={clsx(classes.focusVisible, focusVisibleClassName)}
      ref={ref}
      type={type}
      {...other}
      classes={classes}
    >
      {startIcon}
      {children}
      {endIcon}
    </ButtonRoot>
  );
});

코드를 확인해보면

children은 컴포넌트에 children으로 사용되고 앞 뒤로 startIcon과 endIcon이 내부에서 생서되어서 사용되고 있다.

나머지 인자의 경우 각자의 변환이 이뤄진 후 ButtonRoot로 다시 넘어가게된다.

그렇다면 ButtonRoot로 넘어가보자

const ButtonRoot = styled(ButtonBase, {
  shouldForwardProp: (prop) =>
    rootShouldForwardProp(prop) || prop === "classes",
  name: "MuiButton",
  slot: "Root",
  overridesResolver: (props, styles) => {
    const { ownerState } = props;

    return [
      styles.root,
      styles[ownerState.variant],
      styles[`${ownerState.variant}${capitalize(ownerState.color)}`],
      styles[`size${capitalize(ownerState.size)}`],
      styles[`${ownerState.variant}Size${capitalize(ownerState.size)}`],
      ownerState.color === "inherit" && styles.colorInherit,
      ownerState.disableElevation && styles.disableElevation,
      ownerState.fullWidth && styles.fullWidth,
    ];
  },
})(
  ({ theme, ownerState }) => ({
    ...theme.typography.button,
 
  // ...
 
 }),
  ({ ownerState }) =>
    ownerState.disableElevation && {
 // ...
    },
);

기본 css 설정 관련 코드들이 많아 생략 시켜두었다.

여기서 주의 깊게 보아야 할 것은 styled 함수를 통해서 컴포넌트를 생성한다는 것이다. 보면 styled를 통해 생성한 함수의 return값에 다시 인자를 넣어주어 생성한다. styled함수 안으로 들어가야 좀 더 정확한 파악이 가능하다.

export default function createStyled(input = {}) {
  const {
    defaultTheme = systemDefaultTheme,
    rootShouldForwardProp = shouldForwardProp,
    slotShouldForwardProp = shouldForwardProp,
  } = input;
  return (tag, inputOptions = {}) => {
    const {
      name: componentName,
      slot: componentSlot,
      skipVariantsResolver: inputSkipVariantsResolver,
      skipSx: inputSkipSx,
      overridesResolver,
      ...options
    } = inputOptions;

 // ...

      const Component = defaultStyledResolver(
        transformedStyleArg,
        ...expressionsWithDefaultTheme,
      );

      if (process.env.NODE_ENV !== "production") {
        let displayName;
        if (componentName) {
          displayName = `${componentName}${componentSlot || ""}`;
        }
        if (displayName === undefined) {
          displayName = `Styled(${getDisplayName(tag)})`;
        }
        Component.displayName = displayName;
      }

      return Component;
    };
    return muiStyledResolver;
  };
}

다양한 설정들이 있지만 스킵하고 넘어가보면 defaultStyledResolver를 통해서 Component를 생성하는데, defaultStyledResolver는 styledEngineStyled를 통해 생성된다. 그럼 다시 styledEngineStyled를 들어가보려고 한다.

import emStyled from '@emotion/styled';

export default function styled(tag, options) {
  const stylesFactory = emStyled(tag, options);

 // ...

  return stylesFactory;
}

export { ThemeContext, keyframes, css } from '@emotion/react';
export { default as StyledEngineProvider } from './StyledEngineProvider';
export { default as GlobalStyles } from './GlobalStyles';

코드를 이해하는데 필요없어보이는 부분을 생략해서 보면 emStyled를 통해 생성된 것을 리턴해주는 것을 알 수 있는데 여기서부턴 emotion.js를 통해 생성된다. emotion.js에 관련한 설명은 CSS-in-JS로 이 글 을 참고해보길 바란다.

 emStyled를 따라 들어가보면

import styled from './base'
import { tags } from './tags'

// bind it to avoid mutating the original function
const newStyled = styled.bind()

tags.forEach(tagName => {
  // $FlowFixMe: we can ignore this because its exposed type is defined by the CreateStyled type
  newStyled[tagName] = newStyled(tagName)
})

export default newStyled

해당 코드가 나오는데 다시 styled가 bind되어 있으니 base로 다시 들어간다.

// @flow
import * as React from 'react'
import {
  getDefaultShouldForwardProp,
  composeShouldForwardProps,
  type StyledOptions,
  type CreateStyled,
  type PrivateStyledComponent,
  type StyledElementType
} from './utils'
import { withEmotionCache, ThemeContext } from '@emotion/react'
import { getRegisteredStyles, insertStyles } from '@emotion/utils'
import { serializeStyles } from '@emotion/serialize'

let isBrowser = typeof document !== 'undefined'

let createStyled: CreateStyled = (tag: any, options?: StyledOptions) => {
  // ...
  return function <Props>(): PrivateStyledComponent<Props> {
    //...
    const Styled: PrivateStyledComponent<Props> = withEmotionCache(
      (props, cache, ref) => {
		// ...
        }
     )

    return Styled
  }
}

export default createStyled

createStyled는 또 다시 withEmotionCache를 통해 생성되니 다시 들어가보자

// @flow
import { type EmotionCache } from '@emotion/utils'
import * as React from 'react'
import { useContext, forwardRef } from 'react'
import createCache from '@emotion/cache'
import { isBrowser } from './utils'

let EmotionCacheContext: React.Context<EmotionCache | null> =
  React.createContext(
    typeof HTMLElement !== 'undefined'
      ? /* #__PURE__ */ createCache({ key: 'css' })
      : null
  )
 // ...
let withEmotionCache = function withEmotionCache<Props, Ref: React.Ref<*>>(
  func: (props: Props, cache: EmotionCache, ref: Ref) => React.Node
): React.AbstractComponent<Props> {
  return forwardRef((props: Props, ref: Ref) => {
    let cache = ((useContext(EmotionCacheContext): any): EmotionCache)

    return func(props, cache, ref)
  })
}
// ...

export { withEmotionCache }

그디어 마지막이 보인다. 컴포넌트를 인자로 내려내려 받아서 func에서 보이는 return 인 React.Node가 리턴되는것이다. 

 

... 문제가 생겼다. React.Node를 확인해보려고 하는데 React에서 해당 함수를 찾을 수 없었다. 이는 더 찾아봐야 할 듯하다. 해당 코드를 따라가면서 useContext가 정확히 리턴해주는 값등을 모르니 코드 이해가 겉햝기 식으로 밖에 되지 않아 추가적인 이해가 필요로 하지만, 당장 컴포넌트 분석이 목표이므로 나중으로 미루도록하겠다.

 

Comments