- Development
팀에서 스타일링 방식 개선해보기
2025년 02월 07일스타일링 방식은 기술 발전과 디자인 철학의 변화에 따라 계속 진화해 왔다. 초기 웹의 인라인 스타일에서 CSS 표준화, BEM·OOCSS 같은 방법론, 그리고 CSS-in-JS까지의 흐름을 보면 개발자들은 더 효율적이고 유지보수하기 쉬운 스타일링 방식을 꾸준히 고민해 왔다는 걸 알 수 있다.
이런 흐름 속에서 등장한 다양한 스타일링 도구들은 분명 개발자에게 많은 편의성을 제공한다. 하지만 도구가 정답을 대신 내려주지는 않는다. 결국 각 팀과 프로젝트의 환경에 맞춰 지금 우리에게 맞는 방식을 찾아가고, 불편한 지점을 조금씩 고쳐 나가는 과정은 계속된다.
나 역시 프로젝트를 운영하면서 이런 고민을 이어왔고, 이번 글에서는 팀 단위에서 스타일링 방식을 정리하고 개선해 나간 과정과 그 결과를 기록으로 남겨보려 한다.
개발 환경
내가 속한 프론트엔드 팀은 TypeScript 기반의 React 프로젝트를 개발하고 있고, 스타일링 도구로 styled-components를 사용하고 있다.
styled-components는 컴포넌트 기반 스타일링을 제공하고,
동적 스타일 적용과 테마 기능을 통해 일관된 디자인 시스템을 유지할 수 있게 도와주는 라이브러리다.
styled-components 자체에 큰 불만이 있었던 건 아니다. 다만 프로젝트가 커지고, 스타일 코드가 쌓이면서 테마와 스타일 분기 방식이 점점 읽기 어려워지고 있다는 느낌이 들었다.
이번 개선의 대상은 크게 두 가지다.
- 테마 값을 정의하고 사용하는 방식
- props 기반으로 스타일을 분기하는 패턴
목표는 단순했다.
지금보다 읽기 쉽고, 수정하기 쉬운 구조로 정리해보자는 것이었다.
테마 설정하기
styled-components에서는 theme 객체를 ThemeProvider에 전달하고, Context API를 통해 모든 하위 컴포넌트에서 테마를 사용할 수 있다.
import theme from '@/styles/theme.ts'const StyledComponentsProvider = ({ children }: PropsWithChildren) => {return <ThemeProvider theme={theme}>{children}</ThemeProvider>;};
문제는 theme 객체가 커질수록 구조가 점점 복잡해진다는 점이었다.
특히 색상이나 타이포그래피처럼 규칙적으로 반복되는 값들은 작성하는 쪽에서도, 읽는 쪽에서도 부담이 됐다.
그래서 테마 값을 정의하는 방식부터 정리해보기로 했다.
getFlattenThemeVariables
type ThemeObject = Record<string, Record<number, string>>;const getFlattenThemeVariables = <T extends Record<string, ThemeObject>>(variables: T,) => {// ...};
이 유틸은 중첩된 테마 객체를 1차원 구조로 변환하는 역할을 한다.
{ red: { 100: '#fff' } } 같은 입력을 받아 red_100 형태의 키로 평탄화한다.
덕분에 테마 정의는 색상 단위로 구조화하면서도, 실제 사용 시에는 단순한 Key-Value 형태로 접근할 수 있었다.
반복되는 접두어를 줄이고, 테마 추가 시 실수할 여지도 함께 줄일 수 있었다.
getTypographyWithWeight
const weight = {light: 300,medium: 500,bold: 700,} as const;export const getTypographyWithWeight = <T extends Record<string, string>>(typographies: T,) => {// ...};
이 유틸은 기존 텍스트 스타일에 font-weight를 조합해 새로운 스타일을 만들어준다.
하나의 텍스트 스타일을 기준으로 light, medium, bold 버전을 자동으로 생성하는 구조다.
텍스트 스타일이 늘어나더라도 weight 정의는 한 곳에서만 관리할 수 있어,
디자인 토큰이 증가해도 구조가 복잡해지지 않는다는 장점이 있었다.
스타일링 적용하기
테마를 정리한 뒤에는, 실제 컴포넌트에서 스타일을 분기하는 방식도 함께 손봤다.
특히 props 값에 따라 스타일이 갈리는 컴포넌트들은 시간이 지날수록 조건문이 늘어나
어떤 분기가 있는지 한눈에 파악하기 어려워지는 문제가 있었다.
createVariant
type VariantProperty = string | number | boolean | undefined;
createVariant는 props 값과 스타일 분기를 명시적으로 연결해주는 유틸이다.
분기 조건을 if 문 대신 객체로 선언하고, 실제 props 값을 받아 해당 스타일을 반환한다.
이 방식 덕분에 컴포넌트가 어떤 분기를 가지는지 구조적으로 드러나고,
새로운 분기를 추가할 때도 기존 코드를 해치지 않고 확장할 수 있었다.
TransientProps
styled-components를 쓰다 보면, 스타일 분기를 위한 props가 DOM으로 전달돼 경고가 발생하는 경우가 있다.
이를 해결하기 위해 $ 접두어를 사용하는 Transient Props가 제공된다.
Transient Props 간략 설명
$가 붙은 prop은 DOM으로 전달되지 않고, 스타일 분기 용도로만 사용된다.
다만 실제로 쓰다 보니, 스타일 전용 props를 타입으로 매번 정의하는 과정이 번거로웠다.
그래서 특정 props에만 $ 접두어를 자동으로 붙여주는 제네릭 타입을 추가했다.
export type TransientProps<T, K extends keyof T> = {[P in keyof T as P extends K ? `$${string & P}` : P]: T[P];};
이 타입을 사용하면 외부 컴포넌트 API와 내부 스타일링용 props를 명확하게 분리할 수 있고,
DOM 경고도 자연스럽게 피할 수 있었다.
후기
프로젝트를 진행하면서 이런 공용 유틸이나 패턴을 정리하는 작업은 항상 고민이 된다. 당장 눈에 보이는 기능을 만드는 일은 아니기 때문이다.
그래도 시간이 지나고 코드를 다시 봤을 때
“왜 이렇게 되어 있는지”가 바로 이해되는 구조를 만들어두는 건 충분히 가치 있는 일이라고 생각한다.
지금의 선택이 언제나 정답일 수는 없고, 언젠가는 다시 바꿔야 할 수도 있다.
그래도 시도하지 않으면 변화는 없다. 이번 작업도 그런 시도의 하나였다고 생각한다.