react를 공부하면서 미니 프로젝트를 진행하고 나서 얻은 조언이 styled components를 적용하는 것이었습니다. 살짝 공부해서 적용해 보니, 좀 더 깊게 파고들면 기본 css를 사용하는 것에 비해 장단점이 존재하지만 사용의 편의성면에서는 styled components를 사용하는 것이 훨씬 작업하기 편리했습니다.
조금 더 깊게 공부를 하려고 홈페이지의 공식 문서를 살펴보다가 나중을 위해 요약 정리했습니다.
1. 동기 부여 (Motivation)
styled-components가 React component system에서 스타일을 지정하기 위해 css를 어떻게 향상할 수 있는지 고민한 결과입니다. 단일 사용 사례(a single use case)에 집중함으로써 개발자의 경험과 최종 사용자의 결과를 최적화할 수 있었습니다.
- Automatic critical CSS (자동화된 css 최적화)
styled-components는 페이지에 렌더링 되는 구성 요소를 추적하고 필요한 스타일만 사용합니다. 이는 완전히 자동화되어 있습니다. 사용자는 꼭 필요한 최소한의 코드만 로딩하게 됩니다. - No class name bugs (class name에 의한 버그가 없음)
styled-components는 스타일에 대한 고유한(unique) 클래스 이름을 생성합니다. 중복, 중첩, 혹은 철자 오류에 대해 걱정할 필요가 없습니다. - Easier deletion of CSS (CSS 삭제가 편리함)
css에서는 프로그램 전체 소스 코드에서 어디서 어떤 클래스 이름이 사용 중인지 아닌지 알기 어렵습니다. styled-components를 사용하면 모든 스타일링이 특정 component에 연결되어 있으므로 이를 명확히 알 수 있습니다. 구성 요소가 사용되지 않고 삭제되면 모든 스타일도 함께 삭제됩니다. - Simple dynamic styling (간단한 동적 스타일링)
수십 개의 클래스를 수동으로 관리할 필요 없이 props(속성)나 global theme(전역 테마)를 기반으로 component의 스타일을 조정하는 것이 간단하고 직관적입니다. - Painless maintenance (간편한 유지 관리)
component에 영향을 미치는 스타일을 찾기 위해 여러 파일을 검색할 필요가 없으므로 codebase(프로그램 전체 소스 코드)가 아무리 크더라도 유지 관리가 매우 쉽습니다. - Automatic vendor prefixing (자동화된 vendor prefixing)
css를 현재 표준에 맞게 작성하고 styled-components에서 나머지를 처리하도록 합니다.
- vendor prefixing : css3 표준으로 확정되기 이전 또는 브라주어 개발사가 실험적으로 제공하는 기능을 사용하기 위한 접두어들 (ex: -webkit-, -moz-, -ms-,...)
css를 개별 components에 바인당하 작성하므로 위의 모든 이점을 가질 수 있습니다.
2. 설치 (Installation) 및 import
2.1 Node 환경에서 설치
# with npm
npm install styled-components
# with yarn
yarn add styled-components
Babel plugin도 함께 사용할 것을 강력히 추천합니다. (필수는 아님)
- 더 읽기 쉬운 클래스 이름, server-side rendering 호환성, 보다 작은 번들 등과 같은 이점을 제공합니다.
2.2 CDN을 사용한 script 로딩
<script src="https://unpkg.com/styled-components/dist/styled-components.min.js"></script>
위와 같이 CDN을 이용하는 경우 브라우저 전역 객체로 window.styled 변수를 사용할 수 있습니다.
const Component = window.sytled.div`
color: red;
`
이 방법을 사용할 경우 react CDN bundles과 react-is CDN bundle이 필요합니다. (styled-components script 앞에 위치해야 함)
2.3 JS 파일에 import
react의 js 파일에서 사용하기 위해서는 모듈을 import 해야 합니다.
import styled from "styled-components";
3. 시작하기 (Getting Started)
styled-components 태그가 지정된 템플릿 리터럴을 활용하여 component의 스타일을 지정합니다.
이것은 component와 styles 간의 mapping을 제거합니다. 당신이 자신의 스타일을 정의할 때, 실제로 스타일이 적용된 일반 React component를 생성한다는 뜻입니다.
import styled from "styled-components";
const Button = styled.button`
// css codes
`;
// Create a Title component that'll render an <h1> tag with some styles
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: #BF4F74;
`;
// Create a Wrapper component that'll render a <section> tag with some styles
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
// Use Title and Wrapper like any other React component – except they're styled!
render(
<Wrapper>
<Title>
Hello World!
</Title>
</Wrapper>
);
4. props에 기반한 Adapting (변경)
React에서 사용하는 것처럼 styled components에 props를 전달하여 함수(interpolations)에 값을 전달할 수 있습니다.
JavaScript interpolation
- 다양한 목적을 위해 기존 문자열에 문자열이나 값을 삽입하는 프로세스
- backtick(`)을 사용하여 문자열을 표현하는 것을 '템플릿 리터럴(template literals)'이라고 합니다.
- 문자열 내부에 ${ }를 사용하여 표현식을 포함할 수 있습니다.
https://www.geeksforgeeks.org/string-interpolation-in-javascript/
let name = "John Doe";
function greet() {
return "hello!";
}
// javascript interpolation
let message = `${greet()} ${name}, Welcome~~! 5+6=${5+6}`;
console.log(message); // hello! John Doe, Welcome~~! 5+6=11
아래 버튼의 component에는 색상을 변경하는 기본 상태가 있습니다.
// typescript
const Button = styled.button<{ $primary?: boolean; }>`
/* Adapt the colors based on primary prop */
background: ${props => props.$primary ? "#BF4F74" : "white"};
color: ${props => props.$primary ? "white" : "#BF4F74"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid #BF4F74;
border-radius: 3px;
`;
render(
<div>
<Button>Normal</Button>
<Button $primary>Primary</Button>
</div>
);
$primay prop을 true로 설정하면 배경색과 텍스트 색상이 바뀌게 됩니다.
// javascript
const Button = styled.button`
/* Adapt the colors based on primary prop */
background: ${props => props.$primary ? "#BF4F74" : "white"};
color: ${props => props.$primary ? "white" : "#BF4F74"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid #BF4F74;
border-radius: 3px;
`;
render(
<div>
<Button>Normal</Button>
<Button $primary={true}>Primary</Button>
</div>
// '={true}' 는 생략 가능
);
5. 스타일 확장 (Extending Styles)
하나의 component를 계속 사용하고 싶지만 어떤 경우에는 조금만 변경하여 사용하고 싶은 경우가 자주 발생합니다. 앞서 소개한 props를 전달하는 방법도 있지만 스타일을 다시 재정의하려면 상당한 노력이 필요합니다.
다른 component의 스타일을 상속하는 새 component를 쉽게 만들 수 있습니다. styled() 생성자를 사용하면 됩니다. 앞에서 만든 버튼을 사용하여 색상 스타일만 변경한 다른 버튼을 만들었습니다.
기존에 만든 Button을 사용하여 새로운 스타일을 적용하려면 styled(Button)으로 생성하고 필요한 속성만 추가하면 됩니다.
// The Button from the last section without the interpolations
const Button = styled.button`
color: #BF4F74;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid #BF4F74;
border-radius: 3px;
`;
// A new component based on Button, but with some override styles
const TomatoButton = styled(Button)`
color: tomato;
border-color: tomato;
`;
render(
<div>
<Button>Normal Button</Button>
<TomatoButton>Tomato Button</TomatoButton>
</div>
);
어떤 경우에는 스타일뿐만 아니라 태그나 구성 요소를 변경해야 할 수도 있습니다. 예를 들어 <a> 링크를 Button에 적용하고 싶다면 "as" polymorphic prop를 사용하여 component에 동적으로 적용할 수 있습니다.
render(
<div>
{/* 기본 버튼 */}
<Button>Normal Button</Button>
{/* 기본 버튼에 링크 추가 */}
<Button as="a" href="#">Link with Button styles</Button>
{/* TomatoButton에 링크 추가 */}
<TomatoButton as="a" href="#">Link with Tomato Button styles</TomatoButton>
</div>
);
다음과 같은 방법으로 component를 커스터마이징 할 수도 있습니다.
const ReversedButton = (props) => (
<Button {...props} children={props.children.split("").reverse()} />
);
render(
<div>
<Button>Normal Button</Button>
<Button as={ReversedButton}>Custom Button with Normal Button styles</Button>
</div>
);
props를 처리할 함수를 만들고, 그 함수에서 기존의 Button 컴포넌트를 처리할 함수를 구현한 다음, 이를 as prop에 전달하여 component를 커스터마이징 할 수 있습니다.
5. 모든 component의 스타일 지정 (Styling any component)
styled 메서드는, DOM element에 className prop을 전달하도록만 만든다면, styled로 만든 component뿐만 아니라 서드파티 component도 완벽하게 동작합니다.
만약 react-native를 사용한다면 className 대신 style을 사용해야 합니다.
// This could be react-router-dom's Link for example
const Link = ({ className, children }) => (
<a className={className}>
{children}
</a>
);
// third-party component도 styled를 사용해 css를 적용할 수 있음.
const StyledLink = styled(Link)`
color: #BF4F74;
font-weight: bold;
`;
render(
<div>
<Link>Unstyled, boring Link</Link>
<br />
<StyledLink>Styled, exciting Link</StyledLink>
</div>
);
styled("div")와 같이 styled() factory를 호출할 때 tag 이름을 전달할 수 있습니다. 실제로 styled.tagname helper는 alias로서 같은 역할을 합니다.
6. 전달된 props (Passed props)
만약 스타일 대상이 단순한 element(예: styled.div)인 경우 styled-components는 잘 알려진 HTML attributes를 DOM으로 전달합니다. 사용자 정의 React component(예: styled(MyComponent))인 경우에 styled-components는 모든 props를 전달합니다.
이 예는 React 요소와 마찬가지로 input component의 모든 props가 마운트 된 DOM 노드에 전달되는 방법을 보여줍니다.
// Create an Input component that'll render an <input> tag with some styles
// typescript :
// const Input = styled.input<{ $inputColor?: string; }>`
const Input = styled.input`
padding: 0.5em;
margin: 0.5em;
color: ${props => props.$inputColor || "#BF4F74"};
background: papayawhip;
border: none;
border-radius: 3px;
`;
// Render a styled text input with the standard input color, and one with a custom input color
render(
<div>
<Input defaultValue="@probablyup" type="text" />
<Input defaultValue="@geelen" type="text" $inputColor="rebeccapurple" />
</div>
);
7. CSS로부터 탈출 (Coming from CSS)
7.1 styled-components는 어떻게 component 내에서 동작할까?
만약 component에(예: CSSModules) css를 import 하는 것에 익숙하다면 아래와 같은 코드에 익숙할 것입니다.
import React from 'react';
import styles from './styles.css'; // css 파일 import
export default class Counter extends React.Component {
state = { count: 0 };
increment = () => this.setState({ count: this.state.count + 1 });
decrement = () => this.setState({ count: this.state.count - 1 });
render() {
return (
<div className={styles.counter}>
<p className={styles.paragraph}>{this.state.count}</p>
<button className={styles.button} onClick={this.increment}>
+
</button>
<button className={styles.button} onClick={this.decrement}>
-
</button>
</div>
);
}
}
styled-components는 element와 style을 지정하는 규칙의 조합이므로 Counter component는 다음과 같이 작성할 수 있습니다.
import React from 'react';
import styled from 'styled-components';
// styled-components는 code 내부에 css style을 포함.
const StyledCounter = styled.div`
/* ... */
`;
const Paragraph = styled.p`
/* ... */
`;
const Button = styled.button`
/* ... */
`;
export default class Counter extends React.Component {
state = { count: 0 };
increment = () => this.setState({ count: this.state.count + 1 });
decrement = () => this.setState({ count: this.state.count - 1 });
// 보다 간결한 JSX를 작성할 수 있게 됨.
render() {
return (
<StyledCounter>
<Paragraph>{this.state.count}</Paragraph>
<Button onClick={this.increment}>+</Button>
<Button onClick={this.decrement}>-</Button>
</StyledCounter>
);
}
}
React component인 Counter와 styled-components의 StyledCounter는 이름이 충돌하지 않고, React Developers와 Web Inspector에서 쉽게 식별할 수 있도록 "Styled" prefix(접두사)를 추가했습니다.
7.2 render method 밖에 styled-components를 정의
render method 밖에 styled-components를 정의하는 것은 중요합니다. 그렇지 않다면(내부에 작성한다면) styled-components는 모든 render pass에서 재생성됩니다. styled-components를 render method 내부에 정의하면 caching을 방해하고, 렌더링 속도가 크게 느려지므로 피해야 합니다.
아래와 같이 작성해야 합니다.
// render method 외부에 styled-components 코드를 작성
const StyledWrapper = styled.div`
/* ... */
`;
const Wrapper = ({ message }) => {
return <StyledWrapper>{message}</StyledWrapper>;
};
아래와 같이 작성하면 안 됩니다.
const Wrapper = ({ message }) => {
// WARNING: THIS IS VERY VERY BAD AND SLOW, DO NOT DO THIS!!!
// 절대 피해야 하는 코딩 스타일
const StyledWrapper = styled.div`
/* ... */
`;
return <StyledWrapper>{message}</StyledWrapper>;
};
추천자료 : Talia Marcassa는 Styled Components: To Use or Not to Use? 에 훌륭한 리뷰를 작성했습니다.
7.3 Pseudoelements, pseudoselectors, and nesting v
styled-components에서 사용하는 preprocessor(전처리기)인 stylis는 스타일을 자동으로 중첩하기 위해 scss와 유사한 syntax(문법) 제공합니다.
이 전처리를 통해 styled-components는 일부의 advanced selector patterns을 지원합니다.
7.3.1 & : ampersand
하나의 ampersand(앰퍼샌드, 앤드 기호)는 component의 all instances(모든 인스턴스)를 나타냅니다.
광범위한 재정의를 적용하는 데 사용합니다.
const Thing = styled.div.attrs((/* props */) => ({ tabIndex: 0 }))`
color: blue;
&:hover {
color: red; // <Thing> when hovered
}
& ~ & {
background: tomato;
// <Thing> as a sibling of <Thing>,
// but maybe not directly next to it
}
& + & {
background: lime; // <Thing> next to <Thing>
}
&.something {
background: orange;
// <Thing> tagged with an additional CSS class ".something"
}
.something-else & {
border: 1px solid;
// <Thing> inside another element labeled ".something-else"
}
`;
render(
<React.Fragment>
<Thing>Hello world!</Thing>
<Thing>How ya doing?</Thing>
<Thing className="something">The sun is shining...</Thing>
<div>Pretty nice day today.</div>
<Thing>Don't you think?</Thing>
<div className="something-else">
<Thing>Splendid.</Thing>
</div>
</React.Fragment>
);
7.3.2 && : double ampersand
이중(double) 앰퍼샌드는 component의 instance(인스턴스)를 나타냅니다.
이는 조건부 스타일 재정의를 수행하고 특정 component의 모든 인스턴스에 스타일을 적용하지 않으려는 경우 유용합니다.
// styled와 css helper를 임포트
import { styled, css } from "styled-components";
const Input = styled.input.attrs((props) => ({ type: "checkbox" }))``;
/* input 요소에 type 속성 전달 */
const Label = styled.label`
align-items: center;
display: flex;
gap: 8px;
margin-bottom: 8px;
`;
const LabelText = styled.span`
${(props) => { /* span에 props를 사용하여 조건부 처리 */
switch (props.$mode) {
case "dark": /* dark 모드인 경우 */
return css`
background-color: gray;
color: white;
${Input}:checked + && { /* checked 상태 */
font-weight: bold;
color: blue;
}
`;
default: /* 기본 */
return css`
background-color: white;
color: black;
${Input}:checked + && { /* checked 상태 */
font-weight: bold;
color: red;
}
`;
}
}}
`;
render(
<React.Fragment>
<Label>
<Input />
<LabelText>Text1</LabelText>
</Label>
<Label>
<Input />
<LabelText $mode="dark">Text2</LabelText>
</Label>
<Label>
<Input defaultChecked />
<LabelText>Text3</LabelText>
</Label>
<Label>
<Input defaultChecked />
<LabelText $mode="dark">Text4</LabelText>
</Label>
</React.Fragment>
);
7.3.3 && : doubleampersand - 우선순위 향상
이중(double) 앰퍼샌드만으로도 "우선순위 향상(precedence boost)"이라는 특별한 동작이 가능합니다. 이는 스타일이 styled-components와 vanilla CSS 환경이 혼합된 환경에서 스타일 충돌을 처리하는 경우 유용할 수 있습니다.
// GlobalStyles.js
import { styled, createGlobalStyle } from "styled-components";
export const Thing = styled.div`
/* && 가 없으므로 GlobalStyle로 적용됨 */
font-weight: bold;
&& {
/* && 로 우선 순위가 향상됨 */
/* 없다면 GlobalStyle의 red가 적용됨 */
color: blue;
}
span {
/* Thing의 선택자로 동작 */
color: green;
font-weight: bold;
}
`;
const GlobalStyle = createGlobalStyle`
div {
color: skyblue;
}
div${Thing} { /* Thing을 재정의 */
color: red;
font-weight: normal;
}
span {
color:orange;
}
`;
export default GlobalStyle;
// App.js
import "./App.css";
import GlobalStyle from "./GlobalStyles";
import { Thing } from "./GlobalStyles";
function App() {
return (
<div className="App">
<GlobalStyle />
<div>
I'm skyblue! <span>am i orange?</span>
</div>
<Thing>
I'm blue, <span>green bla</span> bla bla...
</Thing>
</div>
);
}
export default App;
7.3.4 앰퍼샌드를 사용하지 않는 경우
앰퍼샌드 없이 선택자를 추가하면 구성 요소의 하위 항목을 참조하게 됩니다.
const NewThing = styled.div`
color: blue;
.something { /* 앰퍼샌드없이 선택자 추가 */
border: 1px solid; // an element labeled ".something" inside <Thing>
display: block;
}
`;
render(
<NewThing>
<label htmlFor="foo-button" className="something"> { /* 선택자 적용 */ }
Mystery button
</label>
<button id="foo-button">What do I do?</button>
</NewThing>
)
8. 추가 props 부착 (Attaching additional props)
렌더 된 componet 혹은 element(요소)에 props를 전달하기 위한 불필요한 wrapper 사용을 피하기 위해,. attrs constructor를 사용할 수 있습니다. 이것은 componet에 추가로 props(혹은 attributes)를 전달할 수 있도록 해줍니다.
이 방법을 사용하면 element(요소)에 static props를 부착하거나, React Router의 Link component에 activeClassName과 같은 서드 파티 prop에 값을 전달할 수 있습니다. 게다가 더 많은 dynamic props를 component에 전달할 수도 있습니다. object.attrs()는 component가 받는 props 받는 함수를 사용합니다. 반환 값은 결과 props에도 병합됩니다.
여기서는 Input component를 렌더링 하고 dynamic props와 static attributes를 연결합니다. type: "text" 속성이 input으로 전달된 것을 볼 수 있습니다.
import styled from "styled-components";
const Input = styled.input.attrs((props) => ({
// we can define static props
type: "text",
// or we can define dynamic ones
$size: props.$size || "1em",
}))`
color: #bf4f74;
font-size: 1em;
border: 2px solid #bf4f74;
border-radius: 3px;
/* here we use the dynamically computed prop */
margin: ${(props) => props.$size};
padding: ${(props) => props.$size};
font-weight: ${(props) => (props.$bold ? "bold" : "normal")};
`;
render(
<div>
<Input placeholder="A small text input" />
<br />
<Input placeholder="A bigger text input" $size="2em" />
<br />
<Input placeholder="A bold text input" $bold />
</div>
);
9. .attr 재정의 (Overriding .attrs)
스타일이 지정된 component를 래핑 할 때 .attrs() 가장 안쪽의 스타일이 지정된 component부터 가장 바깥쪽의 스타일이 지정된 componet까지 적용됩니다.
이를 통해 각 wrapper는 스타일시트에서 나중에 정의된 CSS 속성이 이전 선언을 재정의하는 방법과 유사하게 중첩된 .attrs()을 재정의할 수 있습니다.
아래는 Input의 .attrs()가 먼저 적용된 다음 PasswordInput의 .attrs()가 적용됩니다.
import styled from "styled-components";
const Input = styled.input.attrs((props) => ({
type: "text",
$size: props.$size || "1em",
}))`
border: 2px solid #bf4f74;
margin: ${(props) => props.$size};
padding: ${(props) => props.$size};
`;
// Input's attrs will be applied first, and then this attrs obj
const PasswordInput = styled(Input).attrs({
type: "password",
})`
// similarly, border will override Input's border
border: 2px solid aqua;
`;
render(
<div>
<Input placeholder="A bigger text input" $size="2em" />
<br />
{/* Notice we can still use the size attr from Input */}
<PasswordInput placeholder="A bigger password input" $size="2em" />
</div>
);
PasswordInput의 type은 'password'이지만 여전히 Input의 $size 속성을 사용할 수 있습니다.
10. 애니메이션 (Animation)
@keyframes을 사용한 CSS 애니메이션은 single component로 scope가 한정되지 않지만, 전역에서 이름 충돌이 발생하는 것을 원하지 않습니다. 이것이 왜 keyframes helper를 사용하여 app 전체에서 고유한 인스턴스를 생성하는 이유입니다.
import { styled, keyframes } from "styled-components";
// Create the keyframes
const rotate = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
// Here we create a component that will rotate everything we pass in over two seconds
const Rotate = styled.div`
display: inline-block;
animation: ${rotate} 2s linear infinite;
padding: 2rem 1rem;
font-size: 1.2rem;
`;
render(
<Rotate>< 💅🏾 ></Rotate>
);
keyframes는 react-native를 지원하지 않습니다. 대신 ReactNative.Animated API를 사용합니다.
keyframes는 사용 시 늦게 주입되므로(lazily injected), 이는 keyframe과 animation이 어떻게 코드 분할(code-split)되고, 그래서 css helper를 사용하여 공유 스타일 조각(shared style fragments)을 사용해야 하는 이유입니다.
// styled, keyframes, css helper 임포트
import { styled, keyframes, css } from "styled-components";
const rotate = keyframes``;
// ❌ This will throw an error!
const styles = `
animation: ${rotate} 2s linear infinite;
`;
// ✅ This will work as intended
// css helper 사용!!!
const styles = css`
animation: ${rotate} 2s linear infinite;
`;
keyframes를 코드 분할하지 않은 방법은 v3 이하에서 작동했습니다. 그러나 v3에서 업그레이드하는 경우 모든 공유 스타일 조각(shared style fragments)이 css helper를 사용하고 있는지 확인해야 합니다.
11. React Native
styled-components는 React Native에서도 동일한 방법으로 사용할 수 있습니다.
import React from 'react'
import styled from 'styled-components/native' // import는 달라짐.
const StyledView = styled.View`
background-color: papayawhip;
`;
const StyledText = styled.Text`
color: #BF4F74;
`;
class MyReactNativeComponent extends React.Component {
render() {
return (
<StyledView>
<StyledText>Hello World!</StyledText>
</StyledView>
);
}
}
일반적으로 배열인 더 복잡한 스타일(예 : transform)과 단축어(예: margin)를 지원합니다. Thanks To css-to-react-native
Memo
flex 속성은 React Native에서는 레거시와 달리 CSS 단축형으로 동작합니다. 즉 flex: 1; 설정은 flexShrink를 1로, FlexGrow를 1로, flexBasis를 0으로 설정하는 것과 같습니다.
웹 버전과의 차이점 중 일부는 React Native가 키프레임이나 전역 스타일을 지원하지 않기 때문에 keyframes와 createGlobalStyle helper를 사용할 수 없다는 점입니다. 미디어 쿼리를 사용하거나 CSS를 중첩하는 경우에도 경고가 표시됩니다.
참고문헌
https://styled-components.com/docs/basics
댓글