
저는 디자인 시스템을 사용한 프로젝트를 5번 참여했는데요, 매번 타이포 시스템을 정의할 때마다 같은 과정을 반복했습니다.
피그마 dev 모드 → 복사 → 붙여넣기 → 또 복사 → 또 붙여넣기...
타이포그래피 컴포넌트가 20개라면, 이 과정을 20번 반복해야 했습니다.
결과물은 아래와 같은 구조였는데요.

(왼쪽) 피그마 코드를 그대로 객체로 정의 / (오른쪽) 컴포넌트 오버라이딩 한 코드
타이포 객체를 정의해두고, 실제 사용 컴포넌트에서는 color 같은 가변 요소만 styled(컴포넌트) 형식으로 오버라이딩해서 쓰는 방식이었습니다.
ctrl+c, ctrl+v.
행동 자체는 단순했지만, 유지보수가 문제였습니다.
시스템 변경이 생기면 20개의 객체를 전부 찾아서 고쳐야 했고, 코드는 점점 더 지저분해졌습니다.

그래서 직접 설계해봤습니다..!
이번 구현 방식은
@emotion/styled이론 지식 기반입니다!
공식 문서를 먼저 읽어 보신 뒤, 기록을 읽어주시면 더욱 이해가 잘 될거에요!
▶ 🔗 공식 문서 이동 링크
▶ 🔗 @emotion/styled 정리 글
먼저 '공통적으로 사용되는 요소들은 Token으로 정의하고, 이를 기반으로 베이스를 만들어서 활용하자!'로 시작했습니다.

피그마 dev 모드로 전환시 타이포그라피 요소는 위와 같은 형태로 코드가 출력됩니다.
참여한 서비스 내 타이포 시스템은 아래와 같은 특징을 가지고 있었습니다.
1. 불변 요소(빨간 부분) :
font-family,font-style
2. 가변 요소 :font-size,font-weight,font-height,color
다음으로 저는 2가지 문제 상황을 해결해보고자 했습니다.

퍼블리싱 작업 중에 간혹 디자인 시스템에 정의되지 않은 타이포 요소를 확인할 수 있었는데요!
그럴 경우 font-weight (초록색 부분)가 숫자값이 아닌 문자열로 표현이 되어 해당 시스템 요소의 weight 값이 헷갈리는 경우가 있었습니다.
이런 경우에 '리터럴로 작성하여도 내부적으로 자동 변환이 되면 편하겠다.' 라는 생각이 들었습니다.
피그마의 Variables 표현 방식처럼 ' color를 제외한 가변요소(노랑색, 하늘색)만 표현하고, 그 외의 요소는 숨겨보자!' 라는 목표로 삼고 디자인 시스템을 리팩토링했습니다.
이 두 가지 문제를 해결하기 위해 @emotion/styled 기반의 레이어 구조를 설계했습니다.
각 레이어가 다음 레이어의 재료가 되는 구조로, 아래 순서로 구현했어요.
원시값(Token) → 재사용 조각(Mixins) → 공통 기본값(Base) → 조합기(Builder) → 자동 생성기(Factory)
@emotion/styled패키지는 내부적으로@emotion/react를 래핑하고 있어서, 2개의 패키지를 함께 설치 후 사용해주셔야 합니다!
▶🔗 관련 정리 글
먼저 타이포 시스템 파일 상단에 import문을 작성했습니다.

styled 컴포넌트 자동 생성(7. Factory) 기능에 활용
css() : 객체로 스타일을 정의CSSObject : TypeScript 환경에서 Emotion의 자동완성 및 타입 추론" 폰트 상수 정의 "

시스템 전체에서 사용할 원시값인 Token을 정의했습니다.
$typoFont 객체는 아래 2가지 목적을 가지고 있습니다.
1. family 정의 :
font-family를 한 곳에서 관리합니다.
2. weight 값 리터럴 맵핑 : 피그마에서는font-weight를 숫자가 아닌 문자열(Regular, SemiBold 등)으로 표기하는데요, 이를 보고 바로 코드에 적용 가능하기 위해 리터럴 타입으로 매핑했습니다.
( 0-1 문제 해결 !! )
$lh 함수는 행간을 배수로 입력받아 '%문자열'로 변환해주는 유틸입니다.
$lh(1.4)→"140%"
2글자 타이핑이라도 줄여보자는 마음으로 작성했는데, 만족스럽습니다!
$prefix에 대해Sass$변수 선언 컨벤션에서 영감을 받아 붙여봤습니다.
문법적으로는 의미가 없지만, 코드를 보면 스타일 토큰이라는 것을 바로 인식할 수 있어서 꽤 만족스럽습니다!
as const를 사용해 리터럴 타입으로 좁혀주었기 때문에, 컴파일 단계에서 잘못된 키 접근을 바로 잡아줍니다!
" 재사용 스타일 조각 "
여러 곳에서 반복되는 스타일 묶음을 clampLines()함수로 추출하였습니다.
텍스트를 n줄까지만 보여주고, 초과하면 말줄임표(...)로 자르는 함수
display: -webkit-box; : 박스를 webkit flex box로 설정-webkit-box-orient: vertical; : 박스 방향을 세로로 설정-webkit-line-clamp: ${n}; : n줄까지만 표시overflow: hidden; : n줄 초과 텍스트 숨김text-overflow: ellipsis; : 잘린 부분을 ...으로 표시clampLines(2)를 호출하면 2줄 이상은 말줄임표 처리하는 CSS를 반환합니다.
특정 타이포 컴포넌트에 clamp 옵션이 있을 때 이 믹스인이 자동으로 주입됩니다!
( 5.Styled Builder 참고 )
clampLines는 현재 구현에서 사용한 예시이며,
프로젝트 필요에 따라 믹스인 레이어에 자유롭게 함수를 추가해 확장할 수 있습니다!
" 공통 기본 스타일 "

모든 타이포 컴포넌트에 공통으로 깔리는 베이스 스타일입니다.
CSSObject 타입으로 정의하여 Object Style의 자동완성과 타입 검사가 동작하게 했습니다!
( Object Style의 장점! )
BASE_TYPO는 상수임을 명시하기 위해 UPPER_SNAKE_CASE로 작성했습니다.
- UPPER_SNAKE_CASE : 수정하지 않을 것임을 명시하는 '네이밍 컨벤션'
2.Token에서 정의한 토큰$typoFont을 참조합니다.
폰트 렌더링을 부드럽게 처리하기 위해 아래 속성들을 추가했습니다.
아래 두 속성은 함께 써야 Chrome, Safari, Firefox 환경 모두에서 동일하게 부드러운 폰트 렌더링을 보장할 수 있어요.
WebkitFontSmoothing: 'antialiased'
MozOsxFontSmoothing: 'grayscale'
그외 추가적으로 설정하고자 하는 요소들을 붙여줍니다!!
" 스타일 배열 조합기 "

TypoConfig 객체를 받아 스타일 배열을 반환하는 함수입니다.
size(string), weight(number), lh(string / line-height)clamp(number / line-clamp), extra(CSSObject)@emotion/styled는 스타일 인자로 배열을 받을 수 있습니다.
배열 내부를 순서대로 순회하며 병합해주고, null은 자동으로 무시해요.
객체 방식이라면 조건부 스타일을 추가할 때마다 spread 연산자(...)가 필요하지만,
배열 방식은 조건부 결과를 요소로 넣기만 하면 Emotion이 알아서 처리해줍니다!!!
( 최고다...!!!! )
덕분에 BASE_TYPO, 개별 값, clamp, extra 같은 스타일 레이어를 독립적으로 깔끔하게 조합할 수 있었습니다.
styleOf 함수 흐름을 정리하자면, 다음과 같습니다.
styleOf함수가TypoConfig타입의 객체를 인자로 받아 구조 분해BASE_TYPO(공통 기본값)를 배열 첫 번째 요소로 추가fontSize/fontWeight/lineHeight개별 값을 객체로 묶어 추가clamp가 있으면clampLines(clamp), 없으면null— 삼항 연산자로 조건부 추가extra가 있으면 그대로, 없으면null—??연산자로 조건부 추가
" 타이포 스펙 명세 "

타입별 스펙을 한곳에 모아 정의하는 명세서입니다!
$typoFont.weight.sb 참조숫자 대신 토큰을 참조해서 의미를 명확하게 했습니다.
피그마에서 "SemiBold"를 확인하고 바로 $typoFont.weight.sb로
작성할 수 있게 되어 0-1 문제가 해결됐습니다!
$lh 유틸, size )intro에서 세운 목표를 다시 떠올려볼게요.
" color를 제외한 가변요소만 표현하고, 불변요소는 숨기자 "
Config Map을 보면 size, weight, lh —
타입별로 달라지는 가변요소만 명세되어 있어요.
font-family, font-style 같은 불변요소는 Config Map 어디에도 없습니다.
불변요소는 4. Base의 BASE_TYPO에서 처리했기 때문에
Config Map에서는 신경 쓸 필요가 없거든요!
color 역시 Config Map에 없어요.
사용하는 쪽에서 오버라이딩으로 처리하기 때문이에요.
결과적으로 Config Map은
딱 "바뀌는 것만" 보이는 명세서가 됐습니다. 0-2 문제 해결!!!
satisfies Record<string, TypoConfig>각 항목이 5-1. TypoConfig 타입을 만족하는지 컴파일 단계에서 검사하며, 각 키의 정확한 타입 추론을 유지할 수 있습니다.
Caption, Button 타입처럼 필요한 개별 속성을 추가할 수 있습니다.
이미지에는
flexShrink:0추가함!
" styled 컴포넌트 자동 생성 "

Config Map을 순회하며 styled.span 컴포넌트를 자동 생성하는 함수입니다.
핵심은 styled.span(styleOf(map[key])) 한 줄이에요.
Config Map의 각 키styleOf( )로 스타일 배열 생성styled.span( )에 주입styled컴포넌트 반환
제네릭 T를 활용하여 반환 타입이 Record<keyof T, StyledSpan>으로 추론됩니다.
즉, T에 없는 키로 접근하면, TypeScript가 바로 에러를 반환해요!
type StyledSpan = ReturnType<typeof styled.span>;
styled.span이 반환하는 타입을 추출해서 별도 타입으로 정의한 것입니다.
ReturnType<typeof styled.span>을 직접 쓰면 너무 길고 가독성이 떨어지기 때문에, StyledSpan이라는 이름으로 추출해서 재사용할 수 있게 했습니다.
📝 용어 정리
ReturnType : TypeScript 유틸리티 타입으로, 함수 T의 반환 타입을 추출해줍니다.
ReturnType<typeof styled.span>→styled.span()이 반환하는 타입을 그대로 가져옴
function createTypo<T extends Record<string, TypoConfig>>(
map: T
): Record<keyof T, StyledSpan>
Config Map을 받아 styled.span 컴포넌트를 자동으로 생성해주는 팩토리 함수입니다.
제네릭 T를 활용한 덕분에 두 가지 이점이 생겨요.
1. 타입 안전성
T는 Record<string, TypoConfig>를 확장해야 하므로, TypoConfig 형태에 맞지 않는 값을 넘기면 컴파일 단계에서 바로 에러가 발생합니다.
2. 반환 타입 자동 추론
반환 타입이 Record<keyof T, StyledSpan>으로 추론되기 때문에, T에 없는 키로 접근하면 TypeScript가 바로 잡아줘요.
const T = createTypo(CONFIGS);
T.Headline1 // ✅ 정상
T.존재하지않는키 // ❌ TypeScript 에러
6. Config Map에서 정의한 CONFIGS를 인자로 넘기면,
각 키에 해당하는 styled.span 컴포넌트가 자동으로 생성됩니다.
" 외부 노출 "

마지막으로 createTypo(CONFIGS)를 실행해 전체 컴포넌트를 생성하고,
Named Export로 하나씩 내보냅니다.
import { Headline1, Body2 } from '@/styles/typo';
Named Export를 사용하면 사용하는 쪽에서 필요한 컴포넌트만 골라서 import할 수 있어요.

야호!! 사용 방식은 그대로 유지하되, 내부 로직만 변경 성공이에요!!
마치 젠가 가운데 블럭을 뺀 것 같이..내부 로직만 수정 변경한게 너무 짜릿하지 않나요!~!~!
프로젝트 보안상 실제 적용 화면 첨부가 어렵지만,
아래는 실제 사용 예시 코드입니다!

(왼쪽) 타이포 컴포넌트를 오버라이딩해서 사용 / (오른쪽) 실제 사용 예시
기존에 타이포 시스템을 사용하던 방식(export/import, 오버라이딩)은 그대로 유지하되,
타이포 시스템 구축 자체는 훨씬 효율적인 구조로 개선되었습니다!
이제 시스템 변경이 필요하면, 6. Config Map와 8. export 문만 추가/수정하면 됩니다.
실제 커밋 기록을 보면, 1개 파일에서 304줄이 제거되고 213줄로 재작성되었습니다.

약 100줄 이상의 코드를 줄이고, 재사용 가능한 구조를 만들 수 있었어요.
( 코드양 약 33% 감소!! )
각 레이어별 역할이 분리되어 있어서, 폰트 자체를 변경하더라도
Token 한 줄만 고치면 전체에 반영됩니다!!
언젠간 지금보다 더 나은 방식으로 발전시킬 수 있겠죠?
야호~ 이렇게 기분좋게 기록 마무리합니다~ ^0^~

🔗 참고
Emotion 공식 문서 : https://emotion.sh/docs/styled