return (
<Container>
<NavContainer>
<Stack.Navigator>
<Stack.Sreen name="Projects" component={CategoriesSreen} />
<Stack.Sreen name="Projects2" component={CategoriesSreen2} />
<Stack.Sreen name="Projects3" component={CategoriesSreen3} />
</Stack.Navigator>
</NavContainer>
</Container>
);
리액트로 개발하다 보면 구성 요소를 컴포넌트로 나눠서 개발하는 것이 익숙할 것입니다.
위 예시를 보면 리액트는 기본적으로 모든 구성 요소가 컴포넌트로 나눠서 관리되고 있습니다.
리액트로 개발을 하다보면 많이 사용되는 컴포넌트 예를 들어 버튼 컴포넌트를 하나 만들어두고 이 컴포넌트를 이용해 다양한 버튼을 만듭니다.
리액트에서는 재사용성을 높이기 위해 이러한 방식을 자주 사용하는데 이를 더 체계적으로 관리하고 테스트하기 위해 스토리북을 사용합니다.
UI 컴포넌트를 독집적으로 분리해서 개별 관리, 테스트를 도와주는 도구입니다.
************************개발자의 관점************************
**********디자이너의 관점**********
기타 장점
설치
npx storybook@latest init
스토리북 실행
npm run storybook
먼저 src 폴더에 components 폴더를 생성하고 Button.js
파일을 하나 만듭니다.
**********Button.js**********
const Button = ({ label, backgroundColor, size, handleClick }) => {
let scale = 1;
if (size === "sm") scale = 0.75;
if (size === "lg") scale = 1.5;
const style = {
backgroundColor,
padding: `${scale * 0.5}rem ${scale * 1}rem`,
border: "none",
color: "white",
};
return (
<button onClick={handleClick} style={style}>
{label}
</button>
);
};
위 코드에서 Button
컴포넌트는 label
, backgroundColor
, size
, handleClick
을 props로 받습니다.
이제 Button.js
파일이 위치한 곳에 Button.stories.js
파일을 하나 만들어줍니다.
먼저 스토리북 설정을 해줍니다.
import React from "react";
import Button from "./Button";
export default {
title: "Button", // 스토리북에서 사용되는 컴포넌트의 카테고리 이름
component: Button, // 스토리북에 표시할 컴포넌트
argTypes: {
handleClick: {
action: "clicked", // handleClick 액션을 정의하여 버튼 클릭 시 스토리북에 이벤트를 기록할 수 있게합니다.
},
},
};
title
: 스토리북에서 이 스토리 그룹을 어떻게 표시할 지 정의합니다.component
: 스토리북에서 사용할 컴포넌트를 지정합니다.argTypes
: 컴포넌트 속성을 관리하는데 사용됩니다. 여기서는 handleClick
속성을 버튼 클릭 액션으로 정의했습니다.스토리 북의 함수를 정의해줍니다.
const Template = (args) => <Button {...args} />;
함수를 선언한 뒤 args
라는 인자를 받습니다. 이 인자는 스토리에서 전달되는 컴포넌트의 속성들를 나타냅니다.
Button
컴포넌트를 렌더링 해주고 …args
로 전달된 속성들을 모두 Button
컴포넌트로 전파합니다.
즉, 스토리에서 설정한 버튼의 속성(label
, backgroundColor
, size
, handleClick
)이 Button
컴포넌트에 전달 됩니다.
각각의 스토리를 만들어줍니다.
export const RedButton = Template.bind({});
RedButton.args = {
label: "Red",
backgroundColor: "red",
size: "md",
};
export const BlueButton = Template.bind({});
BlueButton.args = {
label: "Blue",
backgroundColor: "blue",
size: "md",
};
export const SmButton = Template.bind({});
SmButton.args = {
label: "Small Button",
backgroundColor: "gray",
size: "sm",
};
export const LgButton = Template.bind({});
LgButton.args = {
label: "Large Button",
backgroundColor: "black",
size: "lg",
};
인자로 전달되는 속성들을 정의해 주는 스토리들을 만들어줍니다.
이때 위에서 정의한 Template
함수를 바인딩해주는데 이유는 다음과 같습니다.
Template
함수는 스토리북에 전달될 것이며, 스토리북은 해당 함수를 호출하여 컴포넌트를 렌더링합니다.Template
함수는 스토리에서 사용되는 컴포넌트를 렌더링하는 역할을 합니다. 이때 함수 내에서 this
값은 해당 함수의 컨텍스트가 되며, 그 컨텍스트에서 this
를 사용할 수 있습니다.Template
함수를 스토리북에 바인딩(bind) 하지 않으면, 스토리북은 함수를 적절한 컨텍스트 없이 호출하려고 시도할 것이고, 이로 인해 예기치 않은 동작이 발생할 수 있습니다.요약하자면 Template.bind({})
는 Template
함수를 현재 컨텍스트에 바인딩하여 스토리북에서 올바르게 사용할 수 있도록 보장합니다. 이렇게 하면 스토리북이 Template
함수를 호출할 때 this
를 올바르게 설정하게 되며, 함수 내에서 다른 컴포넌트 속성을 참조할 수 있습니다.
import { RedButton, BlueButton, SmButton, LgButton } from "./Button.stories";
import { render, screen } from "@testing-library/react";
test("should render RedButton", () => {
render(<RedButton {...RedButton.args} />);
expect(screen.getByRole("button")).toHaveTextContent(/Red/i);
expect(screen.getByRole("button")).toHaveStyle("backgroundColor: red");
});
test("should render BlueButton", () => {
render(<BlueButton {...BlueButton.args} />);
expect(screen.getByRole("button")).toHaveTextContent(/Blue/i);
expect(screen.getByRole("button")).toHaveStyle("backgroundColor: blue");
});
test("should render SmButton", () => {
render(<SmButton {...SmButton.args} />);
expect(screen.getByRole("button")).toHaveTextContent(/Small Button/i);
expect(screen.getByRole("button")).toHaveStyle("backgroundColor: gray");
});
test("should render LgButton", () => {
render(<LgButton {...LgButton.args} />);
expect(screen.getByRole("button")).toHaveTextContent(/Large Button/i);
expect(screen.getByRole("button")).toHaveStyle("backgroundColor: black");
});
해당 로직을 알아봅시다.
위 코드는 테스팅 라이브러리인 @testing-library/react
를 사용하여 리액트 컴포넌트를 테스트하는 코드입니다. 이 코드는 스토리 북에서 정의한 버튼 컴포넌트 스토리들을 렌더링하고, 각각의 버튼이 올바르게 동작하고 스타일이 적용되었는지를 확인합니다.
import { RedButotn, BlueButton, SmButton, LgButton } from "./Button.stories";
Button.stories.js
파일에서 정의한 버튼 컴포넌트 스토리들을 가져옵니다.test(…)
) 는 다음과 같은 수행을 진행합니다.render(<RedButton {...RedButton.args} />);
: 해당버튼 컴포넌트를 렌더링합니다.expect(screen.getByRole("button")).toHaveTextContent(/Red/i);
: 버튼이 특정 텍스트(”Red”)를 포함하고 있는지 확인합니다.expect(screen.getByRole("button")).toHaveStyle("backgroundColor: red");
: 버튼의 배경색이 “red”로 설정되어 있는지 확인합니다.RedButton
스토리를 렌더링하고 해당 버튼이 “Red” 라는 텍스트를 가지고 있으며 배경색이 “red”로 설정되어 있는지 확인합니다.이제 pakage.json 파일을 설정해주어야 합니다.
"jest": {
"collectCoverageFrom": [
"<rootDir>/src/components/**/*.{js,jsx}",
"!**/node_modules/**",
"!**/*.stories.{js,jsx}"
]
},
위와 같은 코드를 추가합니다.
이 설정은 jest 에게 어떤 파일을 수집하고 어떤 파일을 제외하고 커버리지를 계산해야 하는지 알려주는 로직입니다.
"collectCoverageFrom"
속성은 어떤 파일에서 커버지리 정보를 수집할지 지정하는 배열입니다. 배열의 각 항목은 파일 경로를 나타내며, 파일 확장자로 .js
또는 .jsx
를 가진 자바스크립트 파일들을 대상으로 합니다.<rootDir>/src/components/**/*.{js,jsx}
속성은 jest 프로젝트 루트 디렉토리(<rootDir>
) 아래의 src/components
디렉토리 내의 모든 js 또는 jsx 파일에 대한 커버리지 정보를 수집합니다.!**/node_modules/**
패턴은 node_modules
디렉토리 내에 있는 몯느 파일을 제외하도록 설정합니다. 보통 node_modules
디렉토리에는 프로젝트의 의존성 모듈이 설치되어 있으며, 이 파일들은 프로젝트 코드가 아니므로 테스트 커버리지에서 제외됩니다.!**/*.stories.{js,jsx}
패턴은 파일 이름이 .stories.js
또는 .stories.jsx
로 끝나는 파일을 제외하도록 설정합니다. 이러한 파일은 주로 스토리북 관련 파일로 테스트 커버리지에 포함되지 않아야 합니다.pakage.json
파일의 scripts
안에 "test:coverage": "react-scripts test --watchAll=false --coverage",
라인을 추가합니다.
위 라인은 테스트를 실행하고 테스트 커버리지 보고서를 생성하는 역할을 합니다. 보통 리액트 프로젝트에서 사용됩니다.
테스트를 실행하기 위해 터미널에 npm run test
를 입력합니다.
테스트를 전부 진행할 것이기 때문에 a를 입력합니다.
테스트가 성공했을 경우
**테스트가 실패했을 경우**coverage를 사용한 테스트도 진행하겠습니다.
터미널에 npm run test:coverage
를 입력합니다.
테스트가 성공했을 경우
테스트가 실패했을 경우
만약 기능들을 전부 테스트 하지 않고 일부만 테스트 한다면 어떤식으로 나올까요? 예시를 보기 위해 Button.test.js
파일의 일부를 주석처리했습니다
import { RedButton, BlueButton, SmButton, LgButton } from "./Button.stories";
import { render, screen } from "@testing-library/react";
test("should render RedButton", () => {
render(<RedButton {...RedButton.args} />);
expect(screen.getByRole("button")).toHaveTextContent(/Red/i);
expect(screen.getByRole("button")).toHaveStyle("backgroundColor: red");
});
// test("should render BlueButton", () => {
// render(<BlueButton {...BlueButton.args} />);
// expect(screen.getByRole("button")).toHaveTextContent(/Blue/i);
// expect(screen.getByRole("button")).toHaveStyle("backgroundColor: blue");
// });
// test("should render SmButton", () => {
// render(<SmButton {...SmButton.args} />);
// expect(screen.getByRole("button")).toHaveTextContent(/Small Button/i);
// expect(screen.getByRole("button")).toHaveStyle("backgroundColor: gray");
// });
test("should render LgButton", () => {
render(<LgButton {...LgButton.args} />);
expect(screen.getByRole("button")).toHaveTextContent(/Large Button/i);
expect(screen.getByRole("button")).toHaveStyle("backgroundColor: black");
});
이는 보다 편리하게 브라우저에서도 확인이 가능합니다.
폴더를 보면 coverage 라는 폴더가 생성되었습니다. coverage 폴더 안에 lcov-report 폴더 안에 index.html 파일을 브라우저로 열어줍니다.
위 이미지와 같이 보기 좋게 나타나있습니다. 해당 이미지의 Button.js 를 클릭하면
이렇게 테스트를 진행하는데 뭐가 빠져있는지 친절하게 알려줍니다.
스토리북을 배포하기 위해서는 github에 레포지토리가 필요합니다.
해당 프로젝트에 chromatic
패키지를 개발 의존성으로 추가합니다.
설치 명령어: npm i -D chromatic
chromatic 페이지에 로그인(회원가입)을 한 뒤
choose from GitHub 를 클릭하여 연동할 깃 저장소를 선택합니다.
chromatic 프로젝트가 생성되었다면
npm i chromatic --project-token=<project-token>
위 명령어를 해당 프로젝트 터미널에서 실행해줍니다.
완료되면 게시된 스토리북에 대한 링크가 제공됩니다.
storybook이 업데이트될 때마다 명령을 수동으로 실행하는 것이 아닌 github를 연동해 코드를 푸시할 때마다 storybook을 지속적으로 배포할 수 있습니다.
해당 프로젝트의 GitHub 저장소로 들어가서 setting을 클릭합니다.
그리고 화면에 표시된 Secrets and variables / actions 을 클릭한 뒤 Repository secrets 의 New repository secrets 을 클릭하고 나서 위에서 발급받은 project-token
을 입력해줍니다.
프로젝트의 루트 폴더에 .github
라는 이름의 새 디렉토리를 만든 뒤 그 안에 workflows
디렉토리를 만듭니다.
위 디렉토리에 chromatic.yml
이라는 이름의 새 파일을 만든 뒤
# Workflow name
name: 'Chromatic Deployment'
# Event for the workflow
on: push
# List of jobs
jobs:
test:
# Operating System
runs-on: ubuntu-latest
# Job steps
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- run: yarn
#👇 Adds Chromatic as a step in the workflow
- uses: chromaui/action@v1
# Options required for Chromatic's GitHub Action
with:
#👇 Chromatic projectToken, see https://storybook.js.org/tutorials/intro-to-storybook/react/en/deploy/ to obtain it
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
위 로직을 추가합니다.
이제 깃허브 코드를 푸시할 때마다 Storybook이 Chromatic에 배포됩니다. Chromatic의 프로젝트 빌드 화면에서 게시된 모든 스토리북을 찾을 수 있습니다.