FE팀 해방전쟁 - UI Kit 편
Table of Contents
v1 retrospective
별다른 사상없이 antd를 사용해서 디자인 시스템 v1을 운영한지 8개월차... 문제가 끊임없이 발생했는데 대표적인 것들만 추리면
- 디자인이 나오지 않으면 디자인을 기다리고 있는 팀원들
- antd 컴포넌트에 기능, 스타일, 테스트 추가 후 통째로 관리해서 유지보수가 헬게이트
- inline / block, width, height 등과 같이 레이아웃 관련 스타일도 컴포넌트 내부에서 직접 정하다보니
- e.g. modal 컴포넌트의 prop이 어느새 modalSize: small, medium-480, medium-540, medium-560, medium, large, large-894, etc.. 2절에 3절 끝도 없는 prop 연장
- antd 스타일 덮어쓰기 실패 → !important, :global() 선택자가 남용
등 어느새 컴포넌트에 문제가 생기면 컴포넌트 만든 사람과 그걸 리뷰한 사람을 찾고 있는 술래잡기가 반복되고 있었다. 도저히 못참겠어서 리뉴얼해서 v2를 만들자고 제의하기에 이르렀지만, 새로운 제품이 곧 출시되기에 들일 수 있는 리소스는 최저치였다.
v2 goal
어떻게든 직접 구현하는 일을 줄이고 R&R이 사람한테 가서는 안된다는 생각이 들었다. 그래서 제안한 v2의 멘탈 모델은 다음과 같다.
- 디자인이 나오지 않았더라도, 디자인이 크게 깨지지 않는 선에서 개발 가능하도록 theme 활용
- mantine에 스타일링만한 core와 기능 및 테스트도 추가한 extended 별도 운영해서 유지보수 범위 명시
- 레이아웃 관련 스타일은 외부에서 주입
- 직업 컴포넌트 내부를 스타일링 해야한다면 width:100%, height: auto를 가지고 외부에서 크기 조절할 수 있도록하자
- theme이 지정한 스타일 그대로 활용 / 그외 custom component는 직접 작성 (unstyled prop활용)
한마디로 축약하면 컴포넌트 기능에 이상이 있으면 그건 Mantine을 제대로 활용하지 못했기에 혹은 Mantine 자체에 버그가 있는걸로 R&R을 바꾼 것이라고 할 수 있다. 그 외 정말 우리 서비스에서만 사용해야할 custom component를 구현해야 할 때도 theme system이 잘 구축된 mantine을 활용하면 전체 스타일 가이드에서 크게 벗어나지 않고 구현가능 하리라하는 가정이었다.
Mantine을 선택한 이유
모든 mantine 컴포넌트는 box factory에서 생성된다. 즉 어떤 컴포넌트를 사용하던 간에 공용 prop이 존재한다는 거고, 그 중 가장 고마운 것은 theme에서 값을 뽑아올 수 있는 styles api였다.
import { Box } from '@mantine/core'
function Demo() {
return (
<Box mx="auto" maw={400} bg="#fff">
Your component
</Box>
)
}
styles api는 아래 예시 표처럼 prop으로 직접 꽂아줄 수도 있고 class를 활용해서 더 세분화 시켜 styling할 수 있다.
Prop | CSS Property | Theme key |
---|---|---|
m | margin | theme.spacing |
mt | marginTop | theme.spacing |
mb | marginBottom | theme.spacing |
ml | marginLeft | theme.spacing |
Selector | Static selector | Description |
---|---|---|
root | .mantine-Button-root | Root element |
loader | .mantine-Button-loader | Loader component, displayed only when loading prop is set |
inner | .mantine-Button-inner | Contains all other elements, child of the root element |
section | .mantine-Button-section | Left and right sections of the button |
label | .mantine-Button-label | Button children |
다만 이렇게 좋아보여도 mantine의 styles api를 잘 활용하기 위해서는 초기 세팅 비용 상당히 드는데...
Style api 적극 활용하기
우선 기본 골자로 MantineProvider
에 theme 객체를 넣어주고 시작을 한다
import { MantineProvider } from '@mantine/core'
import { primitiveTheme } from '@/ui-v2/primitive-theme'
const root = createRoot(document.querySelector('#root') as HTMLElement)
root.render(
<StrictMode>
<MantineProvider
theme={primitiveTheme}
>
<App />
</MantineProvider>
</StrictMode>
)
이렇게 main부터 감싸고 시작하면 페이지마다 theme이 달라질 경우 어떻게 할거냐! 한다면
import { MantineThemeProvider, mergeThemeOverrides } from '@mantine/core'
import { primitiveTheme } from '@/ui-v2/primitive-theme'
import { buttonTheme } from '@/ui-v2/components/button'
// Note: It is better to to store theme override outside of component body
// to prevent unnecessary re-renders
const theme = mergeThemeOverrides(primitiveTheme, buttonTheme)
export function Page() {
return (
<MantineThemeProvider theme={theme}>
<Button>오버라이딩 된 버튼</Button>
</MantineThemeProvider>
)
}
와 같은 방법도 있다.
기본 골자를 살펴봤으니 theme을 잘활용하기 위해서 대략 정리한 세팅 순서를 보자
- 우선 전역에서 사용될 theme의 변수들이 가장 영향력이 크다. 그렇기에 이것부터 세팅해야 한다. global theme's variables + override
- 전역 theme의 변수로 안된다면 컴포넌트 개별 theme이 그 다음으로 영향력이 크다. css variables
- 만약 컴포넌트의 variant가 스타일 가이드와 맞지 않는다면 custom variant를 추가할 수도 있다. variantColorsResolver
- 다 추가한 이후에는 전역 theme 객체와 합쳐줘야 한다. mergeThemeOverrides
- 여기까지 하면 왠만한 스타일링은 다 할 수 있지만, 그래도 못하는 경우가 있다. 그럴 때는 초심으로 돌아가서 css를 직접 작성해야 한다. style api
- 만약 custom component를 구현해야하는데 위 3방법으로도 커버가 안되는 스타일링이 있다면 아예 백지부터 시작할수도 있다. unstyled prop
아무래도 theme을 활용해야 하다보니 컴포넌트와 1대1로 스타일링 할때보다는 신경쓸게 많아졌지만 경험상 theme의 진가는 세팅이 많이 되어있을 수록 개발을 이어서 할 사람이 스타일링에 대한 부담이 점점 적어져간다는 것이다. (물론 일관된 디자인을 하는 디자이너와 함께한다는 가정 하에서)