
CSS-in-JS는 CSS-in-CSS보다 더 강력한 추상화 수준을 제공한다. CSS-in-JS를 통해 자바스크립트 사용하여 스타일을 선언적이고 유지보수할 수 있는 방식으로 표현할 수 있다.
인라인스타일: HTML 요소 내부에 직접 스타일을 적용하는 방식, HTML 태그의
style속성을 사용하여 인라인 스타일을 적용할 수 있다.
인라인 스타일의 예시와 DOM 노드 연결 방식이다.
const textStyles = {
color: 'white',
backgroundColor: 'black',
};
const SomeComponent = () => {
return <p style={textStyles}>inline style!</p>;
};
<p style="color: white; background-color: black;">inline style!</p>
인라인은 DOM노드에 속성으로 스타일을 추가했다.
CSS-in-JS 예시와 DOM 노드 연결 방식이다.
import styled from 'styled-components';
const Text = styled.div`
color: white;
background: black;
`;
// 다음처럼 사용
const Example = () => <Text>Hello CSS-in-JS</Text>;
<style>
.hash136s21 {
background-color: black;
color: white;
}
</style>
<p class=”hash136s21”>Hello CSS-in-JS</p>
CSS-in-JS는 DOM 상단에 <style>태그를 추가했다.
CSS-in-JS를 사용하면 실제로 CSS가 생성되기 때문에 미디어 쿼리, 슈도 선택자 등과 같은 CSS기능을 쉽게 사용할 수 있다. styled-components 같은 라이브러리는 SASS의 기능까지 지원한다.
CSS-in-JS의 장점은 다음과 같다.
리액트 컴포넌트의 스타일리을 위해 순수 CSS만 사용할 수 있지만 라이브러리를 적용할 수도 있다. 라이브러리는 크게 두 가지로 나뉜다.
CSS Preprocessor
sass/scss
less
stylus
CSS-in-JS
styled-components
emotion
2014년 메타의 엔지니어 크리스토퍼 쉬도는 규모가 크고 동적인 웹 애플리케이션을 유지보수하기 위해 해결해야 할 css의 문제점을 7가지로 분류하여 설명하면서 해결책으로 CSS-in-JS 개념을 제시했다.
크리스토퍼 쉬도가 언급한 CSS의 7가지 문제점은 다음과 같다.
Global Namespace (글로벌 네임스페이스): 모든 스타일이 전역 공간을 공유하므로 중복되지 않는 CSS 클래스 이름을 고민해야한다.
Dependencies (의존성): CSS의 의존성과 자바스크립트의 의존성이 달라서 사용하지 않는 스타일이 포함되거나 꼭 필요한 스타일이 누락되는 문제가 발생한다.
→ 현재는 번들러의 발전으로 거의 해결
Dead Code Elimination (불필요한 코드 제거): 기능 추가 · 수정 · 삭제 과정에서 불필요한 CSS를 삭제하기 어렵다.
Minification (최소화): 클래스 이름을 최소화하기 힘들다.
Sharing Constants (상수 공유): 자바스크립트와 상태 값을 공유할 수 없다.
→ 현재는 CSS Variable이 도입되어 CSS의 공식 기능으로 제공
Non-deterministic Resolution (비결정적 해결): CSS 로드 순서에 따라 스타일 우선순위가 달라진다.
Isolation (고립): CSS의 외부 수정을 관리하기 어렵다.
이때를 기점으로 CSS-in-JS를 구현하기 위한 움직임이 있었고 그 결과 styled-components, emotion과 같은 CSS-in-JS 라이브러리가 등장하게 되었다.
import styled from '@emotion/styled';
// 탬플릿 리터럴을 활용, boolean으로 primary의 타입 지정
export const Button = styled.button<{ primary: boolean }>`
background: transparent;
border: none;
cursor: pointer;
font-size: inherit;
padding: 0;
margin: 0;
color: ${({ primary }) => (primary ? 'red' : 'blue')};
`;
대부분의 CSS-in-JS 사용 방식은 유사한데 템플릿 리터럴을 활용하여 동적인 스타일을 정의한다. props 타입을 정의하고, 이 props를 활용해서 동적인 스타일링을 구현한다.
만약 variant props의 유형에 따라 다른 스타일을 적용하고 싶다면 css 함수를 사용하여 스타일을 정의하고 variant 값에 따라 맵 객체를 생성하여 사용할 수도 있다.
import { css, SerializedStyles } from '@emotion/react';
import styled from '@emotion/styled';
type ButtonRadius = 'xs' | 's' | 'm' | 'l';
export const buttonRadiusStyleMap: Record<ButtonRadius, SerializedStyles> = {
xs: css`
border-radius: ${radius.extra_small};
`,
s: css`
border-radius: ${radius.small};
`,
m: css`
border-radius: ${radius.medium};
`,
l: css`
border-radius: ${radius.large};
`,
};
export const Button = styled.button<{ radius: string }>`
${({ radius }) => css`
/* ...기타 스타일은 생략 */
${buttonRadiusStyleMap[radius]}
`}
`;
만약 RoundButton, SquareButton 등 여러 버튼 컴포넌트를 구현해야 한다면, 공통적인 버튼 스타일을 따로 정의한 다음에 각 컴포넌트 스타일에서 이를 확장하여 구현할 수 있다.
// CommonButton 이라는 공통적인 스타일의 버튼을 RoundButton, SquareButton으로 각각 확장
const RoundButton = styled(CommonButton)`
// 스타일링
`;
const SquareButton = styled(CommonButton)`
// 스타일링
`;
컴포넌트에서 사용하는 props 뿐만 아니라 styled-components로 전달되는 스타일 관련 props의 타입도 정의해줘야한다.
보통 styled-components에 넘겨주는 타입은 props에서 받은 타입과 동일하다. 이때 TS에서 제공하는 Pick, Omit 같은 유틸리티 타입을 활용할 수 있다.
유틸리티 타입을 사용하지 않을 경우에는 다음과 같은 불편함이 생길 수 있다.
여기에서 styled-components에 넘겨주는 타입은 props와 똑같은 타입이지만 StyledProps로 따로 정의해줘야 하는 점 때문에 코드 중복이 발생한다. 또한 props의 타입이 변경되면 StyledProps도 변경되어야 한다.
interface Props {
height?: string;
color?: keyof typeof colors;
isFull?: boolean;
className?: string;
// ...
}
export const Hr: VFC<Props> = ({ height, color, isFull, className }) => {
// ...
return (
<HrComponent
height={height}
color={color}
isFull={isFull}
className={className}
/>
);
};
interface StyledProps {
height?: string;
color?: keyof typeof colors;
isFull?: boolean;
}
const HrComponent = styled.hr<StyledProps>`
height: ${({ height }) => height || '10px'};
margin: 0;
background-color: ${({ color }) => colors[color || 'gray7']};
border: none;
${({ isFull }) =>
isFull &&
css`
margin: 0 -15px;
`}
`;
만약 컴포넌트가 커지고 여러 styled-components를 활용할 때는 중복되는 타입과 관리해야 하는 포인트가 늘어나게 된다. 이때 Pick, Omit 같은 유틸리티 타입을 활용할 수 있다.
const HrComponent = styled.hr<Pick<Props, 'height' | 'color' | 'isFull'>>`
// ...
`;
이 코드는 Pick을 사용해 Props에서 height, color, isFull 세가지 속성을 가져왔다.
styled-components 뿐만 아니라 상속받는 컴포넌트나 props 등의 경우에도 Pick이나 Omit 같은 유틸리티 타입을 활용하면 중복되는 타입을 피할 수 있어 유지보수적인 측면에서 긍정적인 효과를 얻을 수 있다.