Adebiyi Adedotun Luman, 2020년 7월 23일
짧은 요약 → 컴포넌트 위주의 접근이 웹 애플리케이션들을 구축하는 새로운 방식을 개척했지만 이것도 결점이 없지는 않다. 하나는 그 사용성과 CSS 확장성이다. 이것은 컴포넌트 특유의 방식 - 또는 CSS-in-JS로 알려짐 - 에서 스타일들을 구성하고 관리하는 새로운 방식을 탄생시켰다.
스타일드-컴포넌츠는 컴포넌트들과 스타일링 사이의 틈을 이어주는 CSS-IN-JS(자바스크립트 안의 CSS) 도구로써, 컴포넌트들을 스타일링하는 데 있어 실용성 있고 재사용 면에서 제대로 작동할 수 있도록 수 많은 특질들을 제공해준다. 이 글에서는 스타일드-컴포넌츠의 기초를 배우고 그것들을 리액트 애플리케이션에 어떻게 적절히 적용하는지를 배울 것이다. 리액트 컴포넌트들을 스타일링 하는 다양한 선택 사항들을 찾고 있다면 이 주제 관련 우리의 예전 게시글을 살펴볼 수 있다.
CSS의 핵심은 DOM 트리에서 어느 위치에 있는지에 상관 없이 어떠한 HTML 요소라도 - 전체적으로 - 겨냥할 수 있다는 것이다. 이는 컴포넌트들과 사용될 때는 장애물이 될 수 있는데, 왜냐면 컴포넌트들은 어느 정도는 그것이 사용되는 곳에 가까운 곳에 동일 배치(즉, 상태나 스타일링과 같은 자산을 가지고 있는 것)를 요구하기 때문이다. (현지화-localization-로 알려짐)
리액트 내의 뜻으로, 스타일드-컴포넌츠는 "컴포넌트들을 위한 시각적 초기 단계" 이며 이것들의 목표는 컴포넌트들을 스타일링 하기 위한 융통성 있는 방법을 주는 것이다. 그 결과, 컴포넌트들과 그것들의 스타일들은 서로 꽉 결합된다.
노트: 스타일드-컴포넌츠는 리액트와 리액트 네이티브 둘 다에 쓸 수 있다. 리액트 네이티브 가이드도 반드시 살펴봐야 하겠지만 우리는 여기서 리액트에 대한 스타일된 컴포넌트들에만 중점을 두겠다.
왜 스타일드-컴포넌츠(styled-components)인가?
스타일을 자세히 알아보는 것을 도와주는 것은 일단 제쳐두고, 스타일된 컴포넌트들은 다음 특징들을 포함한다:
- 자동 벤더 접두사 붙이기
당신은 표준 CSS 속성들을 쓸 수 있고, 그러면 스타일된 컴포넌트들이 필요한 대로 벤터 접두사를 덧붙일 것이다.
- 유일한 클래스 이름들
스타일된 컴포넌트들은 서로 독립적이이고, 그 이름들은 라이브러리가 처리해줄 것이므로 걱정할 필요가 없다.
- 죽은 스타일들 제거
사용되지 않는 스타일들은 코드 안에 선언되어 있다 하더라도 스타일된 컴포넌트들이 제거할 것이다.
- 그리고 더 많음
설치
스타일드-컴포넌츠를 설치하는 것은 쉽다. CDN이나 Yarn 과 같은 패키지 매니저를 통해서 할 수 있다.
yarn add styled-components
또는 npm:
npm add styled-components
우리의 데모는 create-react-app을 사용한다.
시작하기
아마도 스타일드-컴포넌츠에서 첫번째로 눈에 들어오는 것이 그 문법일텐데, 스타일드-컴포넌츠 뒤의 마법을 이해하지 못하면 벅찰 수도 있다. 간략히 말하자면, 스타일드-컴포넌츠는 컴포넌트들과 스타일들의 틈을 잇기 위해서 자바스크립트의 템플릿 축약어를 사용한다. 그래서 스타일된 컴포넌트 하나를 만들 때 실제로는 리액트 컴포넌트를 스타일과 함께 만들게 되는 것이다. 이렇게 생겼다:
import styled from "styled-components";
// Styled component named StyledButton
const StyledButton = styled.button`
background-color: black;
font-size: 32px;
color: white;
`;
function Component() {
// Use it like any other component.
return <StyledButton> Login </StyledButton>;
}
여기서, StyledButton은 스타일된 컴포넌트이고 이것이 구현될 때는 HTML 버튼이 스타일을 보유한 것처럼 된다. styled는 스타일링을 자바스크립트에서 실제 CSS로 변환하는 (라이브러리) 내부의 다용도 메소드이다.
가공되지 않은 HTML과 CSS에서는 이렇게 돼있을 것이다:
button {
background-color: black;
font-size: 32px;
color: white;
}
<button> Login </button>
만약 스타일드-컴포넌츠가 리액트 컴포넌트들이라면 프롭(prop)을 쓸 수 있을까? 그렇다. 쓸 수 있다.
프롭(Prop)에 근거하여 적용하기
스타일드-컴포넌츠는 기능적이어서 요소들을 쉽게 역동적으로 스타일링 할 수 있다. 한 페이지에 두 가지 종류의 버튼이 있다고 가정해보자. 하나는 검은색 배경이고 다른 하나는 파란색이다. 그것들을 위해 두 개의 스타일드-컴포넌츠를 만들 필요는 없다; 프롭에 근거하여 그것들의 스타일링을 적용할 수 있다.
import styled from "styled-components";
const StyledButton = styled.button`
min-width: 200px;
border: none;
font-size: 18px;
padding: 7px 10px;
/* The resulting background color will be based on the bg props. */
background-color: ${props => props.bg === "black" ? "black" : "blue";
`;
function Profile() {
return (
<div>
<StyledButton bg="black">Button A</StyledButton>
<StyledButton bg="blue">Button B</StyledButton>
</div>
)
}
StyledButton이 프롭을 수용하는 리액트 컴포넌트기 때문에, bg 프롭의 존재나 그 값에 근거하여 다른 배경색을 지정할 수 있다.
그런데 그 버튼에 type을 지정해주지 않았다는 것을 눈치챘을 것이다. 그렇게 하자:
function Profile() {
return (
<>
<StyledButton bg="black" type="button">
Button A
</StyledButton>
<StyledButton bg="blue" type="submit" onClick={() => alert("clicked")}>
Button B
</StyledButton>
</>
);
}
스타일드-컴포넌츠들은 자신들이 받는 프롭의 종류를 구분할 수 있다. type이 HTML의 성질임을 알아서 자기 것을 처리 하면서 bg프롭을 사용하여 <button type="button">Button A</button>을 실제로 구현할 수 있다. 이벤트 핸들러도 어떻게 붙였는지 봤는가?
성질들에 대한 말이 나왔으니 말인데, 확장된 문법은 attrs 생성자를 사용하여 프롭들을 관리할 수 있게 해준다.
const StyledContainer = styled.section.attrs((props) => ({
width: props.width || "100%",
hasPadding: props.hasPadding || false,
}))`
--container-padding: 20px;
width: ${(props) => props.width}; // Falls back to 100%
padding: ${(props) =>
(props.hasPadding && "var(--container-padding)") || "none"};
`;
너비를 설정하는데 3변수 없이 어떻게 하는지 보이는가? 그것은 우리가 이미 width: props.width || "100%", 로 기본값을 설정했기 때문이다. 또 우리는 CSS 맞춤 속성들도 사용했다. 그렇게 할 수 있으니까!
노트: 스타일드-컴포넌츠가 리액트 컴포넌트들이고 프롭들을 패스할 수 있다면 상태들도 사용할수 있겠지? 이 라이브러리의 깃헙 계정에서 바로 이 문제를 제기하고 있다.
스타일들 확장
당신이 랜딩 페이지 작업을 하고 있다고 치고, 당신은 요소들을 가운데 정렬하기 위해 컨테이너를 특정 최대 너비로 설정했다. 이를 위해 StyledContainer가 있다:
const StyledContainer = styled.section`
max-width: 1024px;
padding: 0 20px;
margin: 0 auto;
`;
그리고나서 당신은 더 작은 컨테이너가 필요하다는 것을 발견한다. 양쪽이 20 픽셀인 대신 10 픽셀인 것을. 처음 드는 생각은 또 다른 스타일된 컴포넌트를 만드는 것일테고 그것도 옳을 수 있겠지만, 머지 않아 당신은 스타일들을 복제하고 있다는 것을 깨닫게 될 것이다.
const StyledContainer = styled.section`
max-width: 1024px;
padding: 0 20px;
margin: 0 auto;
`;
const StyledSmallContainer = styled.section`
max-width: 1024px;
padding: 0 10px;
margin: 0 auto;
`;
StyledSmallContainer를 만들러 가기 전에, 위의 코드 토막처럼 스타일을 재사용하고 전수 받는 방법을 배워보자. 이것은 spread 연산자가 작동하는 방식과 다소 흡사하다.
const StyledContainer = styled.section`
max-width: 1024px;
padding: 0 20px;
margin: 0 auto;
`;
// Inherit StyledContainer in StyledSmallConatiner
const StyledSmallContainer = styled(StyledContainer)`
padding: 0 10px;
`;
function Home() {
return (
<StyledContainer>
<h1>The secret is to be happy</h1>
</StyledContainer>
);
}
function Contact() {
return (
<StyledSmallContainer>
<h1>The road goes on and on</h1>
</StyledSmallContainer>
);
}
StyledSmallContainer에 StyledContainer의 모든 스타일들을 받을 수 있지만 패딩은 덮어쓰기 될 것이다. 대개는 StyledSmallContainer를 위해 구현된 섹션 요소가 있을 것이라는 점을 염두에 두어라. 왜냐하면 그것이 StyledContainer가 구현하는 것이기 때문이다. 하지만 그것이 돌에 각인되어 바뀔 수 없다는 것을 의미하는 것은 아니다.
다형성 프롭 "as"
다형성 프롭 as로, 구현되는 양 끝 요소들을 교환할 수 있다. 한 사례는 스타일을 물려 받을 때이다. (이 전 예시에서처럼) 만약 예를 들어 당신이 StyledSmallContainer에 section 대신 div를 선호한다면 당신이 선호하는 요소의 값을 가진 as 프롭을 스타일된 컴포넌트에 패스할 수 있다. 이렇게:
function Home() {
return (
<StyledContainer>
<h1>It’s business, not personal</h1>
</StyledContainer>
);
}
function Contact() {
return (
<StyledSmallContainer as="div">
<h1>Never dribble when you can pass</h1>
</StyledSmallContainer>
);
}
이제, StyledSmallContainer는 div로 구현될 것이다. 심지어 당신의 값과 같은 맞춤 컴포넌트를 가질 수도 있다.
function Home() {
return (
<StyledContainer>
<h1>It’s business, not personal</h1>
</StyledContainer>
);
}
function Contact() {
return (
<StyledSmallContainer as={StyledContainer}>
<h1>Never dribble when you can pass</h1>
</StyledSmallContainer>
);
}
이걸 당연한 것으로 여기지 말아라.
SCSS 같은 문법
CSS 전처리 장치인 Stylis는 SCSS 같은 문법, 네스팅과 같은 것을 지원하기 위해 스타일된 컴포넌트들을 활성화 한다.
const StyledProfileCard = styled.div`
border: 1px solid black;
> .username {
font-size: 20px;
color: black;
transition: 0.2s;
&:hover {
color: red;
}
+ .dob {
color: grey;
}
}
`;
function ProfileCard() {
return (
<StyledProfileCard>
<h1 className="username">John Doe</h1>
<p className="dob">
Date: <span>12th October, 2013</span>
</p>
<p className="gender">Male</p>
</StyledProfileCard>
);
}
애니메이션
스타일드-컴포넌츠는 (재사용 가능한) 애니메이션 키프레임들을 구성하는데 도움을 주는 keyframes 도우미를 가지고 있다. 이것의 장점은 키프레임들이 스타일드-컴포넌츠로부터 분리되어 필요한 곳 어디에서든지 내보내지고 재사용될 수 있는 것이다.
import styled, {keyframes} from "styled-components";
const slideIn = keyframes`
from {
opacity: 0;
}
to {
opacity: 1;
}
`;
const Toast = styled.div`
animation: ${slideIn} 0.5s cubic-bezier(0.4, 0, 0.2, 1) both;
border-radius: 5px;
padding: 20px;
position: fixed;
`;
글로벌 스타일링
CSS-in-JS, 더 나아가 스타일드-컴포넌츠의 원래 목적이 스타일들의 영역을 지정하는 것이긴 하지만 우리는 또한 스타일드-컴포넌츠의 전체적인 스타일링도 가져다 쓸 수 있다. 대부분 영역이 지정된 스타일들로만 작업할 수 있기 때문에 그것이 불변의 공장 설정이라고 생각하겠지만 당신이 틀렸을 수도 있다. 생각해 보라: 영역 지정이란 게 진짜 무엇인가? 이런 것과 유사한 것을 - 글로벌 스타일링이란 명목 하에 - 하는 것이 기술적으로 가능하다.:
ReactDOM.render(
<StyledApp>
<App />
</StyledApp>,
document.getElementById("root")
);
하지만 우리에겐 이미 자신의 유일한 존재의 이유가 전체적으로 스타일을 적용하는 도우미 함수 - createGlobalStyle - 가 있다. 그럼 왜 그 책임을 거부하는가?
createGlobalStyle을 쓰는 곳 한군데는 CSS 정상화를 위해서이다:
import {createGlobalStyle} from "styled-components";
const GlobalStyle = createGlobalStyle`
/* Your css reset here */
`;
// Use your GlobalStyle
function App() {
return (
<div>
<GlobalStyle />
<Routes />
</div>
);
}
노트: createGlobalStyle로 생성된 스타일들은 자식들을 받아들이지 않는다. 문서에서 더 알아보기.
이쯤에서 당신은 왜 createGlobalStyle을 굳이 써야하는지 궁금할 것이다. 여기 몇 가지 이유가 있다:
- 이것 없이는 뿌리 구현 말고 다른 것은 할 수가 없다. (html, body 등이 예)
- createGlobalStyle은 스타일들을 주입하지만 실제 요소는 하나도 구현하지 않는다. 마지막 예시를 자세히 보면 구현할 어떤 HTML 요소도 구체화하지 않았다는 것을 알 수 있을 것이다. 실제로 그 요소가 필요하지 않을 수도 있기 때문에 이건 좋다. 결국에 우리는 글로벌 스타일에만 관심이 있다. 우리는 구체적인 요소가 아니라 전반적으로 셀렉터들을 겨냥한다.
- createGlobalStyle의 영역은 지정되지 않아 앱의 어디에서든 구현될 수 있고 DOM 내에 있는 한 어디에도 적용될 수 있다. 구조가 아닌 개념을 생각하라.
import {createGlobalStyle} from "styled-components";
const GlobalStyle = createGlobalStyle`
/* Your css reset here */
.app-title {
font-size: 40px;
}
`;
const StyledNav = styled.nav`
/* Your styles here */
`;
function Nav({children}) {
return (
<StyledNav>
<GlobalStyle />
{children}
</StyledNav>
);
}
function App() {
return (
<div>
<Nav>
<h1 className="app-title">STYLED COMPONENTS</h1>
</Nav>
<Main />
<Footer />
</div>
);
}
구조를 생각해 보면 app-title은 GlobalStyle에 설정된 것처럼 스타일되어서는 안 된다. 그런데 이게 그렇게 작동하지 않는다. 당신이 GlobalStyle을 구현하려고 선택할 때마다 컴포넌트가 구현될 때 이것이 주입될 것이다.
주의: createGlobalStyles는 DOM안에 있을 때에만 구현될 것이다.
CSS 도우미
이미 우리는 어떻게 프롭들에 근거하여 스타일을 적용하는 지를 보았다. 만약 우리가 좀 더 깊이 들어가고 싶다면? CSS 도우미 함수가 이것을 달성하게 도와준다. 글자 입력 필드 두 개가 상태들과 함께 있다고 가정하자: 비었으나 활성화되어 있고 각자 다른 색상이다. 이렇게 할 수 있다:
const StyledTextField = styled.input`
color: ${(props) => (props.isEmpty ? "none" : "black")};
`;
다 잘 되었다. 그 다음에 우리는 또 다른 채워진 상태를 더할 필요가 있으니, 스타일을 수정해야 한다:
const StyledTextField = styled.input`
color: ${(props) =>
props.isEmpty ? "none" : props.active ? "purple" : "blue"};
`;
이제 3변수 연산자가 더 복잡하게 커지고 있다. 이 후에 이 글자 입력 필드들에 또 다른 상태를 추가하면 어떻게 될까? 혹은 각 상태에 색깔 외 추가 스타일들을 주고 싶다면? 3변수 연산자 안에 스타일들을 욱여넣는 것을 상상할 수 있는가? css 도우미가 쓸모가 있다.
const StyledTextField = styled.input`
width: 100%;
height: 40px;
${(props) =>
(props.empty &&
css`
color: none;
backgroundcolor: white;
`) ||
(props.active &&
css`
color: black;
backgroundcolor: whitesmoke;
`)}
`;
우리가 한 것은 그 3 변수 문법을 더 많은 스타일들을 수용할 수 있고 더 이해하기 쉽고 정리된 형태로 일종의 확장을 한 것이다. 만약 이전 명령문이 틀린 것 같다면 코드가 너무 많은 것을 하려 하고 있기 때문이다. 그러니 한 발자국 떨어져서 다듬자:
const StyledTextField = styled.input`
width: 100%;
height: 40px;
// 1. Empty state
${(props) =>
props.empty &&
css`
color: none;
backgroundcolor: white;
`}
// 2. Active state
${(props) =>
props.active &&
css`
color: black;
backgroundcolor: whitesmoke;
`}
// 3. Filled state
${(props) =>
props.filled &&
css`
color: black;
backgroundcolor: white;
border: 1px solid green;
`}
`;
다음었더니 스타일링이 관리 가능하고 이해하기 쉬운 세 개의 다른 묶음으로 나뉘었다. 이득이다.
스타일시트매니저
CSS 도우미처럼, StyleSheetManager도 어떻게 스타일들이 처리되는지를 수정하는 도우미 메소드이다. 특정한 프롭들을 받는데 - disableVendorPrefixes 같은 (전체 목록을 확인 할 수 있다) - 벤더 접두사의 종속 트리로부터 손 뗄 수 있게 도와준다.
import styled, {StyleSheetManager} from "styled-components";
const StyledCard = styled.div`
width: 200px;
backgroundcolor: white;
`;
const StyledNav = styled.div`
width: calc(100% - var(--side-nav-width));
`;
function Profile() {
return (
<div>
<StyledNav />
<StyleSheetManager disableVendorPrefixes>
<StyledCard> This is a card </StyledCard>
</StyleSheetManager>
</div>
);
}
disableVendorPrefixes는 <StyleSheetManager>에 프롭으로 넘겨진다. 그래서 <StyledNav>에 있는 것이 아닌 <StyleSheetManager>로 쌓여있는 스타일된 컴포넌트 들은 비활성화된다.
더 쉬운 디버깅
스타일드-컴포넌츠를 내 동료들 중 한 명한테 소개할 때 불만 중 하나가 DOM 안에서 구현된 요소를 찾기가 어렵다는 것이다. - 또는 같은 문제로 리액트 개발자 도구 안에서. 이것이 스타일드-컴포넌츠의 단점들 중 하나이다: 유일한 클래스 이름들을 제공하려는 시도를 하면서 요소들에 유일한 해쉬들을 배정하고 이것이 암호화되는 경우가 있다. 하지만 이것은 더 쉬운 디버깅을 위한 displayName을 해독할 수 있게 한다.
import React from "react";
import styled from "styled-components";
import "./App.css";
const LoginButton = styled.button`
background-color: white;
color: black;
border: 1px solid red;
`;
function App() {
return (
<div className="App">
<LoginButton>Login</LoginButton>
</div>
);
}
기본적으로 스타일된 컴포넌트들은 LoginButton을 <button class="LoginButton-xxxx xxxx">Login</button>로 DOM에 , 리액트 개발자 도구에는 LoginButton로 구현하는데, 이것은 디버깅을 쉽게 해준다. 우리는 displayName이 불리언처럼 행동하길 바라지 않는 이상 그렇게 전환할 수 없다. 이것은 바벨 환경설정을 필요로 한다.
노트: 문서에 babel-plugin-styled-components 패키지가 구체화되어 있고 .babelrc 환경설정 파일도 있다. 여기에서 문제는 우리가 create-react-app을 사용하고 있기 때문에 많은 것들을 꺼내지 않고는 설정 변경을 할 수 없다는 것이다. 여기가 바벨 매크로들이 들어오는 지점이다.
우리는 babel-plugin-macros를 npm이나 Yarn으로 설치하고, 그리고 나서 다음 내용과 함께 애플리케이션의 뿌리에 babel-plugin-magros.config.js를 생성할 필요가 있다:
module.exports = {
styledComponents: {
displayName: true,
fileName: false,
},
};
fileName 값이 반전되면 displayName에는 심지어 더 독창적으로 정교한 접두사가 붙는다.
우리는 또한 macro에서 불러오기를 해야 한다:
// Before
import styled from "styled-components";
// After
import styled from "styled-components/macro";
결론
이제 당신이 코딩으로 CSS를 구성할 수 있다고 해서 그 자유를 남용하지 말라. 스타일된 컴포넌트들을 온전한 상태로 유지하는 데에 최선을 다하라는 것이 내 의견이다. 무거운 조건문들을 구성하려 하지 말고 모든 것들이 스타일된 컴포넌트여야 한다는 생각을 하지 말라. 또, 언젠가는 쓰겠지란 생각으로 그런 사례들을 위한 스타일된 컴포넌트들을 초기에 과하게 추상화 하지 말라.
'웹개발' 카테고리의 다른 글
자바스크립트 개발자로서 알아야할 데이터 구조 (0) | 2020.07.28 |
---|---|
자바스크립트로 더 깔끔한 코드를 쓰는 방법 (0) | 2020.07.27 |
디자인 와이어프레임을 접근성 있는 HTML/CSS로 옮기기 (0) | 2020.07.23 |
잼스택 소개: 안전한 고성능 사이트 만들기 (0) | 2020.07.22 |
어떤 리액트 자바스크립트 프로젝트든지 그 수준을 높여줄 다섯 가지 마이크로 라이브러리들 (0) | 2020.07.21 |