React에서 모든 UI 요소를 한 곳에 모아 관리하려는 시도를 해본 적이 있으신가요?
만약 React를 처음 접하신다면 아마도 아직 경험이 없을 것입니다.
이것은 무슨 의미일까요?
예를 들어 react-beautiful-dnd를 참조해 보세요.
예시에서 보신 것과 같은 것을 ‘스토리’라고 부릅니다. 그리고 스토리를 만드는 데 사용되는 도구를 스토리북(Storybook)이라고 합니다.
이제 이 글에서 다룰 내용이 무엇인지 짐작하셨을 것입니다. 바로 알아보도록 하겠습니다.
스토리북이란 무엇일까요?
스토리북은 사용자 인터페이스(UI) 컴포넌트를 격리된 환경에서 개발할 수 있도록 도와주는 도구입니다. 실제 애플리케이션을 실행하지 않고도 다양한 방식으로 컴포넌트를 테스트하고 살펴볼 수 있습니다. 스토리북은 설정에 따라 특정 포트에서 실행됩니다.
React뿐만 아니라 Vue, Angular, Mithril, Marko, Svelte 등 다양한 프론트엔드 프레임워크에서 스토리북을 활용할 수 있습니다.
더 자세한 내용은 여기에서 확인할 수 있습니다.
스토리란 무엇일까요?
스토리는 컴포넌트의 렌더링 상태를 정의합니다. 하나의 컴포넌트도 다양한 props 조합으로 여러 가지 모습으로 표현될 수 있습니다. 각 상태에 대해 별도의 스토리를 작성할 수 있습니다.
예를 들어, ‘Button’ 컴포넌트가 있다고 가정해 봅시다.
이 버튼은 비활성화됨, 로딩 중, 기본, 보조, 작음, 큼, 중간 등의 다양한 상태를 가질 수 있습니다. 모든 상태를 일일이 나열하면 내용이 너무 복잡해질 것입니다. 스토리북을 사용하면서 더 많은 것을 이해할 수 있을 것입니다.
다양한 크기(큼, 중간, 작음)를 가진 버튼 스토리를 예시로 볼 수 있습니다.
프로젝트에 스토리북 설정하기
이제 React 프로젝트에 스토리북을 설정해 보겠습니다.
시작해 봅시다.
- 먼저 다음 명령어를 사용하여 React 프로젝트를 만듭니다. 프로젝트 이름은 자유롭게 지정할 수 있습니다.
npx create-react-app storybook-demo
- 다음으로, 프로젝트에 스토리북을 설치합니다.
npx sb init
스토리북 설정이 완료되었습니다.
스토리북은 별도의 개발 서버를 제공합니다.
어떻게 실행할 수 있을까요?
스토리북은 자동으로 package.json 파일의 scripts 섹션에 명령어를 추가합니다. 다음 명령어를 실행하여 스토리북 서버를 시작할 수 있습니다.
npm run storybook
스토리북은 package.json 파일의 scripts 섹션에 지정된 포트로 새 서버를 시작합니다. 브라우저에서 자동으로 스토리북이 열립니다(React 서버와 동일).
기본적으로 몇 가지 예시 스토리가 제공됩니다. 필요하지 않다면 제거하거나 참고용으로 남겨둘 수 있습니다. 앞서 언급했듯이, 버튼은 여러 상태를 가질 수 있으며, 스토리북에서 이를 확인할 수 있습니다(모든 상태가 다 포함된 것은 아닙니다). 이 튜토리얼의 마지막 부분에서 버튼에 대한 다양한 스토리를 작성해 볼 것입니다.
스토리북의 다른 섹션을 탐색하면서 기능을 익혀보세요. 튜토리얼에서 몇 가지 기능을 더 자세히 다룰 것입니다.
첫 번째 스토리를 작성해 봅시다.
스토리북 테스트하기
스토리북이 실행되는 것을 확인했고, 몇 가지 예시도 살펴보았습니다.
- src 폴더 안에 ‘Button’이라는 폴더를 만듭니다.
- ‘Button.jsx’, ‘Button.css’, ‘constants.js’ 파일을 만듭니다.
- 아래 코드 스니펫을 해당 파일에 복사하여 붙여넣습니다.
Button.jsx
import React, { Component } from "react"; import PropTypes from "prop-types"; import "./Button.css"; import { buttonTypes, buttonVariants, buttonSizes } from "./constants"; class Button extends Component { static defaultProps = { isDisabled: false, type: "filled", variant: "oval", size: "medium", backgroundColor: "#1ea7fd", textColor: "#ffffff", }; static buttonTypes = buttonTypes; static buttonVariants = buttonVariants; static buttonSizes = buttonSizes; renderButton = () => { const { text, isDisabled, type, variant, size, backgroundColor, textColor, onClick, } = this.props; return ( <button onClick={onClick} className={`default ${variant} ${size} ${ isDisabled ? "disabled" : "" }`} style={ type === buttonTypes.outline ? { border: `1px solid ${backgroundColor}`, color: "#000000", backgroundColor: "transparent", } : { backgroundColor: `${backgroundColor}`, border: `1px solid ${backgroundColor}`, color: textColor, } } disabled={isDisabled} > {text} </button> ); }; render() { return this.renderButton(); } } Button.propTypes = { text: PropTypes.string, isDisabled: PropTypes.bool, type: PropTypes.oneOf([buttonTypes.outline, buttonTypes.filled]), variant: PropTypes.oneOf([buttonVariants.oval, buttonVariants.rectangular]), size: PropTypes.oneOf([ buttonSizes.small, buttonSizes.medium, buttonSizes.large, ]), backgroundColor: PropTypes.string, textColor: PropTypes.string, onClick: PropTypes.func, }; export { Button };
Button.css
.default { border: none; cursor: pointer; background-color: transparent; } .default:focus { outline: none; } .disabled { opacity: 0.75; cursor: not-allowed; } .small { font-size: 12px; padding: 4px 8px; } .medium { font-size: 14px; padding: 8px 12px; } .large { font-size: 16px; padding: 12px 16px; } .oval { border-radius: 4px; } .rectangular { border-radius: 0; }
constants.js
export const buttonTypes = { outline: "outline", filled: "filled", }; export const buttonVariants = { oval: "oval", rectangular: "rectangular", }; export const buttonSizes = { small: "small", medium: "medium", large: "large", };
이 코드는 어떤 역할을 할까요?
다양한 용도로 사용할 수 있는 ‘Button’이라는 공통 컴포넌트를 만들었습니다. 이제 이 컴포넌트는 여러 가지 다른 상태를 가질 수 있습니다.
아래 단계를 따라 첫 번째 스토리를 작성해 봅시다.
- ‘Button.stories.jsx’라는 파일을 만듭니다.
- React와 Button 컴포넌트를 해당 파일로 가져옵니다.
- 컴포넌트 스토리의 제목 또는 경로를 정의합니다. 아래 코드를 사용하여 정의합니다.
export default { title: ‘common/Button’, }
위 코드는 현재 파일에 있는 모든 스토리를 ‘common/Button’ 디렉토리에 배치합니다.
- 필수 props를 가진 버튼을 다음과 같이 export합니다.
export const defaultButton = () => ( <Button text=”Default Button” onClick={() => {}} /> );
첫 번째 스토리가 완성되었습니다. 아래 명령어로 스토리북을 실행하고 결과를 확인해 봅시다.
npm run storybook
더 많은 스토리를 작성해 볼 것입니다. 걱정하지 마세요.
프론트엔드 개발에서 어떻게 유용할까요?
스토리북을 사용하는 주요 이점은 무엇일까요?
만약 10명으로 구성된 팀에서 일하고 있고, 현재 작업 중인 프로젝트에서 모두가 작성한 공통 컴포넌트를 확인해야 한다고 가정해 봅시다.
어떻게 해야 할까요?
모든 공통 컴포넌트를 하나씩 확인해야 할 것입니다. 하지만 이는 시간이 많이 걸리고 효율적인 방법이 아닙니다. 바로 이럴 때 스토리북이 등장합니다.
스토리북을 어떻게 활용할 수 있을까요?
스토리북을 사용하여 공통 컴포넌트(모든 UI 컴포넌트)에 대한 스토리를 작성할 수 있습니다. 팀원들은 다른 사람이 작성한 공통 컴포넌트를 확인하고 싶을 때마다 스토리북 서버를 실행하면 됩니다. 위에서 보았던 것처럼 모든 UI 컴포넌트를 한눈에 볼 수 있습니다.
스토리북에서 렌더링된 컴포넌트를 사용하여 더 많은 작업을 수행할 수 있습니다. 스토리북에는 ‘애드온’이라는 개념이 있어 스토리 기능 향상에 도움이 됩니다.
스토리북 자체에서 UI 컴포넌트의 반응형 동작을 확인하고 싶다고 가정해 봅시다. 스토리북에서 ‘Viewport’라는 애드온을 사용할 수 있습니다. 다음 섹션에서 애드온에 대해 자세히 알아보겠습니다.
스토리북 활용하기
이 섹션에서는 공통 컴포넌트인 Button의 다양한 상태를 정의하는 다양한 스토리를 작성해 볼 것입니다.
스토리 작성은 어렵지 않습니다. 스토리는 컴포넌트의 상태를 정의합니다. 컴포넌트의 props를 살펴보면 컴포넌트의 다양한 사용 사례를 쉽게 이해할 수 있습니다.
선택적 props를 사용하여 몇 가지 스토리를 작성해 봅시다.
export const largeButton = () => ( <Button text="Large Button" onClick={() => {}} size="large" /> ); export const outlineSmallButton = () => ( <Button text="Outline Small Button" onClick={() => {}} size="small" type="outline" /> ); export const rectangularLargeButton = () => ( <Button text="Rectangular Large Button" onClick={() => {}} size="large" variant="rectangular" /> ); export const disabledButton = () => ( <Button text="Disabled Button" onClick={() => {}} isDisabled={true} /> ); export const warningButton = () => ( <Button text="Warning Button" onClick={() => {}} backgroundColor="orange" /> );
위의 세 가지 스토리는 Button 컴포넌트의 서로 다른 사용 사례를 정의합니다. 이제 공통 컴포넌트에 대한 더 많은 사례를 추가할 차례입니다. ‘disabledSmallRectangularButton’, ‘dangerButton’, ‘successDisabledButton’ 등을 추가해 보세요.
위의 경우에 대한 코드는 제공하지 않을 것입니다. 직접 코드를 작성하면서 이해를 높이시는 것이 좋을 것입니다. 지금까지 작성한 전체 스토리 코드를 보여드리겠습니다.
import React from "react"; import { Button } from "./Button"; export default { title: "src/common/Button", }; export const defaultButton = () => ( <Button text="Default Button" onClick={() => {}} /> ); export const largeButton = () => ( <Button text="Large Button" onClick={() => {}} size="large" /> ); export const outlineSmallButton = () => ( <Button text="Outline Small Button" onClick={() => {}} size="small" type="outline" /> ); export const rectangularLargeButton = () => ( <Button text="Rectangular Large Button" onClick={() => {}} size="large" variant="rectangular" /> ); export const disabledButton = () => ( <Button text="Disabled Button" onClick={() => {}} isDisabled={true} /> ); export const warningButton = () => ( <Button text="Disabled Button" onClick={() => {}} backgroundColor="orange" /> );
이제 컴포넌트에 대한 스토리 작성에 대한 기본적인 이해가 되었을 것입니다.
다음 섹션으로 넘어가서 애드온에 대해 알아보고 스토리를 더욱 효과적으로 활용하는 방법에 대해 알아보겠습니다.
스토리북 애드온
스토리북은 다양한 애드온을 기본적으로 제공합니다. 이 섹션에서는 개발에 가장 유용한 애드온을 살펴보겠습니다.
버튼 스토리를 더욱 강화해 봅시다.
컨트롤(Controls)
컨트롤은 스토리북 자체에서 컴포넌트에 사용자 지정 props를 제공하는 기능을 추가합니다. Button 컴포넌트의 경우, 스토리북에서 다양한 props를 변경하는 컨트롤을 추가할 수 있습니다.
버튼 배경색에 가장 적합한 색상을 찾아야 한다고 가정해 봅시다. 컴포넌트에 하나씩 색상을 적용하여 배경색을 확인하는 테스트를 진행하는 것은 비효율적입니다. 대신 스토리북에서 다양한 색상을 선택할 수 있는 컨트롤을 추가할 수 있습니다. 스토리북 자체에서 배경색을 테스트할 수 있습니다.
Button 스토리에 컨트롤을 추가하는 방법을 알아봅시다.
먼저 title 아래에 모든 props를 다음과 같이 정의해야 합니다.
export default { title: "src/common/Button", argTypes: { text: { control: "text" }, backgroundColor: { control: "color" }, isDisabled: { control: "boolean" }, size: { control: { type: "select", options: ["small", "medium", "large"] }, }, type: { control: { type: "select", options: ["filled", "outline"] }, }, variant: { control: { type: "select", options: ["oval", "rectangular"] }, }, }, };
다음으로 props를 컴포넌트에서 분리하고 args로 지정합니다.
export const outlineSmallButton = (args) => ( <Button {...args} onClick={() => {}} /> ); outlineSmallButton.args = { text: "Outline Small Button", size: "small", type: "outline", };
컴포넌트 미리보기 창 하단에서 컨트롤을 확인할 수 있습니다.
컴포넌트 미리보기 창 하단에서 ‘Controls’ 탭을 볼 수 있습니다. 다양한 컨트롤을 활용해 보세요.
위와 같은 방식으로 모든 스토리를 업데이트합니다. 스토리북 애드온의 구문을 익히는 것과 같습니다. argTypes에서 다양한 유형의 컨트롤을 사용했습니다. 스토리북에서 제공되는 모든 컨트롤은 여기에서 확인할 수 있습니다.
업데이트된 버튼 스토리 코드는 다음과 같습니다.
import React from "react"; import { Button } from "./Button"; export default { title: "src/common/Button", argTypes: { text: { control: "text" }, backgroundColor: { control: "color" }, isDisabled: { control: "boolean" }, size: { control: { type: "select", options: ["small", "medium", "large"] }, }, type: { control: { type: "select", options: ["filled", "outline"] }, }, variant: { control: { type: "select", options: ["oval", "rectangular"] }, }, }, }; export const defaultButton = (args) => <Button {...args} onClick={() => {}} />; defaultButton.args = { text: "Default Button", }; export const largeButton = (args) => ( <Button {...args} onClick={() => {}} size="large" /> ); largeButton.args = { text: "Large Button", }; export const outlineSmallButton = (args) => ( <Button {...args} onClick={() => {}} /> ); outlineSmallButton.args = { text: "Outline Small Button", size: "small", type: "outline", }; export const rectangularLargeButton = (args) => ( <Button {...args} onClick={() => {}} /> ); rectangularLargeButton.args = { text: "Rectangular Large Button", size: "large", variant: "rectangular", }; export const disabledButton = (args) => <Button {...args} onClick={() => {}} />; disabledButton.args = { text: "Disabled Button", isDisabled: true, }; export const warningButton = (args) => <Button {...args} onClick={() => {}} />; warningButton.args = { text: "Warning Button", backgroundColor: "orange", };
액션(Actions)
액션은 JavaScript에서 발생하는 이벤트입니다. 버튼을 클릭하는 것도 JavaScript 이벤트입니다. 액션 애드온을 사용하여 버튼 클릭에 대한 일부 작업을 수행할 수 있습니다.
액션을 통해 이벤트가 제대로 작동하는지 여부를 테스트할 수 있습니다. 비활성화된 버튼은 클릭할 수 없어야 하며, 활성화된 버튼은 클릭 가능해야 합니다. 액션을 사용하여 이를 확인할 수 있습니다.
버튼 클릭에 액션을 추가하는 방법을 살펴봅시다.
이전에는 onClick prop에 익명 함수를 제공했습니다. 이제 변경해야 합니다.
- 다음 명령문을 사용하여 스토리북 애드온에서 액션을 가져옵니다.
import { action } from "@storybook/addon-actions";
- 모든 ‘() => {}’를 다음 문으로 바꿉니다.
action("Button is clicked!")
이제 스토리북으로 이동하여 버튼을 클릭합니다. 컨트롤 탭 옆에 있는 ‘Actions’ 탭 아래에 메시지가 출력되는 것을 확인할 수 있습니다. 비활성화된 버튼을 클릭하면 메시지가 출력되지 않습니다.
‘onChange’, ‘onMouseOver’, ‘onMouseOut’ 등 다양한 이벤트에 대한 액션을 사용하여 이벤트가 제대로 작동하는지 확인할 수 있습니다. 입력 요소에 대해 ‘onChange’를 동일하게 구현해 보십시오.
액션에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
배경(Backgrounds)
배경 애드온을 사용하면 미리보기 창의 배경을 변경할 수 있습니다. 코드를 작성할 필요 없이 스토리북 내에서 바로 변경할 수 있습니다. 아래 GIF를 참조하세요.
뷰포트(Viewport)
스토리북에서 컴포넌트의 반응형 동작을 테스트할 수도 있습니다. 뷰포트 옵션에 대한 자세한 내용은 아래 GIF를 참조하세요.
문서(Docs)
Docs 애드온을 사용하면 스토리북에서 컴포넌트를 문서화할 수 있습니다. 팀으로 작업할 때 매우 유용합니다. 컴포넌트를 읽고 바로 이해할 수 있으므로 개발 시간을 크게 절약할 수 있습니다.
스토리북의 컴포넌트 미리보기 창에서 ‘Canvas’ 탭의 오른쪽 상단에 있는 ‘Docs’를 볼 수 있습니다. 여기에는 컴포넌트의 모든 스토리에 대한 모든 문서가 포함되어 있습니다. 마크다운과 컴포넌트 렌더링을 모두 포함하는 컴포넌트에 대한 문서를 작성하려면 ‘Button.stories.mdx’ 파일을 사용해야 합니다. 컴포넌트 스토리와 함께 마크다운 코드를 추가하여 작성합니다.
스토리에 대한 문서를 작성하고 있습니다. 코드에는 마크다운과 컴포넌트 렌더링이 모두 포함되어 있습니다. 구문을 배우기만 하면 됩니다. 어렵지 않게 익힐 수 있을 것입니다.
‘Button.stories.mdx’ 문서 코드를 살펴봅시다.
<!--- Button.stories.mdx --> import { Meta, Story, Preview, ArgsTable } from '@storybook/addon-docs/blocks'; import { Button } from './Button'; <Meta title="MDX/Button" component={Button} /> # Button Documentation With `MDX` we can define a story for `Button` right in the middle of our Markdown documentation. <ArgsTable of={Button} /> export const Template = (args) => <Button {...args} /> ## Default Button We can write the documentation related to the Default Button <Preview> <Story name="Default Button" args={{ text: 'Default Button' }}> {Template.bind({})} </Story> </Preview> ## Large Button We are writing sample docs for two stories, you can write rest of them <Preview> <Story name="Large Button" args={{ text: "Large Button", }}> {Template.bind({})} </Story> </Preview>
문서화 컴포넌트에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
다른 추가 기능에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
결론
이 튜토리얼을 통해 스토리북에 대해 배우고 유용하게 활용하는 방법을 익혔기를 바랍니다. 팀에서 스토리북을 효율적으로 사용하여 생산성을 높이세요.
React를 처음 접하시나요? 학습 자료를 확인해 보세요.
즐거운 코딩하세요! 🙂