CSS-in-JS는 모던 프론트엔드 개발, 특히 React 커뮤니티에서 점점 더 일반적으로 사용되고 있습니다. styled-components는 Tagged templates의해 구동되어 스타일만 정의하면 일반적인 react 컴포넌트를 생성할 수 있기 때문에 다른 CSS-in-JS 목록에서 눈에 띕니다. 이는 CSS 모듈성과 같은 중요한 문제를 해결하고, 중첩과 같은 non-CSS 기능을 제공하며, 이 모든 기능은 별도의 설정 없이 사용할 수 있습니다. 개발자는 CSS 클래스의 고유 이름에 대해 생각할 필요도 없으며, 클래스에 대해 전혀 생각할 필요도 없습니다. 그럼 styled-components는 어떻게 이를 해결할까요?
styled-components에 익숙하지 않다면 우선 공식문서를 읽어보세요.
마법 같은 구문
우선 styled-components를 사용하여 정적인 스타일의 간단한 버튼을 생성합니다:
const Button = styled.button`
color: coral;
padding: 0.25rem 1rem;
border: solid 2px coral;
border-radius: 3px;
margin: 0.5rem;
font-size: 1rem;
`;
여기서 styled.button은 styled('button')의 단축어이며, 사용 가능한 HTML element 목록에서 동적으로 생성된 많은 함수 중 하나입니다. Tagged templates에 익숙하다면 button이 그저 함수라는 것을 알고, 간단한 문자열을 배열 매개변수로 호출할 수 있다는 것을 알 것입니다. 이 코드를 이해하기 쉽게 바꿔보죠.
const Button = styled('button')([
'color: coral;' +
'padding: 0.25rem 1rem;' +
'border: solid 2px coral;' +
'border-radius: 3px;' +
'margin: 0.5rem;' +
'font-size: 1rem;'
]);
styled가 단순히 컴포넌트 팩토리라는 것을 이해하면, 그 구현이 어떻게 생겼는지 상상할 수 있습니다.
styled-components를 구현해보자
const myStyled = (TargetComponent) => ([style]) => class extends React.Component {
componentDidMount() {
this.element.setAttribute('style', style);
}
render() {
return (
<TargetComponent {...this.props} ref={element => this.element = element } />
);
}
};
const Button = myStyled('button')`
color: coral;
padding: 0.25rem 1rem;
border: solid 2px coral;
border-radius: 3px;
margin: 0.5rem;
font-size: 1rem;
`;
이 구현은 꽤 간단합니다 — 팩토리는 클로저에 저장된 태그 이름을 기반으로 새로운 컴포넌트를 생성하고 마운팅 후에 인라인 스타일을 설정합니다. 그런데 우리의 컴포넌트가 props를 기반으로 한 스타일을 가지고 있다면 어떨까요?
const primaryColor = 'coral';
const Button = styled('button')`
background: ${({ primary }) => primary ? 'white ' : primaryColor};
color: ${({ primary }) => primary ? primaryColor : 'white'};
padding: 0.25rem 1rem;
border: solid 2px ${primaryColor};
border-radius: 3px;
margin: 0.5rem;
`;
컴포넌트가 마운트될 때 또는 해당 props가 업데이트될 때 스타일에서 보간(interpolations)을 평가하도록 구현체를 업데이트해야 합니다.
const myStyled = (TargetComponent) => (strs, ...exprs) => class extends React.Component {
interpolateStyle() {
const style = exprs.reduce((result, expr, index) => {
const isFunc = typeof expr === 'function';
const value = isFunc ? expr(this.props) : expr;
return result + value + strs[index + 1];
}, strs[0]);
this.element.setAttribute('style', style);
}
componentDidMount() {
this.interpolateStyle();
}
componentDidUpdate() {
this.interpolateStyle();
}
render() {
return <TargetComponent {...this.props} ref={element => this.element = element } />
}
};
const primaryColor = 'coral';
const Button = myStyled('button')`
background: ${({ primary }) => primary ? primaryColor : 'white'};
color: ${({ primary }) => primary ? 'white' : primaryColor};
padding: 0.25rem 1rem;
border: solid 2px ${primaryColor};
border-radius: 3px;
margin: 0.5rem;
font-size: 1rem;
`;
여기서 가장 까다로운 부분은 스타일 문자열을 얻는 것입니다:
const style = exprs.reduce((result, expr, index) => {
const isFunc = typeof expr === 'function';
const value = isFunc ? expr(this.props) : expr;
return result + value + strs[index + 1];
}, strs[0]);
우리는 하나씩 모든 문자열 조각을 표현식의 결과와 연결합니다. 그리고 표현식이 함수라면 컴포넌트의 속성이 전달되어 호출됩니다.
이 간단한 팩토리의 API는 styled-components가 제공하는 것과 유사하게 보이지만, 원래의 구현은 내부적으로 훨씬 더 흥미롭게 작동합니다: 그것은 인라인 스타일을 사용하지 않습니다. styled-components를 가져오고 컴포넌트를 만들 때 무슨 일이 벌어지는지 좀 더 자세히 살펴봅시다.
styled-components 내부
styled-components를 가져올 때
앱에서 라이브러리를 처음 가져올 때, styled 팩토리를 통해 생성된 모든 컴포넌트를 세기 위해 내부 카운터 변수를 생성합니다.
styled.tag-name 팩토리를 호출할 때
const Button = styled.button`
font-size: ${({ sizeValue }) => sizeValue + 'px'};
color: coral;
padding: 0.25rem 1rem;
border: solid 2px coral;
border-radius: 3px;
margin: 0.5rem;
&:hover {
background-color: bisque;
}
`;
styled-components가 새로운 컴포넌트를 만들면 내부 식별자 componentId도 생성됩니다. 식별자가 어떻게 계산되는지는 다음과 같습니다:
counter++;
const componentId = 'sc-' + hash('sc' + counter);
앱에서 첫 번째 styled 컴포넌트의 componentId는 sc-bdVaJa입니다.
현재 styled-components는 MurmurHash 알고리즘을 사용하여 고유한 식별자를 생성하고, 그 후 해시 번호를 알파벳 이름으로 변환합니다.
식별자가 생성되자마자, styled-components는 새로운 HTML <style> 요소를 페이지의 <head>에 삽입합니다(만약 그것이 첫 번째 컴포넌트이고 요소가 아직 삽입되지 않았다면). 그리고 나중에 사용될 요소에 componentId가 있는 특별한 주석을 추가합니다. 우리의 경우에는 다음과 같이 되었습니다:
<style data-styled-components>
/* sc-component-id: sc-bdVaJa */
</style>
새 컴포넌트가 생성될 때, 팩토리로 전달된 타겟 컴포넌트(target)와 컴포넌트의 고유 식별자(componentId)가 정적 필드(static fields)에 저장됩니다. 여기서 우리 예제의 경우 타겟 컴포넌트는 'button'입니다.
StyledComponent.componentId = componentId;
StyledComponent.target = TargetComponent;
여러분이 볼 수 있듯이, 단순히 styled 컴포넌트를 생성할 때 성능 상의 오버헤드는 전혀 없습니다. 수백 개의 컴포넌트를 정의하고 사용하지 않더라도, 내부에 주석이 있는 하나 또는 그 이상의 <style> 요소만 얻게 됩니다.
styled 팩토리를 통해 생성된 컴포넌트에 관한 또 하나의 중요한 사항이 있습니다: 그들은 몇 가지 라이프 사이클 메소드를 구현하는 BaseStyledComponent 클래스에서 상속됩니다. 그것이 무엇을 위한 것인지 살펴봅시다.
componentWillMount()
위의 버튼의 인스턴스를 만들어 페이지에 마운트합시다:
ReactDOM.render(
<Button sizeValue={24}>I'm a button</Button>,
document.getElementById('root')
);
BaseStyledComponent의 라이프사이클 메서드인 componentWillMount()가 작동합니다. 이는 몇 가지 중요한 작업을 담당하고 있습니다:
1. tagged template 평가
이 알고리즘은 우리가 custom myStyled 팩토리에서 구현한 것과 매우 유사합니다. Button 컴포넌트 인스턴스에 대해:
<Button sizeValue={24}>I'm a button</Button>
다음과 같은 evaluatedStyles 문자열을 얻습니다:
font-size: 24px;
color: coral;
padding: 0.25rem 1rem;
border: solid 2px coral;
border-radius: 3px;
margin: 0.5rem;
&:hover {
background-color: bisque;
}
2. CSS 클래스 이름 생성
고유한 props를 가진 각 컴포넌트 인스턴스는 자체 CSS 클래스 이름을 갖습니다. 이 이름은 componentId와 평가된 스타일 문자열(evaluatedStyles)을 기반으로 동일한 MurmurHash 알고리즘을 사용하여 생성됩니다:
const className = hash(componentId + evaluatedStyles);
Button 인스턴스의 경우 생성된 className은 jsZVzX입니다.
그런 다음 이 클래스 이름은 generatedClassName으로 컴포넌트 상태에 저장됩니다.
3. CSS 전처리
여기에서는 매우 빠른 stylis CSS 전처리기가 도움을 주어 유효한 CSS 문자열을 얻을 수 있습니다.
const selector = '.' + className;
const cssStr = stylis(selector, evaluatedStyles);
Button 인스턴스에 대한 결과 CSS:
.jsZVzX {
font-size: 24px;
color: coral;
padding: 0.25rem 1rem;
border: solid 2px coral;
border-radius: 3px;
margin: 0.5rem;
}
.jsZVzX:hover{
background-color: bisque;
}
4. 페이지에 CSS 문자열 삽입
이제 CSS는 헤드에 있는 <style> 태그의 앞에 생선된 주석 바로 뒤에 삽입 됩니다.
<style data-styled-components>
/* sc-component-id: sc-bdVaJa */
.sc-bdVaJa {} .jsZVzX{font-size:24px;color:coral; ... }
.jsZVzX:hover{background-color:bisque;}
</style>
보시다시피 styled-components는 속성값이 없는 CSS 클래스와 componentId (.sc-bdVaJa)도 주입합니다.
render()
CSS 작업이 완료되면 styled-components는 해당 className을 가진 요소를 생성하기만 하면 됩니다.
const TargetComponent = this.constructor.target; // In our case just 'button' string.
const componentId = this.constructor.componentId;
const generatedClassName = this.state.generatedClassName;
return (
<TargetComponent
{...this.props}
className={this.props.className + ' ' + componentId + ' ' + generatedClassName}
/>
);
styled-components는 3개의 클래스 이름으로 요소를 렌더링합니다:
- this.props.className — 부모 컴포넌트에 의해 선택적으로 전달됩니다.
- componentId — 컴포넌트의 고유 식별자이지만 컴포넌트 인스턴스는 아닙니다. 이 클래스에는 CSS 규칙이 없지만 다른 컴포넌트를 참조해야 할 때 중첩 선택자에서 사용됩니다.
- generatedClassName — 실제 CSS 규칙이 있는 모든 컴포넌트 인스턴스에 대해 고유합니다.
그것이 전부입니다! 최종 렌더링된 HTML 요소는 다음과 같습니다:
<button class="sc-bdVaJa jsZVzX">I'm a button</button>
`componentWillReceiveProps()`는
이제 마운트 된 상태에서 버튼 속성을 변경해 봅시다. 이렇게 하려면 Button에 대해 보다 상호 작용 가능한 예제를 만들어야 합니다:
let sizeValue = 24;
const updateButton = () => {
ReactDOM.render(
<Button sizeValue={sizeValue} onClick={updateButton}>
Font size is {sizeValue}px
</Button>,
document.getElementById('root')
);
sizeValue++;
}
updateButton();
버튼을 클릭할 때마다 `componentWillReceiveProps()`가 호출되어 `sizeValue` prop을 증가시키며 `componentWillMount()`에서 수행하는 것과 동일한 작업을 수행합니다:
- 태그가 지정된 템플릿을 평가합니다.
- 새로운 CSS 클래스 이름을 생성합니다.
- stylis로 스타일을 전처리합니다.
- 전처리 된 CSS를 페이지에 삽입합니다.
실제로 생성된 스타일 내용을 확인하려면 개발자 도구를 통해 직접 확인해야 합니다.
<style data-styled-components>
/* sc-component-id: sc-bdVaJa */
.sc-bdVaJa {}
.jsZVzX{font-size:24px;color:coral; ... } .jsZVzX:hover{background-color:bisque;}
.kkRXUB{font-size:25px;color:coral; ... } .kkRXUB:hover{background-color:bisque;}
.jvOYbh{font-size:26px;color:coral; ... } .jvOYbh:hover{background-color:bisque;}
.ljDvEV{font-size:27px;color:coral; ... } .ljDvEV:hover{background-color:bisque;}
</style>
네, 각 CSS 클래스에 대한 유일한 차이점은 `font-size` 속성뿐이며, 사용되지 않는 CSS 클래스는 제거되지 않습니다. 그렇다면 왜 그런 것일까요? 사용되지 않는 CSS 클래스를 제거하는 것은 성능상의 오버헤드를 추가하는 반면, 그대로 두는 것은 그렇지 않기 때문입니다. (이에 대한 Max Stoiber의 코멘트를 참고하십시오.)
여기에는 작은 최적화가 있습니다: 스타일 문자열에 보간이 없는 컴포넌트는 `isStatic`으로 표시되며, 이 플래그는 `componentWillReceiveProps()`에서 같은 스타일의 불필요한 계산을 건너뛰기 위해 확인됩니다.
성능 팁
styled-components의 내부 동작 방식을 알게 되면, 우리는 성능에 더 집중할 수 있습니다.
버튼을 사용한 예제에는 이스터 에그가 있습니다. (힌트: 버튼을 200번 이상 클릭하면 콘솔에 styled-components로부터의 숨겨진 메시지를 볼 수 있습니다. 농담이 아닙니다! 😉).
만약 너무 궁금하다면, 여기 메시지가 있습니다:
Over 200 classes were generated for component styled.button.
Consider using the attrs method, together with a style object for frequently changed styles.
Example:
const Component = styled.div.attrs({
style: ({ background }) => ({
background,
}),
})`width: 100%;`
<Component />
여기 버튼이 리팩토링 후 어떻게 보이는지입니다:
const Button = styled.button.attrs({
style: ({ sizeValue }) => ({ fontSize: sizeValue + 'px' })
})`
color: coral;
padding: 0.25rem 1rem;
border: solid 2px coral;
border-radius: 3px;
margin: 0.5rem;
&:hover {
background-color: bisque;
}
`;
그러나 모든 동적 스타일에 이 기술을 사용해야 할까요? 아닙니다. 하지만 개인적으로 결과의 수가 정해지지 않은 모든 동적 스타일에 대해 `style` 속성을 사용합니다. 예를 들어, 서버에서 다양한 색상으로 로드되는 태그 목록 또는 단어 클라우드와 같은 사용자 정의 `font-size`를 가진 컴포넌트가 있다면 `style` 속성을 사용하는 것이 좋습니다. 그러나 하나의 컴포넌트에서 기본, 주요, 경고 등 다양한 버튼을 가지고 있다면 스타일 문자열에서 조건과 함께 보간을 사용하는 것이 괜찮습니다.
아래 예제에서는 개발 버전을 사용하지만, 프로덕션 번들에서는 항상 styled-components의 프로덕션 빌드를 사용해야 합니다. 왜냐하면 이것이 더 빠르기 때문입니다. React에서처럼 styled-components의 프로덕션 빌드는 많은 개발 경고를 비활성화하며, 가장 중요한 것은 생성된 스타일을 페이지에 삽입하기 위해 `CSSStyleSheet.insertRule()`을 사용하는 반면, 개발 버전은 `Node.appendChild()`를 사용합니다. (여기서 Evan Scott은 `insertRule`이 얼마나 빠른지 보여줍니다).
또한 `babel-plugin-styled-components`의 사용을 고려해 보세요. 이것은 스타일을 로딩하기 전에 스타일을 축소하거나 심지어 전처리 할 수 있습니다.
결론
styled-components의 작업 흐름은 매우 간단합니다. 컴포넌트가 렌더링 되기 직전에 필요한 CSS를 즉시 생성하며, 브라우저에서 태그가 달린 문자열을 평가하고 CSS를 전처리하는 것에도 불구하고 충분히 빠릅니다.
이 글은 styled-component의 모든 측면을 다루지 않았지만, 주요한 부분에 중점을 두려고 노력했습니다.
이 텍스트를 작성하기 위해 styled-components v3.3.3을 사용했습니다. 이 글은 내부 작동 원리에 관한 것이기 때문에 향후 버전에서 많은 것들이 변경될 수 있습니다.
'개발관련 > 프론트엔드 지식' 카테고리의 다른 글
next.js에서 next-auth를 사용한 oauth 구현하기 - next.js 14 app router (0) | 2024.03.14 |
---|---|
styled-components와 Tagged Template Literals styled-components는 어떻게 style을 만들까 (0) | 2023.08.09 |
Styled-Components의 특징 (0) | 2023.08.09 |
리플로우가 일어나는 css 스타일 속성과 일어나지 않는 스타일 속성이 무엇이 있나요? (0) | 2023.07.11 |
OWASP Top 10: 웹 애플리케이션 보안 취약점 (0) | 2023.07.09 |