컴포넌트 스타일링 방식

리액트에서 컴포넌트를 스타일링할 때는 다양한 방식을 사용할 수 있습니다.


일반 CSS

프로젝트는 일반 CSS 방식으로 만들어져 있습니다.

  • CSS를 작성할 때 가장 중요한 점은 CSS 클래스를 중복되지 않게 만드는 것입니다.

이름 짓는 규칙

프로젝트에 자동 생성된 App.css를 읽어 보면 클래스 이름이 컴포넌트 이름-클래스 형태로 지어져 있습니다.
(예 : App-header) 클래스 이름에 컴포넌트 이름을 포함시켜 다른 컴포넌트에서 실수로 중복되는 클래스를 만들어 사용하는 것을 방지합니다.

비슷한 방식으로 BEM 네이밍 방식도 있습니다. BEM 네이밍은 CSS 방법론 중 하나로 해당 클래스가 어디에서 어떤 용도로 사용되는지 명확하게 작성합니다. (예: .card_title-primary)


CSS Selector

CSS Selector를 사용하면 CSS 클래스가 특정 클래스 내부에 있는 경우에만 스타일을 적용합니다.

1
2
3
4
/* App 클래스 내부의 logo 클래스에만 스타일 적용 */
.App .logo {
...
}

Sass 사용하기

Sass(Syntactically Awesome Style Sheets)는 CSS 전처리기로 스타일 코드의 재활용성을 높여주고 복잡한 작업을 쉽게하여 가독성을 높여 줍니다.

Sass는 두 가지 확장자 .sass와 .scss를 지원합니다.

.sass
1
2
3
4
5
6
$font-stack: Helvetica, sans-serif
$primary-color: #333

body
font: 100% $font-stack
color: $primary-color
.scss
1
2
3
4
5
6
7
$font-stack: Helvetica, sans-serif;
$primary-color: #333;

body {
font: 100% $font-stack;
color: $primary-color;
}

.sass 확장자는 중괄호{}와 세미콜론;을 사용하지 않습니다.

  • 또한 Sass에서는 믹스인 기능을 사용하여 재사용되는 스타일 블록을 함수처럼 사용할 수 있습니다.
mixin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 변수 사용하기
$red: #fa5252;

// 믹스인 사용하기
@mixin square($size) {
$calculated: 32px * $size;
width: $calculated;
height: $calculated;
}

//클래스에 적용하기
.red {
background: $red;
@include square(1);
}

함수 분리하기

Sass 변수 및 믹스인은 다른 파일로 분리하여 작성한 뒤 필요한 파일에서 불러와 여러 파일에 적용시킬 수 있습니다.

utils.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 변수 사용하기
$red: #fa5252;
$orange: #fd7e14;
$yellow: #fcc419;
$green: #40c057;
$blue: #339af0;
$indigo: #5c7cfa;
$violet: #7950f2;

// 믹스인 만들기 (재사용되는 스타일 블록을 함수처럼 사용 할 수 있음)
@mixin square($size) {
$calculated: 32px * $size;
width: $calculated;
height: $calculated;
}

선언한 변수 파일과 믹스인을 따로 빼네어 utils.scss 파일에 저장했습니다.
다른 파일에서 불러올 때는 @import 구문을 사용하여 불러오면 됩니다.

1
2
@import './styles/utils';
(...)

node_modules에서 라이브러리 불러오기

Sass의 장점 중 하나는 라이브러리를 쉽게 불러와서 사용할 수 있다는 점입니다.

1
@import '../../../node_modules/library/styles';

위와 같이 상대 경로를 사용하여 node_modules까지 들어가서 불러올 필요없이 물결 문자(~) 을 사용하여 불러올수 있습니다.

1
@import '~library/styles';

물결 문자(~)를 사용하면 자동으로 node_modules에서 라이브러리 디렉터리를 탐지하여 스타일을 불러옵니다.


CSS Module

CSS Module은 CSS를 불러와서 사용할 때 클래스 이름을 고유한 값으로 자동으로 만들어 컴포넌트 스타일 클래스 이름이 중첩되는 현상을 방지해 주는 기술입니다.

[파일 이름]_[클래스 이름]_[해시값] 형태로 만듭니다.

create-react-app v2 버전 이상 부터 .module.css 확장자로 파일을 저장하기만 하면 CSS Module이 적용됩니다.

CSSModule.module.css라는 파일을 만들어 보겠습니다.

CSSModule.module.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 자동으로 고유해질 것이므로 흔히 사용되는 단어를 클래스 이름으로 마음대로 사용가능*/

.wrapper {
background: black;
padding: 1rem;
color: white;
font-size: 2rem;
}

/* 글로벌 CSS 를 작성하고 싶다면 */
:global {
// :global {} 로 감싸기
.something {
font-weight: 800;
color: aqua;
}
// 여기에 다른 클래스를 만들 수도 있습니다.
}

CSS Module이 적용된 스타일을 불러오면 객체를 전달받게 되는데 CSS Module에서 사용한 클래스 이름이 고유화된 값으로 들어있습니다.

1
2
// ex )
{ wrapper : "CSSModule_wrapper_1SbdQ" }

지정해준 클래스 이름 앞뒤로 파일 이름(CSSModule)과 해쉬값(1SbdQ)이 붙은 형태입니다.

이 고유한 클래스 이름을 사용하는 리엑트 컴포넌트를 작성해 보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
import React from 'react';
import styles from './CSSModule.module.scss';

const CSSModule = () => {
return (
<div className={styles.wrapper}>
안녕하세요, 저는 <span className="something">CSS Module!</span>
</div>
);
};

export default CSSModule;

위와 같이

  • 고유한 클래스 이름을 사용하려면 클래스를 적용하고 싶은 JSX 엘리먼트에 className={styles.[클래스 이름]} 형태로 전달.

  • :global을 사용하여 전역적으로 선언한 클래스의 경우 일반적으로 사용하던 방법과 똑같이 그냥 클래스이름을 넣어주면됩니다. className="something"


클래스 이름 두 개 이상 적용

CSS Module로 만든 클래스 이름을 두 개 이상 적용할 때

ES6의 문법 템플릿 리터럴을 사용하여 문자열을 합침으로 간단히 처리할 수 있습니다.

1
2
3
4
5
6
7
8
// CSSModule.module.css
.wrapper {...}
.inverted {...}

// jsx
import styles from '.CSSModule.module.css'

<div class name= {`${styles.wrapper} ${styles.inverted}`}><div>

템플릿 리터럴 문법으로 작성하고 싶지 않다면 다음과 같은 방식도 있습니다.

1
className={[styles.wrapper, styles.inverted].join(' ')}

classnames 라이브러리

classnames 라이브러리는 CSS 클래스를 조건부로 설정할 때 매우 유용한 라이브러리입니다.

npm으로 설치 시 : npm install classnames
yarn으로 설치 시 : yarn add classnames

classnames의 기본적인 사용법을 살펴봅니다.

classnames 기본적인 사용법
1
2
3
4
5
6
7
8
9
import classNames from 'classnames';

classNames('one', 'two'); // = 'one two'
classNames('one', { two : true }); // = 'one two'
classNames('one', { two : false }); // = 'one'
classNames('one', ['two', 'three']); // = 'one two three'

const myClass = 'hello';
classNames('one', myClass, { myCondition: true }); // = 'one hello myCondition'

이런 식으로 여러 가지 종류의 파라미터를 조합해 CSS 클래스를 설정할 수 있기 때문에 컴포넌트에서 조건부로 클래스를 설정할 때 매우 편합니다. 예를 들어 props 값에 따라 다른 스타일을 주기 편합니다.

예시 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
// classnames 등의 라이브러리 도움없이 작성한다면
const MyComponent = ({ highlighted, theme }) => (
<div className ={`MyComponent ${theme} ${highlighted ? 'highlighted' : ''}`}>
Hello
</div>
);

// classnames 라이브러리 사용 시
const MyComponent = ({ highlighted, theme }) => (
<div className ={classNames('MyComponent', {highlighted}, theme)}>
Hello
</div>
);

classnames 라이브러리 사용 시 엘리먼트의 클래스에 highlighted 값이 true이면 highlighted 클래스가 적용되고, false이면 적용되지 않습니다. 추가로 theme으로 전달받는 문자열은 내용 그대로 클래스에 적용됩니다.

  • classnames 라이브러리는 CSS Module과 함께 사용하면 CSS Module 사용이 훨씬 쉬워집니다.

classnames에 내장되어 있는 bind 함수를 사용하면 클래스를 넣어 줄 때마다 styles.[클래스 이름] 형태를 사용할 필요가 없습니다.

사전에 미리 styles에서 받아 온 후 사용하게 설정해 주고 cx('클래스 이름1', '클래스 이름2') 형태로 사용할 수 있습니다.

CSSModule 컴포넌트에 classnames의 bind 함수 적용한 예
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';
import classNames from 'classnames/bind';
import styles from './CSSModule.module.scss';

const cx = classNames.bind(styles); // 미리 styles 에서 클래스를 받아오도록 설정하고

const CSSModule = () => {
return (
<div className={cx('wrapper', 'inverted')}>
안녕하세요, 저는 <span className="something">CSS Module!</span>
</div>
);
};

export default CSSModule;

classnames의 bind를 사용하여 CSS Module을 사용할 때 클래스를 여러 개 설정하거나, 조건부로 클래스를 설정할 때 편리하게 작성할 수 있습니다.


styled-components

컴포넌트 스타일링 중 자바스크립트 파일 안에 스타일을 선언하는 방식CSS-in-JS라고 부릅니다.

styled-components 는 ‘CSS-in-JS’ 라이브러리 중에서 개발자들이 가장 선호하는 라이브러리입니다.

npm 으로 설치 시 : npm i styled-components
yarn 으로 설치 시 : yarn add styled-components

styled-components를 사용하면 자바스크립트 파일 하나에 스타일까지 작성할 수 있기 때문에
별도의 스타일 파일을 만들지 않아도 된다는 이점이 있습니다.

styled-components를 사용한 예제 코드를 작성해 보겠습니다.

styled-components
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import React from 'react';
import styled, { css } from 'styled-components';

const Box = styled.div`
/* props 로 넣어준 값을 직접 전달해줄 수 있습니다. */
background: ${(props) => props.color || 'blue'};
padding: 1rem;
display: flex;
`;

const Button = styled.button`
background: white;
color: black;
border-radius: 4px;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
font-size: 1rem;
font-weight: 600;

/* & 문자를 사용하여 Sass 처럼 자기 자신 선택 가능 */
&:hover {
background: rgba(255, 255, 255, 0.9);
}

/* 다음 코드는 inverted 값이 true 일 때 특정 스타일을 부여해줍니다. */
${props =>
props.inverted &&
css`
background: none;
border: 2px solid white;
color: white;
&:hover {
background: white;
color: black;
}
`};
& + button {
margin-left: 1rem;
}
`;

const StyledComponent = () => (
<Box color="black">
<Button>안녕하세요</Button>
<Button inverted={true}>테두리만</Button>
</Box>
);

export default StyledComponent;

styled-components의 가장 큰 장점은 props 값으로 전달해 주는 값을 쉽게 스타일에 적용할 수 있다는 것입니다.


Tagged 템플릿 리터럴

스타일을 작성할 때 ` 을 사용하여 만든 문자열에 스타일 정보를 넣어 주었습니다.
여기서 사용한 문법을 Tagged 템플릿 리터럴 이라고 부릅니다.

CSS Module을 배울 때 나온 일반 템플릿 리터럴과 다른 점은 템플릿 안에 자바스크립트 객체나 함수를 전달 할 때 리터럴 안에 넣어준 값을 알아낼 수 있다는 것입니다.

일반 템플릿 리터럴
1
2
`hello ${{foo: 'bar' }} ${() => 'world'}!`
// 결과: "hello [object Object] () => 'world'!"

위 코드는 [object Object] 이런식으로 문자열로 들어가게되면서 형태를 잃어버리게 되는데요,
만약에 함수를 다음과 같이 만들어서 사용하면 이 템플릿 리터럴 안에 넣어준 값들을 온전히 알아낼 수 있게 됩니다.

1
2
3
4
5
6
7
8
function tagged(...args) {
console.log(args);
}
tagged`hello ${{foo: 'bar' }} ${() => 'world'}!`
// (3) [Array(3), {…}, ƒ]
// 0: (3) ["hello ", " ", "!", raw: Array(3)]
// 1: {foo: "bar"}
// 2: () => 'world'

styled-components는 이러한 속성을 사용하여 styled-components로 만든 컴포넌트의 props를 스타일 쪽에서 쉽게 조회할 수 있게 해줍니다.


스타일링 된 엘리먼트 만들기

스타일링 된 엘리먼트를 만들 땐, 상단에서 styled 를 불러오고 styled.태그명 을 사용하여 구현합니다:

1
2
3
4
5
import styled from 'styled-components';

const MyComponent = styled.div`
font-size: 2rem;
`;

이렇게 styled.div 뒤에 Tagged 템플릿 리터럴 문법을 통해 스타일을 넣어 주면,

해당 스타일이 적용된 div로 이루어진 리액트 컴포넌트가 생성됩니다.

그래서 나중에 <MyComponent>Hello</MyComponent>와 같은 형태로 사용할 수 있습니다.

.div 자리에 button 이던, input 이던, 원하는걸 넣으시면 됩니다.

하지만, 만약에 보여줘야 할 태그 형식이 유동적이거나,
특정 컴포넌트에 스타일링을 해야 하는 상황이라면 다음과 같은 형태로 구현 할 수 있습니다

1
2
3
4
5
6
7
8
// 문자열로 styled 의 인자로 전달
const MyInput = styled('input')`
background: gray;
`
// 아예 컴포넌트 형식의 값을 넣어줌
const StyledLink = styled(Link)`
color: blue;
`

스타일에서 props 조회하기

styled-components를 사용하면 스타일링 한 컴포넌트에 전달하는 props 값을 스타일쪽에서 그대로 사용할 수 있습니다.

1
2
3
4
5
6
const Box = styled.div`
/* props 로 넣어준 값을 직접 전달해줄 수 있습니다. */
background: ${props => props.color || 'blue'};
padding: 1rem;
display: flex;
`;

background 값에 props를 조회해서 props.color의 값을 사용하게 하고 color 값이 주어지지 않았을 때는 blue를 기본 색상으로 설정했습니다.

이렇게 만들어진 코드는 JSX에서 사용될 때 다음과 같이 color 값을 props로 사용할 수 있습니다.

1
<Box color="black">(...)</Box>

props 에 따른 조건부 스타일링

일반 CSS 클래스를 사용했더라면 주로 클래스이름으로 조건부 스타일링을 해왔었을텐데요,
styled-components 에서는 그냥 props 로도 처리 가능합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import styled, { css } from 'styled-components';
/* 단순 변수의 형태가 아니라 여러줄의 스타일 구문을 조건부로 설정해야 하는 경우엔
css 를 불러와야합니다.
*/
const Button = styled.button`
background: white;
color: black;
border-radius: 4px;
padding: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
font-size: 1rem;
font-weight: 600;

/* & 문자를 사용하여 Sass 처럼 자기 자신 선택 가능 */
&:hover {
background: rgba(255, 255, 255, 0.9);
}

/* 다음 코드는 inverted 값이 true 일 때 특정 스타일을 부여해줍니다. */
${props =>
props.inverted &&
css`
background: none;
border: 2px solid white;
color: white;
&:hover {
background: white;
color: black;
}
`};
& + button {
margin-left: 1rem;
}
`;

이렇게 만든 컴포넌트는 다음과 같이 props를 사용하여 서로 다른 스타일을 적용할 수 있습니다.

1
2
<Button>안녕하세요</Button>
<Button inverted={ture}>테두리만</Button>

CSS를 사용하지 않고 다음과 같이 바로 문자열을 넣어도 작동하기는 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
${props =>
props.inverted &&
css`
background: none;
border: 2px solid white;
color: white;
&:hover {
background: white;
color: black;
}
`};
& + button {
margin-left: 1rem;
}
`;

이렇게 CSS를 제외하고 작성하면 Tagged 템플릿 리터럴이 아니기 때문에 함수를 받아 사용하지 못해 해당 부분에서는 props 값을 사용하지 못합니다.


반응형 디자인

styled-components를 사용할 때 반응형 디자인을 어떻게 하는지 알아봅니다.

우선 브라우저의 가로 크기에 따라 다른 스타일을 적용하기 위해선 일반 CSS를 사용할 때와 똑같이 media 쿼리(query)를 사용하면 됩니다.

StyledComponent.js - box
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Box = styled.div`
/* props 로 넣어준 값을 직접 전달해줄 수 있습니다. */
background: ${props => props.color || 'blue'};
padding: 1rem;
display: flex;
/* 기본적으로는 1024px 에 가운데 정렬을 하고
가로 크기가 작아짐에 따라 사이즈를 줄이고
768px 미만으로 되면 꽉 채웁니다 */
width: 1024px;
margin: 0 auto;
@media (max-width: 1024px) {
width: 768px;
}
@media (max-width: 768px) {
width: 100%;
}
`;

일반 CSS에서와 큰차이가 없습니다.

만약 여러 컴포넌트에서 이작업을 반복해야 한다면 함수화하여 간편하게 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from 'react';
import styled, { css } from 'styled-components';

const sizes = {
desktop: 1024,
tablet: 768
};

// 위에있는 size 객체에 따라 자동으로 media 쿼리 함수를 만들어줍니다.
// 참고: https://www.styled-components.com/docs/advanced#media-templates
const media = Object.keys(sizes).reduce((acc, label) => {
acc[label] = (...args) => css`
@media (max-width: ${sizes[label] / 16}em) {
${css(...args)};
}
`;

return acc;
}, {});

const Box = styled.div`
/* props 로 넣어준 값을 직접 전달해줄 수 있습니다. */
background: ${props => props.color || 'blue'};
padding: 1rem;
display: flex;
width: 1024px;
margin: 0 auto;
${media.desktop`width: 768px;`}
${media.tablet`width: 768px;`};
`;