Study
11장 CSS-in-JS
11.1 CSS-in-JS란
1) CSS-in-JS와 인라인 스타일의 차이
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 사이에 상수와 함수를 쉽게 공유할 수 있다.
2) 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 라이브러리가 등장하게 되었다.
3) 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)`
// 스타일링
`;
11.2 유틸리티 함수를 활용하여 styled-components의 중복 타입 선언 피하기
컴포넌트에서 사용하는 props 뿐만 아니라 styled-components로 전달되는 스타일 관련 props의 타입도 정의해줘야한다.
보통 styled-components에 넘겨주는 타입은 props에서 받은 타입과 동일하다. 이때 TS에서 제공하는 Pick
, Omit
같은 유틸리티 타입을 활용할 수 있다.
1) props 타임과 styled-components 타입의 중복 선언 및 문제점
유틸리티 타입을 사용하지 않을 경우에는 다음과 같은 불편함이 생길 수 있다.
여기에서 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
같은 유틸리티 타입을 활용하면 중복되는 타입을 피할 수 있어 유지보수적인 측면에서 긍정적인 효과를 얻을 수 있다.