구성 요소 중심 개발( CDD: Component-Driven Development ) 은 구성 요소를 중심으로 빌드 프로세스를 고정하는 개발 방법론입니다.
구성 요소 수준에서 시작하여 페이지 또는 화면 수준에서 끝나는 "아래에서 위로" UI를 구축하는 프로세스입니다
CSS-in-JS 는 단어 그대로 JavaScript코드에서 CSS를 작성하는 방식을 말합니다.
기존 웹사이트는 HTML, CSS, JavaScript를 각자 별도의 파일로 두었는데, React나 Vue, Angluar와 같은 모던 자바스크립트 라이브러리가 인기를 끌고 있습니다
컴포넌트 기반 개발 방법의 주류가 됨에 따라 한 컴포넌트에 HTML, CSS, JavaScript를 모두 포함하는 패턴이 많이 사용되고 있습니다
CSS 코드를 다룰 때 다음과 같은 불편함을 느껴보신 적 있으신가요?
이 외에도 여러 가지 어려움이 있을 수 있는데요. 이런 불편함을 CSS를 컴포넌트화 시킴으로써 해결해 주는 라이브러리가 있습니다. 바로 React 환경에서 사용 가능한 Styled Components라는 라이브러리입니다.
Styled Components는 앞서 배운 CSS in JS라는 개념이 대두되면서 나온 라이브러리입니다. 기존에 HTML, CSS, JS 파일로 쪼개서 개발하던 방법에서, React 등의 라이브러리의 등장으로 컴포넌트 단위 개발이 주류가 되었지만, CSS는 그렇지 못했다는 점에서 출발한 개념이죠.
CSS in JS 라이브러리를 사용하면 CSS도 쉽게 Javascript 안에 넣어줄 수 있으므로, HTML + JS + CSS까지 묶어서 하나의 JS파일 안에서 컴포넌트 단위로 개발할 수 있게 됩니다. 이런 CSS in JS 라이브러리 중에서 현재 가장 인기 있는 라이브러리가 바로 Styled Components입니다.
1. 컴포넌트 만들기
Styled Components로 컴포넌트를 만드는 방법은 다음과 같습니다.
Styled Components는 ES6의 Templete Literals 문법을 사용합니다. 즉, 따옴표가 아닌 백 틱을 사용합니다.
컴포넌트를 선언한 후 styled.태그종류를 할당하고, 백 틱 안에 기존에 CSS를 작성하던 문법과 똑같이 스타일 속성을 작성해 주면 됩니다.
이렇게 만든 컴포넌트를 React 컴포넌트를 사용하듯 리턴문 안에 작성해 주면 스타일이 적용된 컴포넌트가 렌더 되는 것을 확인할 수 있습니다.
import styled from "styled-components";
//Styled Components로 컴포넌트를 만들고
const BlueButton = styled.button`
background-color: blue;
color: white;
`;
export default function App() {
// React 컴포넌트를 사용하듯이 사용하면 됩니다.
return <BlueButton>Blue Button</BlueButton>;
}
2. 컴포넌트를 재활용해서 새로운 컴포넌트 만들기
이미 만들어진 컴포넌트를 재활용해서 새로운 컴포넌트를 만들 수도 있습니다.
컴포넌트를 선언하고 styled()에 재활용할 컴포넌트를 전달해 준 다음, 추가하고 싶은 스타일 속성을 작성해 주면 됩니다.
import styled from "styled-components";
const BlueButton = styled.button`
background-color: blue;
color: white;
`;
//만들어진 컴포넌트를 재활용해 컴포넌트를 만들 수 있습니다.
const BigBlueButton = styled(BlueButton)`
padding: 10px;
margin-top: 10px;
`;
//재활용한 컴포넌트를 재활용할 수도 있습니다.
const BigRedButton = styled(BigBlueButton)`
background-color: red;
`;
export default function App() {
return (
<>
<BlueButton>Blue Button</BlueButton>
<br />
<BigBlueButton>Big Blue Button</BigBlueButton>
<br />
<BigRedButton>Big Red Button</BigRedButton>
</>
);
}
3. Props 활용하기
Styled Component로 만든 컴포넌트도 React 컴포넌트처럼 props를 내려줄 수 있습니다. 내려준 props 값에 따라서 컴포넌트를 렌더링 하는 것도 가능합니다.
위 코드의 경우는 삼항 연산자를 활용해 Button 컴포넌트에 skyblue라는 props가 있는지 확인하고, 있으면 배경색으로 skyblue를, 없을 경우 white를 지정해 주는 코드입니다.
이 코드에 따라 렌더링 된 Button 컴포넌트는 아래 그림과 같을 것입니다.
Button1의 경우는 skyblue라는 props가 있어 배경색이 skyblue로 지정됐고, Button2의 경우는 props가 아예 없어 배경색이 white로 지정된 것을 확인할 수 있습니다.
4. Props 값으로 렌더링하기
3번과 비슷하게, props의 값을 통째로 활용해서 컴포넌트 렌더링에 활용할 수 있습니다.
똑같이 삼항 연산자를 사용하고 있지만, 이번에는 props.color가 없다면 white를, props.color가 있다면 props.color의 값을 그대로 가져와서 스타일 속성 값으로 리턴해주고 있는 것을 볼 수 있습니다.
그 결과 color라는 이름으로 받은 props의 값으로 배경색이 지정된 것을 확인할 수 있습니다.
꼭 삼항 연산자만 사용해야 하는 것은 아닙니다.
JavaScript 코드라면 무엇이든 사용할 수 있으므로 원하는 값을 사용할 수 있도록 함수 코드를 만들어서 사용하면 됩니다. 따라서 위 예시와 같은 코드도 활용할 수 있을 것입니다. props도 원하는 만큼 받아서 사용할 수 있으니 다양하게 활용해 보세요.
5. 전역 스타일 설정하기
스타일을 컴포넌트로 만들 수 있다는 것은 좋지만, 전역에 스타일을 설정하고 싶을 땐 어떻게 하면 좋을까요? Styled Components는 이런 경우를 대비한 컴포넌트도 준비해 놓았습니다.
우선 전역 스타일을 설정하기 위해 Styled Components에서 createGlobalStyle 함수를 불러옵니다.
import { createGlobalStyle } from "styled-components";
// 그다음 이 함수를 사용해 CSS 파일에서 작성하듯 설정해 주고 싶은 스타일을 작성합니다.
const GlobalStyle = createGlobalStyle`
button {
padding : 5px;
margin : 2px;
border-radius : 5px;
}
`
// 이렇게 만들어진 <GlobalStyle> 컴포넌트를 최상위 컴포넌트에서 사용해 주면 전역에 <GlobalStyle> 컴포넌트의 스타일이 적용됩니다.
function App() {
return (
<>
<GlobalStyle />
<Button>전역 스타일 적용하기</Button>
</>
);
}
Styled Components를 사용할 때, CSS 코드를 템플릿 리터럴을 사용해서 작성하게 되는데요. 즉, CSS 코드를 문자열로 작성하는 것이기 때문에, 아래 이미지처럼 VSCode에서 코드 작성 시 CSS 파일에서 작성하는 것처럼 자동 완성 기능을 사용할 수 없습니다.
이때 도움이 되는 VSCode 익스텐션이 있습니다. 바로 VSCode-Styled-Components라는 익스텐션입니다. VSCode 좌측의 Extensions 버튼을 클릭한 후, Vscode-Styled-Components를 검색했을 때 나오는 첫 번째 익스텐션을 설치하시면 됩니다.
Component Driven Development 가 트렌드로 자리 잡게 되면서 이를 지원하는 도구 중 하나인 Component Explorer (컴포넌트 탐색기)가 등장했습니다.
Component Explorer에는 많은 UI 개발 도구가 다양하게 있는데 그중 하나가 Storybook입니다.
Storybook은 UI 개발 즉, Component Driven Development를 하기 위한 도구입니다.
각각의 컴포넌트들을 따로 볼 수 있게 구성해 주어 한 번에 하나의 컴포넌트에서 작업할 수 있습니다. 복잡한 개발 스택을 시작하거나, 특정 데이터를 데이터베이스로 강제 이동하거나, 애플리케이션을 탐색할 필요 없이 전체 UI를 한눈에 보고 개발할 수 있습니다.
Storybook은 재사용성을 확대하기 위해 컴포넌트를 문서화하고, 자동으로 컴포넌트를 시각화하여 시뮬레이션할 수 있는 다양한 테스트 상태를 확인할 수 있습니다. 이를 통해 버그를 사전에 방지할 수 있도록 도와줍니다.
또한 테스트 및 개발 속도를 향상하는 장점이 있으며, 애플리케이션 또한 의존성을 걱정하지 않고 빌드할 수 있습니다.
Storybook은 기본적으로 독립적인 개발 환경에서 실행됩니다. 개발자는 애플리케이션의 다양한 상황에 구애받지 않고 UI 컴포넌트를 집중적으로 개발할 수 있습니다.
회사의 내부 개발자들을 위해 문서화(documentation)를 하여 회사의 UI 라이브러리로써 사용하거나, 외부 공개용 디자인 시스템(Design System)을 개발하기 위한 기본 플랫폼으로 사용할 수 있습니다.
Storybook에서 지원하는 주요 기능은 다음과 같습니다.
React를 공부하면서, React 애플리케이션을 만들 때 DOM을 직접 조작하는 것은 지양해야 한다고 배웠습니다. 하지만, 개발을 하다 보면 DOM을 직접 건드려야 하는 상황이 발생하기도 합니다. 이럴 때 사용할 수 있는 것이 바로 useRef라는 Hook 함수입니다.
지금까지 React를 공부하면서 익히셨던 내용을 돌이켜보면, React만 가지고 거의 대부분의 프론트엔드 요구사항을 구현할 수 있었습니다. DOM 지식이 필요 없다고 생각하실지도 모르겠습니다. 하지만 React로 모든 개발 요구 사항을 충족할 수는 없습니다. 아래와 같이 DOM 엘리먼트의 주소값을 활용해야 하는 경우 특히 그렇습니다.
React는 이런 예외적인 상황에서 useRef로 DOM 노드, 엘리먼트, 그리고 React 컴포넌트 주소값을 참조할 수 있습니다. 아래 예시 코드처럼 작성하시면 주소값을 활용할 수 있습니다.
const 주소값을_담는_그릇 = useRef(참조자료형)
// 이제 주소값을_담는_그릇 변수에 어떤 주소값이든 담을 수 있습니다.
return (
<div>
<input ref={주소값을_담는_그릇} type="text" />
{/* React에서 사용 가능한 ref라는 속성에 주소값을_담는_그릇을 값으로 할당하면*/}
{/* 주소값을_담는_그릇 변수에는 input DOM 엘리먼트의 주소가 담깁니다. */}
{/* 향후 다른 컴포넌트에서 input DOM 엘리먼트를 활용할 수 있습니다. */}
</div>);
이 주소값은 컴포넌트가 re-render 되더라도 바뀌지 않습니다. 이 특성을 활용하여 아래의 제한된 상황에서 useRef를 활용할 수 있습니다.
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>);
}
제시된 상황 제외한 대부분의 경우 기본 React 문법을 벗어나 useRef를 남용하는 것은 부적절하고, React의 특징이자 장점인 선언형 프로그래밍 원칙과 배치되기 때문에, 조심해서 사용해야 합니다.
1. focus
import React, { useRef } from "react";
const Focus = () => {
const firstRef = useRef(null);
const secondRef = useRef(null);
const thirdRef = useRef(null);
const handleInput = (event) => {
console.log(event.key, event);
if (event.key === "Enter") {
if (event.target === firstRef.current) {
secondRef.current.focus();
event.target.value = "";
} else if (event.target === secondRef.current) {
thirdRef.current.focus();
event.target.value = "";
} else if (event.target === thirdRef.current) {
firstRef.current.focus();
event.target.value = "";
} else {
return;
}
}
};
return (
<div>
<h1>타자연습</h1>
<h3>각 단어를 바르게 입력하고 엔터를 누르세요.</h3>
<div>
<label>hello </label>
<input ref={firstRef} onKeyUp={handleInput} />
</div>
<div>
<label>world </label>
<input ref={secondRef} onKeyUp={handleInput} />
</div>
<div>
<label>codestates </label>
<input ref={thirdRef} onKeyUp={handleInput} />
</div>
</div>
);
};
export default Focus;
2. media playback
import { useRef } from "react";
export default function App() {
const videoRef = useRef(null);
const playVideo = () => {
videoRef.current.play();
console.log(videoRef.current);
};
const pauseVideo = () => {
videoRef.current.pause();
videoRef.current.remove();
};
return (
<div className="App">
<div>
<button onClick={playVideo}>Play</button>
<button onClick={pauseVideo}>Pause</button>
</div>
<video ref={videoRef} width="320" height="240" controls>
<source
type="video/mp4"
src="https://player.vimeo.com/external/544643152.sd.mp4?s=7dbf132a4774254dde51f4f9baabbd92f6941282&profile_id=165"
/>
</video>
</div>
);
}