import styled from 'styled-components';
const Button = styled.button`
background-color: #ededed;
border: none;
border-radius: 8px;
`;
function App() {
return (
<div>
<h1>안녕 Styled Components!</h1>
<Button>확인</Button>
</div>
);
}
export default App;
css코드로 컴포넌트를 만들고있다.
기존방식의 문제점인
- 클래스 이름이 겹치는 경우, 의도하지 않은 스타일이 설정될 수 있다.
- 재사용하는 CSS 코드를 관리하기 어렵다.
을 보완할 수 있는 방식이다.
설치
npm install styled-components
이 글에서는 styled-components 버전5를 기준으로 다룬다.
styled-component를 사용하는 방법
import styled from 'styled-components';
const Button = styled.button`
background-color: #6750a4;
border: none;
color: #ffffff;
padding: 16px;
`;
export default Button;
import Button from './Button';
function App() {
return (
<div>
<Button>Hello Styled!</Button>
</div>
);
}
export default App;
- styled-components의 default import로 'styled'를 가져온다. 대부분의 작업은 이 'styled'함수를 사용할 것이다.
- 변수(Button)에 styled.tagname 을 적고, 그 뒤에는 템플릿 리터럴로 CSS코드를 적는다. 이러한 형식을 태그함수라고 부른다.
- styled.tagname으로 만든 컴포넌트는 일반적인 React Component처럼 JSX에서 사용할 수 있다.
nesting 문법
CSS규칙 안에서 CSS규칙을 만드는 것을 말한다.
&선택자, 컴포넌트 선택자로 Nesting을 활용할 수 있다.
&선택자
&선택자를 사용해서 앞에서 만든 버튼 컴포넌트를 호버하거나 클릭했을 때, 배경색이 바뀌는 예시는 아래와 같다.
import styled from 'styled-components';
const Button = styled.button`
background-color: #6750a4;
border: none;
color: #ffffff;
padding: 16px;
&:hover,
&:active {
background-color: #463770;
}
`;
export default Button;
- 위 코드에서 &선택자를 사용해 작성된 부분을 css로 작성해보면 아래와 같다.
.Button:hover,
.Button:active {
background-color: #463770;
}
컴포넌트 선택자
import styled from 'styled-components';
import nailImg from './nail.png';
const Icon = styled.img`
width: 16px;
height: 16px;
`;
const StyledButton = styled.button`
background-color: #6750a4;
border: none;
color: #ffffff;
padding: 16px;
& ${Icon} {
margin-right: 4px;
}
`;
function Button({ children, ...buttonProps }) {
return (
<StyledButton {...buttonProps}>
<Icon src={nailImg} alt="nail icon" />
{children}
</StyledButton>
);
}
export default Button;
- " ${Icon} " 부분이 컴포넌트 선택자이다.
- 이 부분은 css로 작성하면 아래와 같다.
.StyledButton {
...
}
.StyledButton .Icon {
margin-right: 4px;
}
- 위 코드에선 "&" 와, 컴포넌트 선택자"${Icon}" 사이에 공백을 줌으로 자손결합자로 쓰인것을 볼 수 있는데, 자손결합자로 쓰인 경우에는 "&"를 생략하고 아래와같이 작성할 수 있다. (이 방법을 권장!)
const StyledButton = styled.button`
background-color: #6750a4;
border: none;
color: #ffffff;
padding: 16px;
${Icon} {
margin-right: 4px;
}
`;
nesting은 여러겹으로 할 수 있다.
const StyledButton = styled.button`
...
&:hover,
&:active {
background-color: #7760b4;
${Icon} {
opacity: 0.2;
}
}
`;
위 코드는 아래와 같은 의미를 가진다.
.StyledButton:hover .Icon,
.StyledButton:active .Icon {
opacity: 0.5;
}
다이나믹 스타일링
import styled from 'styled-components';
const SIZES = {
large: 24,
medium: 20,
small: 16,
};
const Button = styled.button`
background-color: #6750a4;
border: none;
border-radius: ${({ round }) => round ? `9999px` : `3px`};
color: #ffffff;
font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
padding: 16px;
&:hover,
&:active {
background-color: #463770;
}
`;
export default Button;
import styled from 'styled-components';
import Button from './Button';
const Container = styled.div`
${Button} {
margin: 10px;
}
`;
function App() {
return (
<Container>
<h1>기본 버튼</h1>
<Button size="small">small</Button>
<Button size="medium">medium</Button>
<Button size="large">large</Button>
<h1>둥근 버튼</h1>
<Button size="small" round>
round small
</Button>
<Button size="medium" round>
round medium
</Button>
<Button size="large" round>
round large
</Button>
</Container>
);
}
export default App;
- styled로 만든 컴포넌트도 위와같이 prop을 받을 수 있다.
- 컴포넌트를 불러올 때, prop을 보냈으니, 해당 컴포넌트에서는 그 prop을 사용하여 스타을 다르게 할 수 있다.
- 모든 styled컴포넌트들은 파라미터로 모든 props를 받아오는데, 위에서는 destructuring문법을 사용해 필요한 것만 받아온 것이다.
- 모두 받아오는 형식은 아래와같이 작성한다.
import styled from 'styled-components';
const SIZES = {
large: 24,
medium: 20,
small: 16,
};
const Button = styled.button`
background-color: #6750a4;
border: none;
border-radius: ${(props) => props.round ? `9999px` : `3px`};
color: #ffffff;
font-size: ${(props) => SIZES[props.size] ?? SIZES['medium']}px;
padding: 16px;
&:hover,
&:active {
background-color: #463770;
}
`;
export default Button;
- ${...} : 표현식 삽입.
- 표현식 삽입법 안에는 JavaScript코드인 변수나 함수를 집어넣을 수 있다.
- prop에 따라 스타일을 다르게 적용하려면 함수를 집어넣어야한다.
- 위 코드에서 'font-size'를 다루는 부분에서는 함수의 결과가 undefined면 에러가 발생할 것이다. 따라서 널병합연산자를 통해 널처리를 해주었다.
스타일 재사용 (상속)
styled components로 만들어진 컴포넌트에 스타일을 입히고 싶을 때에는 styled()함수를 사용한다.
import styled from 'styled-components';
const SIZES = {
large: 24,
medium: 20,
small: 16,
};
const Button = styled.button`
background-color: #6750a4;
border: none;
color: #ffffff;
font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
padding: 16px;
${({ round }) =>
round
? `
border-radius: 9999px;
`
: `
border-radius: 3px;
`}
&:hover,
&:active {
background-color: #463770;
}
`;
export default Button;
import styled from 'styled-components';
import Button from './Button';
const SubmitButton = styled(Button)`
background-color: #de117d;
display: block;
margin: 0 auto;
width: 200px;
&:hover {
background-color: #f5070f;
}
`;
function App() {
return (
<div>
<SubmitButton>계속하기</SubmitButton>
</div>
);
}
export default App;
위 코드를 보면, Button컴포넌트를 상속해서 SubmitButton을 만든 것을 확인할 수 있다.
JSX로 직접 만든 컴포넌트에 styled() 사용하기
function TermsOfService({ className }) {
return (
<div className={className}>
<h1>㈜코드잇 서비스 이용약관</h1>
<p>
환영합니다.
<br />
Codeit이 제공하는 서비스를 이용해주셔서 감사합니다. 서비스를
이용하시거나 회원으로 가입하실 경우 본 약관에 동의하시게 되므로, 잠시
시간을 내셔서 주의 깊게 살펴봐 주시기 바랍니다.
</p>
<h2>제 1 조 (목적)</h2>
<p>
본 약관은 ㈜코드잇이 운영하는 기밀문서 관리 프로그램인 Codeit에서
제공하는 서비스를 이용함에 있어 이용자의 권리, 의무 및 책임사항을
규정함을 목적으로 합니다.
</p>
</div>
);
}
export default TermsOfService;
import styled from 'styled-components';
import Button from './Button';
import TermsOfService from './TermsOfService';
const StyledTermsOfService = styled(TermsOfService)`
background-color: #ededed;
border-radius: 8px;
padding: 16px;
margin: 40px auto;
width: 400px;
`;
const SubmitButton = styled(Button)`
background-color: #de117d;
display: block;
margin: 0 auto;
width: 200px;
&:hover {
background-color: #f5070f;
}
`;
function App() {
return (
<div>
<StyledTermsOfService />
<SubmitButton>계속하기</SubmitButton>
</div>
);
}
export default App;
- styled()로 만든 컴포넌트가 아닌 react 컴포넌트에 스타일을 입히기위해서는 위와같이 처리가 필요하다.
- styled()함수의 파라미터로 해당 컴포넌트를 넣어주는 부분은 똑같다.
- 하지만 JSX컴포넌트에서 파라미터로 ClassName을 받아주고, 내부의 태그에 className을 적용시켜주는 과정이 필요하다.
- styled컴포넌트는 내부적으로 알아서 className을 만들고, 그 className으로 스타일을 적용시키는 방식으로 돌아가기때문에 jsx로 만든 컴포넌트는 className이 없기때문에 위와같은 작업이 필요한 것이다.
- 이 작업이 없다면 className이 없으니까 JSX컴포넌트에 스타일이 입혀질 수 없다.
스타일 재사용(CSS함수)
- 중복되는 CSS코드들을 변수처럼 저장해서 여러번 다시 사용하고 싶을 때 사용할 수 있다.
- 기존에는 아래와같이 같은 코드를 두 번 작성해야했다.
import styled from 'styled-components';
const SIZES = {
large: 24,
medium: 20,
small: 16
};
const Button = styled.button`
font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
`;
const Input = styled.input`
font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
`;
- 하지만 css함수를 사용하면 아래와같이 수정할 수 있다.
import styled, { css } from 'styled-components';
const SIZES = {
large: 24,
medium: 20,
small: 16
};
const fontSize = css`
font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
`;
const Button = styled.button`
${fontSize}
`;
const Input = styled.input`
${fontSize}
`;
- css`css코드` 형태로 작성된 것을 변수에 담아두었다가, 실제 컴포넌트를 만들 때 내부에 이 변수를 넣어주면된다.
- 위 코드에서는 size라는 prop을 받아서 prop에 따라 각각 다른 값을 내려주는 함수를 가지고 있는데, 이러한 함수가 없는 경우에는 'css'라는 함수명을 생략해도되지만 권장하지는 않는다.
// 권장하지않음.
const boxShadow = `
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
`;
// 권장.
const boxShadow = css`
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
`;
글로벌 스타일
createGlobalStyle 함수를 사용해서 만들 수 있다.
글로벌 스타일컴포넌트를 최상위 컴포넌트에서 렌더링하면, 글로벌스타일이 항상 적용된 상태가 되도록 할 수 있다.
예를들면, 아래는 사이트 전체의 폰트와 box-sizing을 지정하는 코드이다.
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
* {
box-sizing: border-box;
}
body {
font-family: 'Noto Sans KR', sans-serif;
}
`;
function App() {
return (
<>
<GlobalStyle />
<div>글로벌 스타일</div>
</>
);
}
export default App;
- 이 함수는 <style>태그를 컴포넌트로 만든다. 그렇다고 <GlobalStyle/> 부분에 스타일태그가 생기는 것은 아니고, Styled Component가 내부적으로 처리해서, head태그안에 우리가 작성한 css코드를 넣어준다.
애니메이션
keryframes : CSS 애니메이션을 만들 때 기준이 되는 지정을 정하고, 적용할 CSS속성을 지정하는 문법을 뜻한다.
아래의 코드는 .ball이란 div태그를 위아래로 움직이는 애니메이션효과를 주는 코드이다.
// js
<div class="ball"></div>
// css
@keyframes bounce {
0% {
transform: translateY(0%);
}
50% {
transform: translateY(100%);
}
100% {
transform: translateY(0%);
}
}
.ball {
animation: bounce 1s infinite;
background-color: #ff0000;
border-radius: 50%;
height: 50px;
width: 50px;
}
맨 마지막에 .ball에대한 css규칙을 보면, animation prop에 bounce라는, keyframes로 만든 변수를 쓰고있다.
애니메이션은 이런식으로만들어진다.
아래에서는 placeholder 애니메이션으로 차이점을 알아볼 수 있다.
기존방식
<div class="placeholder">
<div class="placeholder-item a"></div>
<div class="placeholder-item b"></div>
<div class="placeholder-item c"></div>
</div>
@keyframes placeholder-glow {
50% {
opacity: 0.2;
}
}
.placeholder {
animation: placeholder-glow 2s ease-in-out infinite;
}
.placeholder-item {
background-color: #888888;
height: 20px;
margin: 8px 0;
}
.a {
width: 60px;
height: 60px;
border-radius: 50%;
}
.b {
width: 400px;
}
.c {
width: 200px;
}
styled컴포넌트에서의 방식
import styled, { keyframes } from 'styled-components';
const placeholderGlow = keyframes`
50% {
opacity: 0.2;
}
`;
export const PlaceholderItem = styled.div`
background-color: #888888;
height: 20px;
margin: 8px 0;
`;
const Placeholder = styled.div`
animation: ${placeholderGlow} 2s ease-in-out infinite;
`;
export default Placeholder;
import styled from 'styled-components';
import Placeholder, { PlaceholderItem } from './Placeholder';
const A = styled(PlaceholderItem)`
width: 60px;
height: 60px;
border-radius: 50%;
`;
const B = styled(PlaceholderItem)`
width: 400px;
`;
const C = styled(PlaceholderItem)`
width: 200px;
`;
function App() {
return (
<div>
<Placeholder>
<A />
<B />
<C />
</Placeholder>
</div>
);
}
export default App;
- keyframes라는 함수를 사용해서 'placeholderGlow' 변수를 만들었다.
- 이 변수는 placeholder컴포넌트의 animation prop으로 넘겨지고있다.
- 따라서 placeholder 컴포넌트의 하위에 작성된 A, B, C는 애니메이션의 영향을 받는다.
- keyframes함수가 리턴하는 변수는 단순한 문자열이 아니라 javaScript객체이다.
- 크롬 개발자도구로 살펴보면 아래와같이 id, css규칙등의 내용이 값으로 들어가있다.
- 리턴값이 객체기때문에 반드시 styled함수나 css함수를 통해 사용해야한다.
테마
- light 모드, dark 모드 등을 선택할 수 있는것을 여러 웹사이트에서 찾아볼 수 있다.
- 글자크기나 색 등의 css규칙을 모아놓은 것을 테마라고 부른다.
- styled components에서는 'ThemeProvider'로 테마를 설정할 수 있다.
- 테마기능을 만들기위해서는 현재 테마로 설정된 값을 사이트 전체에서 참조할 수 있어야한다.
- React에서는 이렇게 전체 컴포넌트에서 접근할 수 있는 것은 context로 관리한다.
- styled components에서도 context를 기반으로 테마를 사용할 수 있다.
- ThemeProvider가 context를 내려주는 컴포넌트이다.
const Button = styled.button`
background-color: ${({ theme }) => theme.primaryColor};
import { ThemeProvider } from "styled-components";
import Button from "./Button";
function App() {
const theme = {
primaryColor: '#1da1f2',
};
return (
<ThemeProvider theme={theme}>
<Button>확인</Button>
</ThemeProvider>
);
}
export default App;
위에서 ThemeProvider컴포넌트에 theme 이라는 prop을 내려주고있다.
Button컴포넌트에서는 theme을 받아서 background-color를 '#1da1f2' 로 설정해준다.
아래는 여러개의 테마를 선택할 수 있는 코드이다. useState를 활용한다.
import { useState } from 'react';
import { ThemeProvider } from 'styled-components';
import Button from './Button';
function App() {
const [theme, setTheme] = useState({
primaryColor: '#1da1f2',
});
const handleColorChange = (e) => {
setTheme((prevTheme) => ({
...prevTheme,
primaryColor: e.target.value,
}));
};
return (
<ThemeProvider theme={theme}>
<select value={theme.primaryColor} onChange={handleColorChange}>
<option value="#1da1f2">blue</option>
<option value="#ffa800">yellow</option>
<option value="#f5005c">red</option>
</select>
<br />
<br />
<Button>확인</Button>
</ThemeProvider>
);
}
export default App;
select 요소가 바뀔 때 마다, setTheme함수를 실행해서 primaryColor가 바뀌고, 그 값은 Button컴포넌트로 전달된다.
만약, 테마 설정페이지를 만든다고하면, 테마값을 일반적인 컴포넌트에서 참조할 필요도 생기는데, 그럴때에는 ThemeContext를 불러오면 된다.
이 값은 React Context이기때문에 useContext도 필요하다.
import { useContext } from 'react';
import { ThemeContext } from 'styled-components';
// ...
function SettingPage() {
const theme = useContext(ThemeContext); // { primaryColor: '#...' }
}
아래는 light모드와 dark모드가 있는 예시이다.
import styled, { keyframes } from 'styled-components';
import spinnerImg from './spinner.png';
const SIZES = {
large: 24,
medium: 20,
small: 16,
};
const StyledInput = styled.input`
font-size: ${({ size }) => SIZES[size] ?? SIZES['medium']}px;
border: 2px solid ${({ error }) => (error ? `#f44336` : `#eeeeee`)};
border-radius: ${({ round }) => (round ? `9999px` : `4px`)};
background-color: ${({theme})=> theme.backgroundColor};
color: ${({theme})=> theme.color};
outline: none;
padding: 16px;
position: relative;
`;
...
function Input({ loading, ...inputProps }) {
return (
<Container>
<StyledInput {...inputProps} />
{loading && <Spinner />}
</Container>
);
}
export default Input;
import { useState } from 'react';
import { createGlobalStyle, ThemeProvider } from 'styled-components';
import Input from './Input';
const THEMES = {
light: {
backgroundColor: '#ffffff',
color: '#000000',
},
dark: {
backgroundColor: '#121212',
color: '#ffffff',
},
};
const GlobalStyle = createGlobalStyle`
body {
background-color: ${({theme})=> theme.backgroundColor};
color: ${({theme})=> theme.color}
}
`;
function App() {
const [theme, setTheme] = useState(THEMES['light']);
const handleSelectChange = (e) => {
const nextThemeName = e.target.value;
setTheme(THEMES[nextThemeName]);
};
return (
<ThemeProvider theme={theme}>
<div>
<select onChange={handleSelectChange}>
<option value="light">라이트 모드</option>
<option value="dark">다크 모드</option>
</select>
<GlobalStyle />
<Input />
</div>
</ThemeProvider>
);
}
export default App;
일단 전체적인 배경색, 글자색은 GlobalStyle을 통해서 지정해준다.
그리고 Input의 배경색과 글자색은 Input 컴포넌트가 받아서 처리했다.
상황별 팁
1. 모양은 버튼이지만 기능은 링크인 컴포넌트가 필요할때
버튼태그의 style은 정의되어있지만 a태그의 style은 없다. 따로 만들어줘야할 것 같지만 아래와같이 'as' prop으로 처리할 수 있다.
const Button = styled.button`
/* ... */
`;
<Button href="https://example.com" as="a">
LinkButton
</Button>
위 코드는 버튼스타일이 입혀질것인데, as prop으로 a 가 전달되니, 그 기능은 a태그와 같다. 따라서 href 속성을 주어도 잘 기능한다.
2. 원하지 않는 깊이까지 prop이 전달될 때
- transient prop을 사용할 것이다.
- 아래코드는 Link컴포넌트를 상속하는 StyledLink에 underline 이라는 prop을 주고있다.
- 이 underline prop은 styledLink에서만 사용하는 것을 알 수 있다. Link를 상속하고있기때문에 Link에도 ...props 로 underline이 전달될것이다.
- Link컴포넌트에서는 prop을 a태그안에 넣고있는데, a태그에는 underline이라는 prop을 사용하지않기때문에 에러가 발생한다.
// 에러가 발생하는 코드이다.
import styled from 'styled-components';
function Link({ className, children, ...props }) {
return (
<a {...props} className={className}>
{children}
</a>
);
};
const StyledLink = styled(Link)`
text-decoration: ${({ underline }) => underline ? `underline` : `none`};
`;
function App() {
return (
<StyledLink underline={false} href="https://codeit.kr">
Codeit으로 가기
</StyledLink>
);
}
export default App;
Link컴포넌트에 underline prop이 전달되기까지의 과정은 아래와 같다.
- StyledLink 컴포넌트에서 underline prop을 받는다.
- StyledLink 컴포넌트가 스타일링하고있는 Link컴포넌트에 underline prop이 전달된다.
- Link컴포넌트에 Spread문법을 통해 a태그에 underline이 전달된다.
그렇다면 a태그에 underline 속성을 주지않기위해 아래와같이 수정할 수 있겠다.
function Link({ className, children, underline, ...props }) {
return (
<a {...props} className={className}>
{children}
</a>
);
};
- 하지만 근본적으로 사용하지 않을 underline prop은 Link까지 전달되지 않는것이 맞다.
- TransientProp은 syled components로 스타일링하는 컴포넌트에서만 사용되고, 스타일링의 대상이되는 컴포넌트까지는 prop이 전달되지 않도록 할 수 있다.
- transient : 일시적인, 순간적인
아래코드는 transientProp을 사용한 예시이다.
import styled from 'styled-components';
function Link({ className, children, ...props }) {
return (
<a {...props} className={className}>
{children}
</a>
);
};
const StyledLink = styled(Link)`
text-decoration: ${({ $underline }) => $underline ? `underline` : `none`};
`;
function App() {
return (
<StyledLink $underline={false} href="https://codeit.kr">
Codeit으로 가기
</StyledLink>
);
}
export default App;
transientProp은 앞에 "$" 를 붙여서 만들 수 있다.