이번 프로젝트에서는 UI의 일관성과 유지보수성을 높이기 위해 디자이너가 정의한 디자인 시스템을 코드로 적용했다.
특히 색상 팔레트와 타이포그래피를 체계적으로 관리하고, @emotion/react와 TypeScript 환경에서 재사용할 수 있도록 구현했다.
작은 프로젝트에서는 보통 임의로 hex 코드를 사용하거나 CSS에 직접 스타일을 정의하는 경우가 많다.
하지만 프로젝트 규모가 커지거나 협업 인원이 늘어나면 스타일이 제각각 적용되어 유지보수 비용이 크게 증가한다.
그래서 이번에는 디자인 시스템을 코드 레벨에서 토큰화하여 적용하는 방식을 선택했다.
이번 프로젝트에서 디자인 시스템을 적용한 이유는 크게 세 가지다.
특히 협업 과정에서는 디자이너가 색상이나 폰트 스타일을 변경하더라도 토큰만 수정하면 전체 UI에 일괄 적용되기 때문에 유지보수 효율이 크게 높아진다.
디자인 시스템 관련 코드는 src/theme 폴더에 모아 관리했다.
src/
├── components/
│ └── shared/ # 공통 UI 컴포넌트 (예: Text)
│
├── theme/
│ ├── index.ts # Theme 정의
│ ├── palette.ts # 색상 팔레트
│ └── typography.ts # 타이포그래피 설정
│
├── types/
│ └── emotion.d.ts # Theme 타입 확장
│
└── main.tsx # ThemeProvider 적용
이렇게 구조를 잡아두면 프로젝트가 커져도 디자인 토큰을 한 곳에서 관리할 수 있어 유지보수가 편리하다.
색상 팔레트는 gray, primary, secondary, exclusive 네 그룹으로 나누어 코드에 적용했다.
// 샘플 색상 코드
export const palette = {
gray: {
0: '#FFFFFF',
100: '#E0E0E0',
900: '#000000',
},
primary: {
500: '#FFAA99',
900: '#FF5500',
},
secondary: {
500: '#99CCFF',
900: '#0066CC',
},
exclusive: {
e1: '#FF3300',
e7: '#33CC66',
},
} as const;
이렇게 정의해두면 컴포넌트에서는 임의의 hex 코드 대신 theme을 통해 다음과 같이 접근할 수 있다.
color: ${({ theme }) => theme.color.primary[500]};
장점은 두 가지다.
theme.color.primary[500]처럼 의도가 명확하게 드러나 협업이 수월하다.디자이너가 정의한 글꼴 규칙(크기, 두께, 줄 간격, 자간 등)을 객체 형태로 코드에 옮겨 적용했다.
export type TypoSpec = {
size: number;
weight: 400 | 500 | 600;
lh: number | string;
ls: number | string;
};
export const typography = {
heading: {
title1: { size: 32, weight: 600, lh: '150%', ls: '-2%' },
title2: { size: 28, weight: 600, lh: '150%', ls: '-2%' },
},
body: {
body5: { size: 16, weight: 600, lh: '150%', ls: '-2%' },
body10: { size: 10, weight: 400, lh: '150%', ls: '-2%' },
},
};
또한 applyTypography 유틸을 두어 정의된 스펙을 곧바로 CSS 속성으로 변환할 수 있도록 했다.
export const applyTypography = (s: TypoSpec) => ({
fontSize: `${s.size}px`,
fontWeight: s.weight,
lineHeight: typeof s.lh === 'number' ? `${s.lh}px` : s.lh,
letterSpacing: typeof s.ls === 'number' ? `${s.ls}px` : s.ls,
margin: 0,
});
이 구조 덕분에 컴포넌트에서는 직접 폰트 크기나 간격을 지정할 필요 없이 토큰을 불러와 적용하기만 하면 된다.
메인 엔트리에서 ThemeProvider를 사용해 전역적으로 theme을 주입했다.
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</StrictMode>
);
이제 어떤 컴포넌트에서도 theme.color나 theme.typography 같은 토큰을 직접 불러와 사용할 수 있다.
타이포그래피 시스템을 기반으로 Text 계열 컴포넌트를 만들었다.
// src/components/shared/Text.ts
import styled from '@emotion/styled';
import { applyTypography } from '../../theme/typography';
export const Title1 = styled.h1(({ theme }) =>
applyTypography(theme.typography.heading.title1)
);
export const Body7 = styled.p(({ theme }) =>
applyTypography(theme.typography.body.body7)
);
실제 사용 예시는 아래와 같다.
export const CategoryText = styled(Body7)(({ theme }) => ({
color: theme.color.gray[900],
}));
export const CategoryPreviewSubTitle = styled(Body10)`
color: ${({ theme }) => theme.color.gray[300]};
`;
이제 임의로 글꼴 크기나 색상을 지정할 필요 없이, theme에 정의된 토큰만 사용하면 된다.
디자인 시스템을 코드로 정리해두니 협업 효율이 눈에 띄게 향상되었다.
이번 적용기를 통해 디자인 시스템을 초기에 정의해두는 것이 장기적으로 훨씬 큰 이득이라는 교훈을 얻었다.