Storybook 입문 가이드

페이지 단위의 개발이 이루어지던 과거와 달리 요즘의 프론트엔드 개발은 주로 컴포넌트 단위로 이루어진다. 이 컴포넌트라는 개념은 사용하는 라이브러리나 프레임워크에 따라 구현 방식이 다르지만, 철학은 거의 동일하다. 원래 컴포넌트는 외부 상태의 영향을 받지않는 독립된 개체로서, 고립된 환경에서도 자신만의 스타일과 상태를 가질 수 있어야 한다. React의 컴포넌트 정의에 따르면, 컴포넌트가 UI를 독립적이고 재사용 가능한 단위로 분리하고 각 단위를 고립해서 생각할 수 있게 해준다고 설명한다.

이렇게 프론트엔드 개발의 패러다임이 컴포넌트로 넘어오면서 개발자들은 일종의 모순에 봉착했다. 개발은 컴포넌트 단위로 진행하지만 실제 개발환경은 항상 페이지 단위로 만들어진다는 점이다. 현재 개발하고 있는 서비스에서 사용하는 수 많은 버튼의 상태를 의존성과 환경변수가 걸려있는 페이지에서 일일이 코드를 변경해가며 테스트해야 했었고 여전히 그렇게 진행하고 있는 케이스도 있다. 이렇게 개발을 진행하게 되면 개발자는 온전히 뷰에 집중하기 어려워지고 컴포넌트의 의존성을 쉽사리 파악하기가 어려워진다. 컴포넌트를 진짜로 고립시키지 못하게 되는 것이다. 이는 당연히 컴포넌트의 재사용성을 감소시킨다.

Storybook은 이런 문제를 해결할 수 있는, 컴포넌트 단위의 개발 환경을 지원하는 도구다. 개발자가 뷰를 개발할 때 고립된 환경을 제공해서 관심사를 의존성과 환경으로부터 분리시켜 준다. 그리고 개발자는 비로소 뷰에 집중할 수 있게 되고, 외부 상태에 의존하지 않으면서 고립된 상태로 스스로를 표현하는 컴포넌트를 개발할 수 있게 된다.

이 글에서는 Storybook을 한 번도 사용해본 적 없는 사람을 대상으로 기본적인 사용법을 안내한다.

Installation

요즘 충분한 사용자를 갖추고 있는 여러가지 프론트엔드 프레임워크, 그러니까 React, Vue, Angular 등은 컴포넌트 단위의 개발을 기본으로 한다. Storybook은 React 뿐만 아니라 Vue도 지원하며, 최근에는 Angular도 지원하게 되었다. 이 글에서는 React로 예제를 진행할 것이다.

babel이나 webpack 관련 자잘한 설정을 하기에는 설명할 부분이 너무 많기도 하거니와 사실 글의 주제에도 맞지는 않으니, create-react-app을 통해 프로젝트 스캐폴딩을 먼저 진행한다.

1
$ create-react-app storybook-playground

스캐폴딩이 완료되었다면 본격적으로 Storybook을 설치한다. Storybook은 고유의 커맨드라인 인터페이스를 갖추고 있다. CLI를 설치하자.

1
$	 npm install -g @storybook/cli

그리고 다음의 명령어를 사용하여 Storybook을 프로젝트 내에 설치하면 된다.

1
$ getstorybook

그러면 CLI가 알아서 적당히 필요한 의존성을 설치해주고, package.json에도 Storybook 실행 및 빌드 관련 명령어가 추가된다. 이제 실행 스크립트만 사용하면 Storybook이 실행된다.

1
$ npm run storybook

Storybook 개발환경

Story 작성하기

프로젝트 내 src 폴더를 잘 살펴보면 stories라는 폴더가 새로 생긴 걸 확인할 수 있다. 이 폴더가 바로 Storybook에 실제로 올라가는 컴포넌트들을 정의하는 파일들이 있는 폴더다. 그 안의 index.js 파일을 잘 살펴보면 아까 웹 페이지에서 봤던 페이지의 사이드바에 있었던 컴포넌트를 정의하는 코드가 보일 것이다.

보면 알겠지만, 문법은 컴포넌트를 storiesOf 함수로 감싼 뒤에, add로 여러가지 스토리를 추가하는 형태로 사용한다. 여기에서 스토리는 어떠한 환경일 수도 있고, 컴포넌트의 상태일 수도 있다. 예제에서는 Button 컴포넌트는 주입받는 데이터, 그러니까 텍스트와 이모지를 기준으로 스토리를 나누었는데, 뷰가 다르게 보일 수 있는 상태라면 무엇이든 스토리로 써도 된다. 여기에 딱히 엄격한 규칙은 없다. 버튼이라면 일반적으로 사이즈나 컬러, 혹은 :hover:disabled 같은 상태도 별도의 스토리로 써도 된다.

먼저 Storybook을 경험해보기 위해서 새롭게 아주 간단한 컴포넌트와 스토리를 작성해보자.

아주 간단한 Input 컴포넌트를 작성하고, index.js 파일에 관련된 스토리를 추가했다. 이제 저장해서 웹페이지를 확인해보면 새로운 컴포넌트가 생긴 걸 확인해볼 수 있을 것이다.

Action 작성하기

index.js를 다시보면 Button의 onClick 이벤트에 action이라는 함수가 바인딩 되어있는 걸 볼 수 있다. 다시 웹페이지로 돌아가서 버튼을 클릭해보면 아래에 있는 Action Logger에 버튼을 클릭할 때마다 로그가 올라오는 것을 확인할 수 있다. action은 Storybook에서 사용하는 일종의 로깅용 함수다. 각종 이벤트와 함께 날아오는 데이터를 쉽게 볼 수 있다.

마침 우리가 만든 Input 컴포넌트는 이런 데이터를 확인하기에 아주 적합한 컴포넌트다. InputonChange 이벤트에 action을 걸어주자.

그리고 다시 웹으로 돌아가서 <input> 요소 안에 타이핑을 해보면 로그가 계속해서 찍히는 걸 확인해 볼 수 있다. action은 이런 네이티브 이벤트 뿐만이 아니라 프로그래머가 직접 짠 이벤트에도 반응하도록 만들 수 있다.

주의할 점은, action 자체는 함수를 반환하는 함수이므로 이벤트에 바인딩해도 로그를 남기지 않는다는 것이다. 항상 문자열과 함께 함수를 실행시킨 결과를 이벤트에 바인딩하자.

데코레이터(Decorator) 추가하기

Storybook에서 말하는 데코레이터는 현재 Stage 2에 있는 ECMA 데코레이터 스펙과 무관하다. Storybook을 계속해서 사용하다보면 width: 100%인 컴포넌트들은 실제 페이지에 들어갈 때랑 다르게 보이거나, 컴포넌트가 좌상단에 너무 가까이 붙어있어서 전체적인 뷰를 보기에 좋지 않은 경우가 많다. 이 문제를 해결하기 위해서 가장 쉽게 사용할 수 있는 솔루션은 margin이나 padding 값을 가지고 있는 래핑 컴포넌트를 만들어서 테스트할 컴포넌트를 감싸주는 것이다. 이 방법의 문제는 모든 스토리에 래핑 컴포넌트를 번거롭게 일일이 추가시켜줘야 한다는 것이다.

데코레이터는 이러한 불편함을 해결해주는 기능이다. Story를 추가하기 전에 addDecorator라는 메소드를 통해 손쉽게 모든 스토리에 래핑 컴포넌트를 추가할 수 있다.

story는 뒤에 나오는 스토리에서 선언한 컴포넌트를 렌더링하는 함수다. 적절한 래핑 컴포넌트 아래에서 story 함수를 실행시키는 식으로 선언해주면 된다.

addDecorator는 스토리 사이에도 껴넣을 수 있는데, 그러면 addDecorator의 뒤에 있는 스토리들만 그 데코레이터의 영향을 받는다.

애드온 설치하기: Background

실제 작업할 페이지의 배경색깔이 흰색인 경우 상관 없는데, 만약 회색이거나 혹은 뭔가 다른 색이라면 컴포넌트를 작성했을 때의 룩앤필이 페이지에 실제로 들어갔을 때와 좀 다르다고 느끼게 된다. 이런 문제를 개선하기 위해서 Storybook에 별도의 플러그인인 애드온(Add-on)을 설치할 수 있다. 위에서 다뤘던 action도 기본적으로 설치되어있는 애드온이다.

배경 색깔을 지원하기 위한 유틸리티는 크게 두 가지가 있는데, 하나는 데코레이터 형태의 react-storybook-decorator-background이고, 다른 하나는 Storybook Addon Backgrounds이다. 둘 다 사용할 수 있지만 둘 중 후자를 추천한다. 전자는 HMR이 깨지는 버그를 가지고 있고 유지보수도 중지된 것으로 보인다. 후자는 Storybook 레포에 포함되어있는 공식 애드온이라 유지보수가 중단될 일은 없을 것으로 예상된다.

1
$ npm install @storybook/addon-backgrounds --dev

지금도 애드온을 설치하고 쉽게 까먹게 되는 것이, .storybook/addons.js 파일을 수정하는 것이다. Storybook에 설치하는 모든 애드온은 해당 파일에 등록되어야 한다.

보이는 것처럼 addon-actions가 이미 등록되어있고 이 글에서 다루지 않은 addon-links도 등록되어있다. 추가로 addon-backgrounds를 등록해주면 사용 준비가 끝난다.

그리고 아까 데코레이터를 추가했던 것처럼 데코레이터를 추가하면 된다.

그러면 아래처럼 애드온 메뉴가 하나 추가가 되고, 컴포넌트에서 배경화면을 선택할 수 있다.

Storybook Backgrounds Addon

모든 컴포넌트에 이 배경화면 데코레이터를 적용하고 싶다면, .storybook/config.js 파일을 수정할 수도 있다.

이 글에서는 예제로 Background 애드온을 설치했지만 Storybook에는 유용한 애드온들이 많다. 그런 애드온들을 모아둔 페이지도 있으니 한 번 구경해보고 자신의 프로젝트에 맞는 애드온을 선택해 설치하면 더 좋을 것이다.

Storybook을 실제로 도입하기

이렇게 앞에서 스토리를 쓰는 방법에 대해서 알아봤지만 이것만으로 실제 프로젝트에 도입하기는 충분치는 않다. 각 프로젝트 별로 사용하는 기술스택이나 요구사항이 서로 다를 수 있기 때문이다. 만약 모든 스토리들을 index.js에 써야한다면, 제 아무리 Storybook이 좋은 툴이라고 하더라도 도입하기가 매우 망설여질 것이다. 여기서는 Storybook을 실제로 프로젝트에 도입하면서 필요한 작업들을 간단하게 알아본다.

Story 파일 로드 설정

편의상 지금까지는 스토리를 구분하지 않고 index.js에만 작성했으나, 스토리는 index.js 파일에만 작성할 수 있는 것이 아니다. 아까 잠시 살펴본 .storybook/config.js 파일을 다시 보면, loadStories라는 함수 안에 있는 require 구문을 확인할 수 있을 것이다. 즉, loadStories에서 로드하는 모든 파일에 있는 스토리들을 추가한다. 기본적으로 stories/index.js 파일만 로드할 뿐이다.

흔히 React에서 폴더 구조를 짤 때 흔히 접근하는 방법 중에 컴포넌트 별로 폴더를 따로 만들고 그 컴포넌트에 필요한 여러가지 파일들을 만드는 방식이 있다. 아래처럼,

1
2
3
4
5
6
├── Input
| ├── index.js
| ├── Input.js
| ├── Input.scss
| └── Input.test.js
└── ...

Storybook도 컴포넌트 하나하나 스토리가 별도로 필요하기 때문에 stories 폴더 하나에 관리했다가는 곧 stories 폴더가 열기도 싫어지는 불상사가 일어날 것이다. 따라서 스토리 파일도 컴포넌트 별로 폴더에 관리하는 것을 추천한다. 아래처럼,

1
2
3
4
5
6
├── Input
| ├── index.js
| ├── Input.js
| ├── Input.scss
| └── Input.stories.js
└── ...

이때 확장자는 stories.js 혹은 story.js 가 주로 사용되는 것 같다. 나는 보통 stories.js를 사용한다.

이렇게 하려면 위에서 말했던 것처럼, loadStories 함수를 수정해주면 된다.

require.context는 webpack에서 지원하는 함수다. Storybook은 내부적으로 webpack을 사용하기 때문에 쓸 수 있다. 이렇게 하면 src/components 아래에 있는 폴더란 폴더는 다돌면서 *.stories.js 파일을 찾아 모두 로드시킨다.

TypeScript 설정

앞서 말했듯 Storybook은 webpack을 사용하며, 설정이 Customize 가능하다. 이 말은 물론 TypeScript를 지원한다는 말이 된다. ts-loader를 쓰면 되니까. TypeScript 설정 방법은 공식 사이트에도 나와있다. 단지 .storybook/webpack.config.js 파일을 만들어서 아래와 같이 써주면 된다.

마찬가지로 Absolute Import 같은 것이 필요하다거나 하는, webpack의 설정을 건드려야 하는 상황이 있다면 다시 webpack.config.js를 수정하면 된다.

Deploy

Storybook은 컴포넌트 단위로 쪼개서 개발할 수 있는 개발환경이지만, 결국은 그 자체로 컴포넌트들의 카탈로그 혹은 말 그대로 책자가 된다. 이렇게 모아놓은 컴포넌트들은 약간의 문서화를 더한다면1 그 자체로도 인터랙티브한 문서가 되고, 디자이너 혹은 기획자와 협업하는 도구로 사용할 수도 있게 된다. 물론 이렇게 협업하는 도구로 사용하고 싶다면 배포가 필요하다.

Storybook은 기본적으로 서버가 없고, 완전한 정적 애셋만으로 구동이 가능하므로 어디에나 쉽게 배포할 수 있다. 흔히 알고 있는 정적 사이트 호스팅 서비스, GitHub Pages 혹은 AWS S3에도 물론 배포할 수 있고, 가볍게 쓰기에 좋은 Heroku나 now에도 올려놓을 수 있다.

가장 간편한 건 공식 지원이 있는 GitHub Pages다. Storybook에서는 storybook-deployer라는 배포 툴을 지원한다. 하지만 오픈 소스가 아닌이상, GitHub Pages에 배포하기는 조금 꺼려지기 마련이다.

AWS S3도 쉬운 선택지 중에 하나지만, AWS 계정도 있어야되고 셋업하기 귀찮은 면도 있으므로 상대적으로 간단한 솔루션인데다 무료인 now를 사용해서 예제를 진행할 것이다. now도 다루자면 세부적인 설정이 많기 때문에 필요한 부분만 간단히 다루겠다.

먼저 now CLI를 설치한다. now는 별도의 바이너리가 있어서 Node.js 없어도 구동이 가능하지만 이 글을 보고있는 사람들은 모두 Node.js가 설치되어 있을테니, npm으로 설치하는 게 가장 간단하다.

1
$ npm install -g now

now가 설치되었으면 본격적으로 Storybook을 빌드한다. 빌드 명령어는 이미 package.json에 포함되어있으니 그냥 실행만 하면 된다.

1
$ npm run build-storybook

빌드된 파일은 storybook-static 폴더에 모두 들어간다. 이제 빌드된 파일을 디플로이하면 된다.

1
$ now deploy storybook-static

회원가입이 아직 안되어있는 상태라면 회원가입이 필요할 것이다. 회원가입이 어렵지는 않으니 여기서 다루지는 않겠다. 디플로이가 완료되면 .now.sh로 끝나는 URL이 표시되며, 실제 그 URL로 접속하면 Storybook이 잘 나오는 걸 볼 수 있다.

하지만 now로 배포하면 URL에 랜덤하게 문자열이 붙는데, 매번 배포할 때마다 주소가 바뀌기 때문에 다른 사람들과 공유하기가 상당히 번거로워진다. 이걸 고정하기 위해서 alias 설정을 할 수 있다.

위에서 이름이나 alias 필드 같은 경우는 프로젝트에 맞게 수정하면 된다. 그리고 아래처럼 명령어를 입력한다.

1
$ now deploy storybook-static --local-config=now.json

name이나 public 설정이 안먹는 경우가 있는데 만약 그런 경우를 만난다면 다음과 같이 강제로 지정해주면 된다. 깊게 안봐서 그런지 아직 왜 그런지 모르겠는데.. 이 부분 아는 분 댓글 좀..

1
$ now deploy storybook-static --local-config=now.json --public --name={프로젝트명}

마지막으로 alias 명령어도 입력해주면 고정된 주소로 배포할 수 있다.

1
$ now alias --local-config=now.json

성공한다면 {프로젝트명}.now.sh 라는 주소로 배포될 것이다. 이를 모아서 npm 스크립트로 만들어두면 간단하게 배포할 수 있다.

1
$ npm run build-storybook && now deploy storybook-static --local-config=now.json && now alias --local-config=now.json

Wrap up

해외에서 널리 알려진 것에 비해서 우리나라에서는 아직까지는 주위에서 사용사례가 흔치는 않은 것 같다. 사실 별로 어렵지도 않고, 이렇다 할 사용방법도 딱히 필요없는 Storybook을 굳이 이렇게 글까지 써가면서 설명한 건, 내가 Storybook을 도입 후 사용하면서 큰 만족감을 느꼈고 다른 개발자들도 쓰길 바라는 마음에서였다. 물론 나도 그렇게 오랫동안 사용하지는 않았지만. 나 같은 경우는 전혀 알지도 못하고 있다가 작년에 React Seoul에서 진겸님의 발표를 보고 뽕맞아서 시작하게 되었는데 그 발표영상슬라이드도 있으니 한 번 참고하시면 좋겟다.


  1. 1.마크다운으로 문서화를 할 수 있는 애드온도 있다.