디자인 시스템 문서화를 위한 Playground 구현하기 🎨


Playground를 만들게 된 이유 🤔

프론트엔드 개발에서 디자인 시스템은 매우 중요한 역할을 합니다. 일관된 UI를 구축함으로써 사용자 경험을 향상시키고, 개발자와 디자이너의 리소스를 절약하여 효율적인 업무 환경을 조성합니다.

디자인 시스템은 단순히 컴포넌트 모음이 아닌, 팀 전체가 따라야 할 규칙과 원칙을 담고 있습니다. 하지만 아무리 훌륭한 디자인 시스템도 제대로 문서화되지 않으면 그 가치가 크게 떨어집니다. 문서화가 부실하면 팀원들은 규칙을 자주 어기게 되고, 시간이 지날수록 시스템의 일관성이 희미해져 결국 사라지게 됩니다. 코드상으로 디자인 시스템의 규칙을 강제할수도 있지만, 개발자 뿐 아니라 디자이너에게도 제공할 문서가 필요합니다. 따라서 디자인 시스템을 명확히 문서화하고 팀원들이 쉽게 접근할 수 있게 만드는 것이 또 하나의 중요한 과제입니다.

스토리북(Storybook)과 같은 대표적인 문서화 도구들이 있지만, 이를 사용하지 않은 이유는 후술하는 것으로 하겠습니다. 우선 이번 디자인 시스템 도입과 함께 구현한 문서화 도구, Playground에 대해 소개하겠습니다.

Playground 구현 방식 ⚡️

개발 환경에서만 접근 가능하도록 설정

Playground는 개발 환경에서만 필요하므로, 프로덕션 환경에서는 접근할 수 없도록 설정했습니다. 이를 위해 Next.js의 layout 컴포넌트에서 설정해주었습니다.

import { notFound } from 'next/navigation';
export default function PlaygroundLayout({ children }: { children: React.ReactNode }) {
if (process.env.TARGET_ENV === 'production') {
return notFound();
}
return <>{children}</>;
}

이 방식을 통해 프로덕션 환경에서는 404 페이지가 표시됩니다. 하지만 이것만으로는 Playground 코드가 프로덕션 빌드에 포함되는 것을 막을 수 없습니다. 따라서 GitHub Actions 워크플로우에 다음과 같은 스크립트를 추가하여 프로덕션 빌드 시 Playground 폴더 자체를 완전히 제거하도록 했습니다.

- name: Remove playground in production
run: rm -rf src/app/\(unAuth\)/playground

이렇게 함으로써 Playground 관련 코드가 최종 프로덕션 번들에 포함되지 않아 빌드 크기를 줄이고, 불필요한 코드가 배포되는 것을 방지할 수 있습니다. 개발 환경에서는 여전히 Playground에 접근할 수 있어 디자인 시스템을 효과적으로 테스트하고 문서화할 수 있습니다.

색상 시스템 (Colors) 구현 🌈

디자인 시스템의 핵심 요소인 색상 팔레트를 시각화하기 위해 PlaygroundColors 컴포넌트를 구현했습니다. 이 컴포넌트는 SCSS에 정의된 색상 변수를 CSS 커스텀 속성으로 변환하고, 이를 JavaScript에서 읽어와 시각적으로 표시합니다.

SCSS에서 색상 정의 및 CSS 변수 생성

$fds-colors: (
static: (
black: #000000,
white: #ffffff,
),
neutral: (
30: #f5f5f6,
// ... 다른 색상들
),
// ... 다른 색상 그룹들
);
// CSS 변수로 변환하는 mixin
@mixin generate-fds-color-palette {
@each $group, $colors in $fds-colors {
@if type-of($colors) == 'map' {
@each $shade, $value in $colors {
--fds-color-#{$group}-#{$shade}: #{$value};
}
} @else {
--fds-color-#{$group}: #{$colors};
}
}
// 시맨틱 색상도 변환
@each $group, $colors in $fds-semantic-colors {
@each $name, $value in $colors {
--fds-semantic-#{$group}-#{$name}: #{$value};
}
}
}

이 mixin은 전역 스타일에서 호출되어 모든 색상을 CSS 변수로 등록합니다. 이렇게 하면 JavaScript에서 getComputedStyle을 통해 색상 값을 읽을 수 있습니다.

컴포넌트에서 색상 정보 가져오기

export default function PlaygroundColors() {
const [colorSection, setColorSection] = useState<ColorSection>({
basicPalette: {},
semanticColors: {},
});
useEffect(() => {
const root = document.documentElement;
const computedStyle = getComputedStyle(root);
const styleSheets = document.styleSheets;
const fdsVariables = [];
// CSS 변수 추출 로직
for (const sheet of styleSheets) {
try {
const rules = sheet.cssRules;
for (const rule of rules) {
if (rule instanceof CSSStyleRule && rule.selectorText === ':root') {
const styles = Array.from(rule.style);
for (const prop of styles) {
if (prop.startsWith('--fds-')) {
fdsVariables.push(prop);
}
}
}
}
} catch (error) {
console.error(error)
}
}
// 색상 정보 처리 및 상태 업데이트
// ... 코드 생략
}, []);
// 렌더링 로직
// ... 코드 생략
}

이 방식의 장점은 SCSS에서 색상을 정의하고 관리하면서도, JavaScript에서 해당 정보를 동적으로 가져와 시각화할 수 있다는 점입니다. 색상이 추가되거나 변경되어도 Playground는 자동으로 업데이트됩니다.

Playground에서 보여지는 모습
Playground에서 보여지는 모습

아이콘 시스템 (Icons) 구현 🔍

아이콘은 중앙 집중식으로 관리하고, Playground에서 모든 아이콘을 다양한 크기와 색상으로 시각화했습니다.

export default function PlaygroundIcons() {
return (
<div className={styles.container}>
<div className={styles.grid}>
{Object.entries(ICONS).map(([name, icon]) => (
<div key={name} className={styles.iconWrapper}>
<p className={styles.iconName}>{name}</p>
<div className={styles.variationsGrid}>
{Object.values(ICON_COLOR).map((color) => (
<div key={color} className={styles.colorSection}>
<p className={styles.colorLabel}>{color}</p>
<div className={styles.sizeVariations}>
{Object.values(ICON_SIZE).map((size) => (
<div key={size} className={styles.sizeWrapper}>
<FDSSvgIcon icon={icon} color={color} size={size} />
<span className={styles.sizeLabel}>{size}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
}

이 방식의 장점은 ICONS 객체에 새 아이콘을 추가하기만 하면 Playground에 자동으로 표시된다는 점입니다. 각 아이콘은 모든 가능한 색상과 크기 조합으로 표시되어 디자이너와 개발자가 쉽게 참조할 수 있습니다. ICONS 객체에는 SVGR을 통해 리액트 컴포넌트화된 SVG 파일들이 등록되어 있습니다.

Playground에 보여지는 모습
Playground에 보여지는 모습

컴포넌트 시스템 구현 🧩

이번에 구현한 Playground의 가장 강력한 기능은 모든 디자인 시스템 컴포넌트를 동적으로 로드하고 표시하는 기능입니다. 이를 위해 CommonComponents에서 모든 컴포넌트를 가져와 동적으로 렌더링합니다.

export default function PlaygroundPage() {
const [components, setComponents] = useState<ComponentModule<keyof typeof CommonComponents>[]>([]);
const [selectedTab, setSelectedTab] = useState<string>('Colors');
// ... 상태 관리 코드 생략
useEffect(() => {
const componentList = Object.entries(CommonComponents).map(([name, component]) => ({
name: name as keyof typeof CommonComponents,
component: component,
}));
setComponents(componentList);
}, []);
return (
<div className={styles.container}>
{/* 사이드바 및 탭 UI */}
<div className={styles.mainContent}>
<h2 className={styles.mainTitle}>{selectedTab}</h2>
<div className={styles.componentWrapper}>
{/* 정적 탭 렌더링 */}
{selectedTab === STATIC_TABS.COLORS && <PlaygroundColors />}
{/* ... 다른 정적 탭 */}
{/* 동적 컴포넌트 렌더링 */}
{components.map(({ name, component: Component }) => {
if (name !== selectedTab) return null;
const propsArray = componentProps[name] || [];
return (
<div key={name} className={styles.componentContainer}>
{propsArray.map((props, index) => (
<div key={index} className={styles.variantContainer}>
<div className={styles.componentDemo}>
<Component {...props} />
</div>
<div className={styles.propsSection}>
<h3 className={styles.propsTitle}>Props:</h3>
<pre className={styles.propsCode}>
{props}
</pre>
</div>
</div>
))}
</div>
);
})}
</div>
</div>
</div>
);
}

이 접근 방식의 가장 큰 장점은 스토리북과 달리 각 컴포넌트에 대한 별도의 스토리 파일을 만들 필요가 없다는 것입니다. 새 컴포넌트를 CommonComponents에 추가하고 componentProps에 기본 props를 정의하기만 하면 Playground에 자동으로 표시됩니다.

새 컴포넌트를 추가하기 위해서는 먼저 디자인 시스템 컴포넌트를 index.ts 파일에서 export합니다.

export { default as FDSButton } from './FDSButton';
// ... 기존 컴포넌트들
export { default as FDSLabel } from './FDSLabel';
// 새 컴포넌트를 추가할 때는 여기에 한 줄만 추가하면 됩니다

그런 다음 DEFAULT_PROPS에 새 컴포넌트의 예제 props를 추가합니다.

export const DEFAULT_PROPS: ComponentProps = {
// ... 기존 컴포넌트 props
FDSLabel: [
{
children: (
<FDSTextWithIcon text="블루" icon={ICONS.HeartFill} size={TEXT_WITH_ICON_SIZE.XSMALL} color={ICON_COLOR.BLUE} />
),
color: 'BLUE',
},
{
children: (
<FDSTextWithIcon text="오렌지" icon={ICONS.Heart} size={TEXT_WITH_ICON_SIZE.XSMALL} color={ICON_COLOR.ORANGE} />
),
color: 'ORANGE',
},
// ... 다른 예제 props
],
// 새 컴포넌트를 추가할 때는 여기에 예제 props 배열을 추가하면 됩니다
} as const;

특히 주목할 점은 ComponentProps 타입이 index.ts에서 export된 컴포넌트 타입과 연결되어 있다는 것입니다. 이는 타입 안전성을 제공하는 중요한 장점입니다. 만약 index.ts에 새 컴포넌트를 추가했는데 DEFAULT_PROPS에 해당 컴포넌트의 props를 추가하지 않으면 TypeScript가 타입 에러를 발생시킵니다. 이러한 타입 체크 덕분에 생긴 이점은 아래와 같습니다.

  • 개발자가 새 컴포넌트를 추가할 때 반드시 예제 props도 함께 추가해야 한다는 것을 알게 됩니다.
  • 컴포넌트가 Playground에서 누락되는 실수를 방지할 수 있습니다.
  • 문서화가 항상 최신 상태로 유지됩니다.
  • 이렇게 수정하면 새 컴포넌트가 Playground에 자동으로 표시됩니다. 이는 스토리북에서 각 컴포넌트마다 별도의 스토리 파일을 작성해야 하는 것보다 훨씬 간단하고 유지보수가 쉽습니다. 또한 컴포넌트의 다양한 상태와 변형을 쉽게 보여줄 수 있어 디자인 시스템을 문서화하는 데 매우 효과적입니다.

    Playground에 보여지는 모습
    Playground에 보여지는 모습

    스토리북과의 비교 📊

    스토리북은 널리 사용되는 컴포넌트 문서화 도구이지만, 몇 가지 단점이 있습니다.

  • 환경 설정의 어려움: 프로젝트와 별도의 환경으로 실행되어 SCSS 등의 환경을 동일하게 맞추는 추가 작업 필요
  • 복잡한 파일 관리: 각 컴포넌트마다 .stories.tsx 파일을 별도로 생성하고 관리
  • 추가 설정 필요: 별도의 설정과 빌드 프로세스 구성 필요
  • 반면, 이번에 구현한 커스텀 Playground는 다음과 같은 장점이 있습니다.

  • 단일 환경: 실제 프로젝트 내에서 동작하므로 환경 차이가 없습니다.
  • 간소화된 관리: 컴포넌트를 중앙에서 관리하고, props 배열만 추가하면 됨
  • 자동화된 문서화: 컴포넌트가 추가되면 자동으로 Playground에 표시
  • 최적화된 배포: 프로덕션 빌드에는 포함되지 않아 번들 크기에 영향을 주지 않습니다.
  • 마치며 🎯

    Playground를 통한 디자인 시스템 문서화는 디자이너와 개발자 모두 쉽게 접근할 수 있는 참조 자료를 제공하고, 컴포넌트의 다양한 상태와 변형을 시각화함으로써 일관된 UI 구현을 촉진했습니다. 특히 색상, 아이콘, 타이포그래피와 같은 기본 요소부터 복잡한 컴포넌트까지 모든 디자인 시스템 요소를 한 곳에서 확인할 수 있어 개발 효율성이 크게 향상되었습니다. 앞으로도 Playground를 지속적으로 개선하여 더 나은 개발 경험을 제공할 계획입니다. 새로운 컴포넌트가 추가될 때마다 자동으로 문서화되는 시스템을 통해 디자인 시스템의 일관성과 접근성을 유지할 수 있을 것입니다.