
국내에선 아직 Stitches와 Radix 자료가 많이 없길래 자료 공유 겸 개발일지를 포스팅해보려고 한다.
FE를 하다보면 스타일링이 여간 귀찮은 게 아니다. 특히나 처음부터 디자인 시스템을 구축해야 하는 상황이라면 ,, 초기 설정 단계가 꽤나 길어지는 것이 다반사. 물론 그렇게 구축하고 나서는 꽤나 편하게 작업이 가능하지만.
그래서 빠르게 스타일링을 진행할 수 있는 CSS Utility인 Tailwind를 쓰고 있었다. 러닝커브도 낮고, 각 객체에 맞는 스타일링을 그냥 생각한대로 슥슥 진행할 수 있어서 꽤 유용하게 쓰고 있었다. 그렇지만,, 더러운 inline들은 참을 수가 없더라. layer로 따로 정의를 해놓은다 한들 한 줄로 깔롱지게 만들고 싶은 나로서는 Tailwind의 편의성을 포기하면서까지 다른 방법을 찾고 싶었다. 까탈스러운 놈...

수많은 className의 악수...😇
그러므로, 스타일링 라이브러리를 이것저것 찾아보다가 발견하게 된 게 Stitches다. 요놈은 Tailwind와 마찬가지로 기본 토큰을 정의해놓을 수 있었고, 거기에 Emotion이나 Styled Component처럼 컴포넌트 형식으로 사용할 수 있었다. 보다 마음에 들었던 건 Variant를 지원하고 있어서 코드 상으로 분기를 주지 않고 라벨만 적어줘도 디자인의 변경이 가능했다(!)


What a beautiful world,,
언뜻 보기에는 Styled Component와 별 다른 게 없어보이지만, 기본적으로 Stitches는 Object 기반의 CSS이기 때문에 차지하는 메모리도 적고, 용량도 적다. 실제로 렌더링 타임이 다른 것들보다 적다.
그 외에도 Theme을 지원한다든가, 글로벌CSS, 미디어쿼리 처리도 간단하다든가, 또 기본 컴포넌트로부터 확장이 가능했다. 이 말인즉슨, 기본 html element 뿐만 아니라 Framer Motion의 Component라든가, Radix의 Component 역시 확장이 가능했다. (당연하게도 Stitches로 만든 컴포넌트 역시 가능하다) 무엇보다 Tailwind와 Styled Component의 묘한 접점이 있어서 러닝커브가 나한텐 높지 않았던 게 마음에 들었다. 그래서 이번엔 Stitches로 가보기로!
그리고 조사를 하면서 Radix UI라는 것도 있다는 것도 알게 되었다. UI를 짜다보면 사실 눈에 보이는 디자인을 구현하는 건 금방 하는데, 실제로 인터렉션이나 UX를 생각해서 기능을 구현하려고 하면 꽤나 귀찮아진다. Aria 지키고 접근성 지키고 표준 지키고 뭐시기 뭐시기 하면....
Radix는 이런 불편에서 어느 정도 벗어나게 해준다. 기본적으로 Unstyled 상태의 컴포넌트를 지원하는데, 해당 컴포넌트의 목적에 맞는 기능은 자체적으로 구현을 해놓은 셈이다. 예를 들어, Dialog를 만든다고 했을 때 Dialog를 여는 버튼, 열리면 Overlay를 깔고 어떤 Content를 보여주고, Submit or Cancel 이벤트 반응, 스크롤 방지, 열림 닫힘 상태 등등.. 기본적으로 모달이 동작하기 위한 최소한의 boilerplate를 만들어놓은 것이다. 거기에 UX를 생각해서 Esc라든가, 화살표라든가, Auto Focus라든가, TabIndex라든가, 그런 것들도 구현해놓았다고 하더라.
여기에 개발자는 styling만 진행하면 되는 것! 얼마나 간편합니까 ^~^ 그러므로 Radix도 한번 써보기로 했다.
여담으로, 디자인 시스템으로 Stitches와 Radix 조합을 선호한다고 하더라. 내가 아는 누구씨는 Radix와 Tailwind의 조합이 짜세라고 하긴 하더이다
현재 내가 개발하려고 하는 서비스는 모바일 PWA에서 아이디얼하게 굴러가는 걸 기본 전제로 깔고 간다. 즉, 어플 디자인에 가깝다.

초기 버전이니까 최대한 볼륨이 커지지 않게 조절했지만 그래도 뭐가 많이 나왔다. 나름 막디자인은 아니고 마진도 생각하고 컬러도 생각하고 한 거라 나중에 정리할 때 생각보단 쉽게 정리가 됐다.

컬럼 레이아웃으로 6 Count, Margin 24, Gutter 24로 진행했다. (이렇게 하면 w360 기준 한 width가 32가 딱 나온다) 여담으로, 사실 처음엔 느낌만 보려고 눈대중으로 배치를 한 건데 나중에 그리드 세워보니까 얼추 맞아서 기분이 좋았다. 눈에 익었어..! 당신 개발자라고
다크모드와 라이트모드를 지원하려고 했기 때문에, 이 부분을 어떻게 구현할까 조금 고민해본 결과, 다음과 같이 구조를 짜기로 했다.
실제로 필요한 컬러, 명암 토큰(팔레트)을 다 정의해놓은 다음, 테마에 따라서 달라질 목적 컬러를 따로 선언하는 것이었다. 그러면 컴포넌트에서는 목적 컬러만 쓰면 되는 것.
+) 찾아보니 내가 임의로 말한 목적 컬러를 Token Alias라고 하는 것 같다. 별칭!
예를 들어, $greenblue와 $skyblue라는 컬러 토큰이 있다고 해보자. 그리고 메인포인트 컬러(Token Alias)를 $main으로 선언해놓자. Button의 색은 $main을 가리키고 있다. Light 모드일 때 $main은 $greenblue로 설정하고, Dark 모드일 땐 $main을 $skyblue로 설정한다. 그러면~ Button 컴포넌트는 테마마다 다른 색을 가리킬 필요가 없이 그냥 $main만 가리키고 있으면 되고, 제공하는 Theme만 변경하면 되는 것이다.

상태에 따라 지정해줄 색을 정의하고, 포인트 색을 정의하고, 명암(grayscale)을 정의해주었다. 명암은 12단계로 하려다가 넘 많은 것 같아서 8단계로 줄였다. #FFF는 Light Mode의 배경을 위해서 선언해주었다.

이제 이건 테마에 따라서 달라질 컬러들을 정의해놓았다. 메인, 세컨, 배경, 본문, 깊이로 나누었다. 개인적으로 Depth에 대한 이름에 대해서 고민이 많았는데, 이게 뭐냐면 각 모드에 따라서 섹션을 구분하는 용도로 쓰고 싶었다.
라이트모드는 깊이가 깊어질수록 어두워지고, 반대로 다크모드는 깊이가 깊어질수록 밝아진다.
그런데 이걸 drakness/brightness로 쓰자니 라이트 모드일 때랑 다크 모드일 때의 의미가 달라지고, 그렇다고 grayscale로 쓰자니 겹치는 이름이라 쓸 수 없었다. 고민고민하다가 깊이라는 단어가 라이트에도, 다크에도 어울리는 말이라 채용했다.
이제 이 컬러들을 실제로 Stitches에 정의해보자. 먼저 Stitches를 설치한다.
yarn add @stitches/react
그 다음 stitches.config.ts 파일을 생성한 후, 다음과 같이 작성한다.
// stitches.config.ts
import { createStitches } from '@stitches/react';
export const { styled, css } = createStitches({
theme: {
colors: {},
space: {},
fontSizes: {},
fonts: {},
fontWeights: {},
lineHeights: {},
letterSpacings: {},
sizes: {},
borderWidths: {},
borderStyles: {},
radii: {},
shadows: {},
zIndices: {},
transitions: {},
},
});
여기에서 정의된 14개의 토큰들이 Stitches에서 제공하는 토큰들이다. 다 쓰진 않을 것 같지만,, 일단 기본으로 정의해주었다. 먼저 colors를 정의해주자.
colors: {
greenblue: '#135872',
skyblue: '#4BB7E2',
red: '#E91545',
green: '#4CAF50',
gray1: '#121212',
gray2: '#292929',
gray3: '#323232',
gray4: '#797979',
gray5: '#929292',
gray6: '#D9D9D9',
gray7: '#F2F2F2',
gray8: '#F9F9F9',
gray9: '#FFFFFF',
},
위에서 정의한 대로 하나하나 써주었다. Token Alias은 테마별로 달라지기 때문에, 나중에 테마를 만들 때 정의해주어야 한다. 여기에서는 기본이 되는 것만 정의하였다.
space: {
1: '.25rem', // 4
2: '.5rem', // 8
3: '.75rem', // 12
4: '1.5rem', // 24
5: '3rem', // 48
small: '$1',
smaller: '$2',
default: '$3',
double: '$4',
quard: '$5',
},
먼저 서비스에서 쓰이는 Space를 정의해주었다. 순서대로 1, 2, 3, 4, 5로 정의를 했고, 숫자가 높아질수록 간격이 커진다. 이 간격 크기는 Token이고, 이제 실제로 쓰일 Token Alias를 정의해주었다. 굳이 이렇게 한 이유는~ System-ui에서 이렇게 하길 권장하더라.
글꼴 크기와 같이 일반적으로 배열에 저장되는 서수 값의 경우, 객체에 명명된 속성을 추가하여 별칭을 만드는 것이 도움이 될 수 있습니다.
For typically ordinal values like font sizes that are stored in arrays, it can be helpful to create aliases by adding named properties to the object.
// example fontSizes aliases fontSizes: [ 12, 14, 16, 20, 24, 32 ] // aliases fontSizes.body = fontSizes[2] fontSizes.display = fontSizes[5]
나는 기본적으로 12px 단위로 작업을 하고, 세세하겐 4px 단위로 작업을 하기 때문에, 기본값을 12px로 두고 나한테 맞게끔 명명했다.
완전 다른 이야기라 따로 포스팅을 할까 싶다. 일단 적어보기.
나는 vw를 선호하는 편인데, 그 이유는 어떤 화면에서든지 동일한 배치와 구조를 가지고 싶기 때문. 그러려면 브라우저의 크기에 따라 유동적으로 변해야 하는데 그것을 해주는 게 vw, vh이다.
다만,, 브라우저의 크기가 일정 이상 커지게 되면 안 예쁘게 변한다. 360px 기준 16px로 작업하면 4.44vw가 나오는데, 만약 이걸 그대로 1440px까지 키우게 되면 걍 미친 수준으로 크기가 변하게 된다.
그래서 나는 이걸 피하기 위해 각 BP마다 1vw를 다시 재설정해준다. 360, 640, 756, 1024, 1440마다 재설정을 해주는데,, 이게 또 여간 귀찮은 작업이 아닐 수 없다. 언제 일일이 다 계산하고 정의해주니? 그래도 했다. 징한 놈.
게다가 피그마에서 작업은 px단위로 하게 되니까, 이걸 반응형 단위로 옮기는 수고가 꽤나 많다. 이 짓을 계속 반복할 순 없겠다 싶어서 이것저것 찾아보다가 아래의 방법이 제일 괜찮은 것 같았다.
기본적으로 rem 단위를 쓰되, root의 font-size는 vw
객체들의 수치는 rem 단위를 쓰고, 그 기준이 되는 root의 폰트 사이즈를 vw로 접근한다는 개념이다. 이렇게 해도 걍 똑같은 거 아님? 이라고 의심이 들 순 있는데 이렇게 함으로써 얻게되는 편리성이 다음과 같다: 스타일링 단위를 bp마다 안 만들어줘도 된다.
다른 말로는 이제 컴포넌트 디자인은 하나의 단위만 써줘도 된다는 것. 지금까지는 bp마다 vw를 계산해줘 써줬는데, 기준이 되는 놈만 바꾸면 되니 아주 편해졌다. 우하하.
root의 폰트 사이즈는 Viewport / 16px(기준)을 해주면 되므로 다음과 같이 정해주면 된다.
'@initial': { // 360
':root': {
fontSize: 'calc(100vw / 22.5)',
},
},
'@md': { // 768
':root': {
fontSize: 'calc(100vw / 48)',
},
},
'@lg': { // 1024
':root': {
fontSize: 'calc(100vw / 64)',
},
},
'@xl': { // 1440
':root': {
fontSize: 'calc(100vw / 90)',
},
},
물론 이것도 root의 font-size를 bp마다 바꿔줘야 하긴 하지만~ 이것만 해두면 된다!
아참, 참고로 이 root는 globalCss로 넣어줘야 한다.
폰트나 일반 사이즈나 난 똑같이 가져갈거라 같은 값을 넣었다.
fontSizes: {
1: '.5625rem', // 9
2: '.75rem', // 12
3: '.875rem', // 14
4: '1rem', // 16
5: '1.125rem', // 18
6: '1.5rem', // 24
7: '1.75rem', // 28
8: '2rem', // 32
9: '2.625rem', // 42
10: '3rem', // 48
11: '4rem', // 64
12: '8rem', // 128
caption: '$2',
default: '$3',
"button-sm": '$3',
"button-lg": '$4',
"button-text": '$5',
header: '$5',
"tutorial-title": '$7',
"tutorial-content": '$8',
"author": '$5',
"time": '$3',
"content": '$4',
},
sizes: {
1: '.5625rem', // 9
2: '.75rem', // 12
3: '.875rem', // 14
4: '1rem', // 16
5: '1.125rem', // 18
6: '1.5rem', // 24
7: '1.75rem', // 28
8: '2rem', // 32
9: '2.625rem', // 42
10: '3rem', // 48
11: '4rem', // 64
12: '8rem', // 128
nav: '$1',
icon: '$8',
'icon-sm': '$6',
logo: '$12',
'logo-sm': '$8',
'profile-sm': '$5',
'profile-md': '$9',
'profile-lg': '$12',
},
다만 이제 Token Alias가 다른~
일단 눈에 보이는 크기들만 정의해주었다. 여기는 뭐 나중에 계속 추가가 될 거니까! 이정도로 하고 필요할 때마다 정의해주면 될 것 같다.
borderWidths: {
default: '.0625rem', // 1px
},
borderStyles: {},
radii: {
small: '.25rem', // 4px
large: '.75rem', // 12px
},
여기는 값이 몇개 없어서 바로 Alias를 해주었다.
media: {
sm: '(min-width: 640px)',
md: '(min-width: 768px)',
lg: '(min-width: 1024px)',
xl: '(min-width: 1440px)',
},
breakpoint 정의! 4개로 나누었다.
그러면 이제 위에서 정의한 Token들은 기본 구성 바탕이 된다. 여기에 따로 Theme을 만들어주면, 그 Theme에서 정의한 게 적용되는 것!
// stitches.config.ts
import { createTheme } from '@stitches/react';
export const darkTheme = createTheme({
colors: {
point: '$skyblue',
secondary: '$greenblue',
bg: '$gray2',
body: '$gray7',
depth1: '$gray3',
depth2: '$gray5',
depth3: '$gray6',
},
});
export const lightTheme = createTheme({
colors: {
point: '$greenblue',
secondary: '$skyblue',
bg: '$gray9',
body: '$gray1',
depth1: '$gray8',
depth2: '$gray6',
depth3: '$gray4',
},
});
마찬가지로 같은 파일 안이다. 각 모드에 따라 alias에 들어갈 값을 정의해준다. 이런 다음 Root Layout이나 Entry Point에서 이런 식으로 바꿔준다.
// App.tsx
import { darkTheme, lightTheme } from './stitches.config';
const App = () => {
const theme: string = 'light';
return (
<div className={theme === 'light' ? lightTheme : darkTheme}>
</div>
)
}
styled component에서 themeprovider를 썼던 것처럼 theme을 제공해주는 div를 하나 감싸주면 된다. 물론 이제 어떤 theme을 쓰는지 저장해두는 변수는 local을 쓰든~ DB에 저장하든~ 식으로 해야겠지!
전역으로 CSS를 지정해주려면? Stitches에서 제공하는 globalCss() 함수를 쓰면 된다.
아까 stitches.config.ts에서 createStitches를 이용해서 지정한 토큰을 가진 함수 및 오브젝트들이 반환된다.
export const { styled, css, globalCss, keyframes, theme, config } = createStitches(
theme: { ... }
)
그러면 이것을 Entry Point File에서 import한 다음에 다음과 같이 선언해준다.
const globalStyles = globalCss({
/** global styles... */
'*': {
margin: 0,
padding: 0,
boxSizing: 'border-box',
},
});
선언해주었으면, 호출해주면 끄읕!
const App = () => {
globalStyles();
/** ... */
}
여기까지 진행했으면 기본적인 Stitches 설정을 끝난 것! 이제 만들어둔 피그마 디자인을 바탕으로 하나하나 Component를 만들어 가면 된다....😇