본 글은 Demystifying styled-components 를 번역한 글입니다. 오역이 있을 수 있습니다.
처음 styled-components 를 사용했을때, 마법과도 같았습니다.
어찌 되었든, 문자열인 것 같기도 함수인 것 같기도 한 구문을 사용함으로써 이 도구는 임의의 CSS를 가져다 사용할 수도 있고, 우리가 항상 사용해 오던 CSS 선택자들을 전달함으로써 React 컴포넌트에 할당할 수도 있습니다.
많은 개발자들이 그렇듯이, 저 또한 내부동작에 대한 이해 없이 styled-components 의 사용법을 학습했습니다.
동작방식을 아는 것은 도움이 됩니다. 운전을 하기 위해서 자동차가 어떤 방식으로 작동하는지 이해할 필요는 없지만, 자동차가 도로변에 고장 난 채로 서있을 때 도움이 될 것입니다.
CSS 를 디버깅하는 것은 전용 도구를 추가하지 않고는(without adding in a layer of tooling magic) 어렵습니다. styled-components 를 이해함으로써, 낯선(weird) CSS 문제들을 진단하고 수정하는 데 힘을 덜 들일 수 있습니다.
이 글에서, 자동차의 후드를 열고 styled-components 를 직접 구현해 보며 동작 방식을 배울 것입니다.
공식문서에 나와있는 간단한 예시를 살펴봅시다.
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`
styled-components 는 각각의 DOM 노드와 일치하는 도우미 함수(helper method)의 모음과 함께 제공됩니다. DOM 노드에는, h1, header, button 및 수십개의 태그들이 있습니다. (심지어 선과 경로와 같은 SVG 요소들도 지원합니다.)
도우미 함수(helper method) 란?
반복되는 작업을 함수로 만들어 다른 함수에서 호출할 수 있는 함수
도우미 함수는 "taged template literals" 로 알려진 JavaScript 기능을 이용해, CSS 덩어리와 함께 호출됩니다. 지금까지는, 아래와 같이 작성할 수 있다고 가정할 수 있습니다.
const Title = styled.h1(`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`);
h1은 styled
객체의 도우미 함수이며, 하나의 문자열 인자를 받아 호출할 수 있습니다.
이 도우미 함수들은 작은 컴포넌트 공장입니다. 우리가 함수를 호출할 때마다, 새로운 React 컴포넌트를 얻을 수 있습니다.
이 부분을 확인해 봅시다.
// When I call this function...
function h1(styles) {
// ...its generates a brand-new React component...
return function NewComponent(props) {
// ...which will render the associated HTML element:
return <h1 {...props} />
}
}
const Title=styled.h1(...)
을 실행시키면, NewComponent
가 Title
에 할당 될 것입니다. 그런다음 Title
을 렌더링하면 <h1>
DOM 노드가 만들어 집니다.
h1
함수에 전달한 styles
인자는 어떻게 사용되는 것일 까요?
Title
컴포넌트를 렌더링하면, 몇가지 일이 발생합니다:
dKamQW
나 iOacVe
처럼, styles
을 임의의 문자열로 해싱한 유일한 클래스 이름을 만듭니다.Stylis 란?
필요에 따라 벤더 프리픽스를 추가해 주는 CSS 전처리기
styles
문자열의 모든 CSS 선언을 포함시켜 새로운 CSS 클래스를 페이지에 삽입합니다.아래는 위 과정을 코드로 표현한 것입니다.
function h1(styles) {
return function NewComponent(props) {
const uniqueClassName = comeUpWithUniqueName(styles);
const processedStyles = runStylesThroughStylis(styles);
createAndInjectCSSClass(uniqueClassName, processedStyles);
return <h1 className={uniqueClassName} {...props} />
}
}
<Title>Hello World</Title>
을 렌더링하면, 결과는 아래와 같이 됩니다.
<style>
.dKamQW {
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
</style>
<h1 class="dKamQW">Hello World</h1>
리액트에서, 조건에 따라 JSX를 렌더링 하는 것은 흔한 일입니다. 이번 예시에서는, ItemList
컴포넌트가 요소를 가지고 있을 경우에만 <Wrapper>
렌더링 할 것입니다.
이 경우, CSS와 styled-components가 아무런 일을 하지 않는 것이 놀라울 수 있습니다. 이러한 이유는 background
가 DOM에 추가되지 않았기 때문입니다.
styled-components가 정의될 때 CSS 클래스들이 생성되는 대신, 컴포넌트가 렌더링될 때까지 기다린 후 해당 스타일을 페이지에 삽입합니다.
큰 규모의 웹사이트에서, 사용하지 않는 수백 킬로바이트의 CSS를 브라우저에 전송하는 것은 보편적이지 않습니다. styled-components를 사용하면, 작성한 CSS가 아닌 렌더링된 CSS에만 집중할 수 있습니다.
이것이 가능한 이유는, JavaScript의 클로저 때문입니다. styled.h1
으로 부터 생성된 모든 컴포넌트는 CSS 문자열을 가진 그들만의 스코프가 있습니다. 수 초/분/시간 후에 Wrapper
컴포넌트를 렌더링할 때, 우리가 작성한 스타일들에 접근할 수 있습니다.
CSS 삽입을 미룬 또 한 가지 이유는 스타일의 보강(interpolated styles) 때문입니다. 이 내용은 이 글 끝에 살펴보겠습니다.
createAndInjectCSSClass
함수가 어떤 방식으로 동작하는지 궁금했을 수도 있습니다. 정말 CSS 클래스들을 JS 안에서 만들 수 있을까요?
네 가능합니다! <style>
태그를 만들고 그 안에 CSS 문자를 넣는 직관적인 방법이 있습니다.
const styleTag = document.createElement('style');
document.head.appendChild(styleTag);
const newRule = document.createTextNode(`
.dKamQw {
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
`);
styleTag.appendChild(newRule);
이 방법은 동작하지만 무겁고 느립니다. 좀 더 현대적인 방법은 CSS의 DOM 버전인 CSSOM을 사용하는 것입니다. CCSOM은 JavaScript를 사용하여 CSS 규칙들을 추가하고 제거하는 친숙한 방법들을 제공합니다.
const styleSheet = document.styleSheets[0];
styleSheet.insertRule(`
.dKamQW {
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
`);
오랫동안 CSSOM으로 생성된 스타일들은 Chrome 개발자 도구를 이용해 편집할 수 없었습니다. 개발자 도구에서 회색으로 칠해져있는 부분을 본 적이 있다면, 편집 제한 때문입니다.
그러나 다행이도, Chrome 85부터 바꼈습니다. 관심이 있다면, Chrome 팀이 어떻게 CSS-in-JS 라이브러리에 대한 지원을 개발 도구에 추가했는지에 대한 블로그 게시물을 읽어보세요.
초반에 styled.h1
을 따라하기 위해 h1
함수를 만들었습니다.
function h1(styles) {
return function NewComponent(props) {
const uniqueClassName = comeUpWithUniqueName(styles);
const processedStyles = runStylesThroughStylis(styles);
createAndInjectCSSClass(uniqueClassName, processedStyles);
return <h1 className={uniqueClassName} {...props} />
}
}
이 방법은 동작하지만, 많은 도우미 함수가 필요합니다! 우리는 button, link, footer, marquee 태그가 필요합니다.
또 다른 문제도 있습니다. 아래에서 보겠지만, styled
은 함수로 직접 호출될 수 있습니다.
const AlternativeSyntax = styled('h1')`
font-size: 1.5em;
`
styled
객체는 함수이며 객체입니다. 놀랍게도 JavaScript에서는 가능한 일입니다. 아래와 같이 작성할 수 있습니다.
function magic() {
console.log('🌠');
}
magic.hands = function() {
console.log('🔭');
}
magic(); // logs '🌠'
magic.hands(); // logs '🔭'
styled-components 클론을 새로운 요구 사항을 만족할 수 있도록 업데이트해봅시다. 이 기능을 구현하기 위해서는 함수형 프로그래밍의 아이디어를 빌려와야 합니다.
const styled = (Tag) => (styles) => {
return function NewComponent(props) {
const uniqueClassName = comeUpWithUniqueName(styles);
const processedStyles = runStylesThroughStylis(styles);
createAndInjectCSSClass(uniqueClassName, processedStyles);
return <Tag className={uniqueClassName} {...props} />
}
}
styled.h1 = styled('h1');
styled.button = styled('button');
// ...And so on, for all DOM nodes!
위 코드는 커링(currying)을 사용했습니다. 이를 통해 태그 인자들을 미리 로드(preload)를 할 수 있습니다.
한 번도 커링을 본 적이 없다면, 당황스러울 수 있습니다. 하지만 이 경우, 많은 도우미 함수들을 쉽게 만들 수 있어 유용합니다.
// This:
styled.h1(`
color: peachpuff;
`);
// ...is equivalent to this:
styled('h1')(`
color: peachpuff;
`);
styled-components의 훌륭한 기능중 하나는 사용자 지정 컴포넌트와 합칠 수 있다는 것 입니다.
예시는 아래와 같습니다.
언뜻보기에 마법 같습니다. 어떻게 사용자 지정 컴포넌트에 스타일을 지정할 수 있는 걸까요?
핵심은 prop 전파(prop delegation) 입니다. 아래는 delegated
객체를 출력한 예시 입니다.
function Message({children, ...delegated}) {
console.log(delegated);
// {className: 'OkjqvF'}
return (
<p {...delegated}>
You've received a message: {children}
</p>
)
}
className
은 어디서 온 것일까요? 사실 styled 도우미 함수에서 만들었습니다.
const styled = (Tag) => (styles) => {
return function NewComponent(props) {
const uniqueClassName = comeUpWithUniqueName(styles);
const processedStyles = runStylesThroughStylis(styles);
createAndInjectCSSClass(uniqueClassName, processedStyles);
return <Tag className={uniqueClassName} {...props} />
}
}
순서는 다음과 같습니다:
Message
컴포넌트를 구성하는 styled component인 UrgentMessage
를 렌더링합니다.Tag
변수(Message
컴포넌트)를 className
prop와 함께 렌더링 합니다.className
prop이 <p>
태그에 전달되며, Message
가 렌더링 됩니다.이 방법은 className
prop을 컴포넌트 안에 있는 HTML 노드에 전달할 때만 동작합니다. 아래는 동작하지 않습니다.
function Message({children}) {
/*
Because we're ignoring the `className` prop,
the styles will never be set.
*/
return (
<p>
You've received a Message: {children}
</p>
)
}
Message
가 전달받은 props들을 <p>
태그가 전달받음으로써 이러한 방식을 사용할 수 있습니다. 다행히 많은 third-party 컴포넌트들이 (eg.react-router Link
컴포넌트) 이 방식을 따르고 있습니다.
사용자 지정 컴포넌트들을 감싸는 것 외에, styled-component들을 합칠 수도 있습니다.
저는 처음에 라이브러리가 이 스타일들을 합쳐, 모든 선언들을 포함하는 새로운 "mega class"를 만드는 것으로 생각했습니다. 하지만 사실 두개의 다른 클래스들을 만듭니다.
생성된 HTML/CSS를 살펴보면 다음과 같습니다.
<style>
.abc123 {
background-color: transparent;
font-size: 2rem;
}
.def456 {
background-color: pink;
}
</style>
<button class="abc123 def456">Hello World</button>
이 과정의 목표는 PinkButton
이 Button
의 styles을 상속받는지 확인하는 것입니다. 충돌이 발생하면, PinkButton
이 우선순위가 높습니다.
JavaScript에서 복잡한 작업을 하는 것보다, CSS를 통해 어려운 작업을 진행할 수 있습니다. CSS에는, 충돌을 해소하는 복잡한 우선순위 규칙이 있습니다. Id 선택자인 #btn
은 class 선택자인 .btn
보다 우선순위가 높습니다. class 선택자는 tag 선택자인 button
보다 우선순위가 높습니다.
하지만 이 경우, 우리는 두개의 클래스들을 가지고 있습니다! 두개의 선택자인 .abc123
과 .def456
은 동일한 우선순위를 가지고 있습니다. 따라서 알고리즘은 stylesheet에 선언된 순서를 살펴보는 두번째 규칙을 적용합니다.
.def456
이 .abc123
보다 나중에 선언되었기 때문에 우선순위가 높습니다. 따라서 버튼이 핑크색일 것입니다.
중요한 설명: 클래스의 적용순서는 중요하지 않습니다. 아래의 경우를 살펴봅시다:
<style>
.red {
color: red;
}
.blue {
color: blue;
}
</style>
<p class="blue red">Hello</p>
<p class="red blue">Hello</p>
첫 번째 p 태그는 빨간색, 두 번째 p 태그는 파란색일 것으로 예상했을 것입니다. 하지만 이는 틀린예상 입니다: 두 p태그는 모두 파란색입니다. class
속성의 클래스들 순선는 중요하지 않습니다.
styled-components 라이브러리는 스타일이 올바르게 적용될 수 있게 하기 위해서 올바른 순서로 삽입될 수 있도록 세심한 주의를 기울입니다. 이는 쉬운 문제가 아닙니다. 우리는 React에서 많은 동적인 작업을 하며 라이브러리는 순서를 유지하면서 클래스들을 계속 추가/제거합니다.
이제 styled-components 클론을 작업해 봅시다.
두 클래스들을 적용하기 위해 코드를 업데이트해야 합니다. 아래와 같이 합칠 수 있습니다.
const styled = (Tag) => (styles) => {
return function NewComponent(props) {
const uniqueClassName = comeUpWithUniqueName(styles);
const processedStyles = runStylesThroughStylis(styles);
createAndInjectCSSClass(uniqueClassName, processedStyles);
const cobinedClassess =
[uniqueClassName, props.className].join(' ');
return <Tag {...props} className={combinedClasses} />
}
}
PinkButton
을 렌더링할 때, Tag
변수는 Button
입니다. PinkButton
는 유일한 클래스 이름(def456)을 생성하고, 이것을 className
prop로 Button
에 전달합니다.
이 과정을 수행하는 JavaScript 메커니즘은 중요하지 않습니다. 다음은 구현 세부 사항입니다. 중요한 것은 이 사항들을 이해하는 것입니다.
PinkButton
을 렌더링할때, Button
도 렌더링 됩니다.abc123
이나 def456
와 같은 유일한 클래스를 만듭니다.PinkButton
의 스타일이 Button
과 겹치는 부분은 덮어쓸 수 있게 합니다.최소 기능의 styled-components 클론을 거의 마쳤으나, interpolated styles가 무엇인지 살펴보는 것이 남았습니다.
가끔 CSS는 React의 props에 의존할 때가 있습니다. 예를 들어 이미지가 maxWidth
prop을 가지는 상황을 예로 들어 봅시다.
아래는 이 이미지들이 렌더링된 다음의 DOM의 모습입니다.
<style>
.JDSLg {
display: block;
margin-bottom: 8px;
width: 100%;
max-width: 200px;
}
.eXyedY {
display: block;
margin-bottom: 8px;
width: 100%;
}
</style>
<img
alt="A Dummy Image"
src="https://via.placeholder.com/350"
class="sc-bdnxRM JDSLg"
/>
<img
alt="A Dummy Image"
src="https://via.placeholder.com/350"
class="sc-bdnxRM eXyedY"
/>
첫 번째 클래스인 sc-bdnxRM
은 렌더링 된 React 컴포넌트(ContentImage
)를 구별하기 위해 사용됐습니다. 이 클래스는 어떤 스타일도 제공하지 않기 때문에, 이 글에서는 무시하도록 하겠습니다.
흥미로운 사실은 각각의 이미지들이 완전히 다른 클래스를 가지고 있다는 점입니다.
무작위로 만들어진 것 같은 JDSLg
와 eXyedY
는 각각의 적용된 스타일의 실제 해쉬값 입니다. 우리가 다른 maxWidth
prop을 추가한다면, 다른 스타일들이 적용될 것이고 그로 인해 다른 클래스가 생성됩니다.
이것이 클래스를 미리 생성하지 않는 이유입니다! 컴포넌트가 렌더링 될 때까지 기다려야 CSS가 적용됩니다. 같은 styled.img
인스턴스라 하더라도 항상 같은 스타일들을 만드는 것은 아니기 때문입니다.
Interpolation이 컴포넌트를 커스텀할 수 있는 유일한 방법은 아닙니다. 개인적으로 좋아하는 방법은 CSS 변수를 사용하는 것입니다. 예시는 아래와 같습니다.
const ContentImage = styled.img`
display: block;
margin-bottom: 8px;
width: 100%;
max-width: var(--max-width);
`
render(
<>
<ContentImage
alt="A Dummy Image"
src="https://via.placeholder.com/350"
style={{
'--max-width': '200px'
}}
/>
<ContentImage
alt="A Dummy Image"
src="https://via.placeholder.com/350"
/>
</>
)
HTML을 검사해보면 두 요소가 동일한 CSS 클래스를 가지고 있음을 확인할 수 있습니다.
<style>
.JDSLg{
display: block;
margin-bottom: 8px;
width: 100%;
max-width: var(--max-width);
}
</style>
<img
alt="A Dummy Image"
src="https://via.placeholder.com/350"
class="sc-bdnxRM JDSLg"
style="--max-width: 200px"
/>
<img
alt="A Dummy Image"
src="https://via.placeholder.com/350"
class="sc-bdnxRM JSDLg"
/>
최신 CSS에 보다 다양한 기능들이 추가됨으로써, 짧은 CSS를 작성할 수 있습니다. 동적으로 데이터를 바꿀때, 새로운 CSS 클래스를 만들어 페이지에 붙일 필요가 없어 성능적인 이점이 있습니다.
하지만 styled-components는 최적화가 매우 잘돼있는 라이브러리입니다. 따라서 대부분의 상황에서 괄목할만한 큰 차이는 없을것 입니다.
글 윗부분에서 아래의 두 코드가 동일하다고 이야기한 것을 상기해 보십시오.
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`
const Title = styled.h1(`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`)
inperpolation을 알고 난 후부터, 둘은 더 이상 같다고 할 수 없습니다.
Tagged template literal은 복잡해서(wild), 어떤 방식으로 동작하며 어떤 역할을 수행하는지를 설명하려면 독자적인 주제로 글을 써야합니다.
중요한 것은 컴포넌트가 렌더링 될 때, interpolation 함수들이 호출되고 스타일 문자를 채우기 위해 사용된다는 것입니다.
아래는 예시입니다.
const styled = (Tag) => (rawStyles, ...interpolations) => {
return function NewComponent(props) {
const styles = reconcilStyled(
rawStyles,
interpolations,
props
);
const uniqueClassName = comeUpWithUniqueName(styles);
const processedStyles = runStyledThroughStylis(styles);
createAndInjectCSSClass(uniqueClassName, processedStyles);
const combinedClasses = [uniqueClassName, props.className].join(' ');
return <Tag {...props} className={combinedClasses} />
}
}
컴포넌트를 렌더링할 때, reconcileStyles
은 props를 통해 전달받은 데이터를 이용해 interpolated 함수를 실행합니다.
props가 바뀌면, 위 과정은 반복되고 새로운 클래스가 만들어 집니다.