한동안 작업을 마치고 PR을 날리면 다른 팀원들이 코드리뷰할 때 확인할 수 있도록, 작업한 UI의 스크린샷을 함께 첨부해왔었다. 그 동안은 하나 두 개 정도의 스크린샷만 첨부하니 크게 문제가 없었지만, 바로 어제 랜딩 페이지 작업을 마치고 PR을 날리려는데 스크린샷을 찍으려하니 거의 20개의 스크린샷이 필요하여 (데스크탑 9 섹션 + 모바일 9 섹션) 더 이상 이렇게 살 수는 없다고 판단했다!
스토리북
이란 컴포넌트 주도 개발(CDD)를 목적으로 만들어진 UI 테스팅 도구로, 이번 프로젝트를 통해 처음 시도해본 수많은 도구 중 가장 만족스러운 툴이었다.
일반적으로 다른 테스팅 도구들(JEST, Vitest)로 UI 테스트를 하려고 시도해본 결과
1. 원하는 컴포넌트 렌더링
2. 해당 렌더링 결과에 getBy~를 통해 원하는 대상이 있는지 체크
형식으로 진행되었다. 따라서, 내가 원하는 모양으로 렌더링 되었는지, 화면 크기가 달라져도 의도한 대로 그려지는 지 확인하기에는 적절하지 않다고 느껴졌다.
반면, 스토리북을 사용하면, 내가 작성한 Story를 통해 UI의 모습을 바로 확인할 수 있고, 반응형에 맞추어 잘 동작하는 지도 확인할 수 있다. 또한, 자동으로 documentation을 만들어주는 옵션이 있어 다른 개발자와 협업하기에도 유리하고, story를 잘 작성하면 props에 따라 달라지는 UI 모습을 버튼이나 토글을 조작함으로써 쉽게 확인해볼 수 있다.
(인생 첫 스토리북 시도 사진. buttonStyle, label, isRound와 같은 props를 바로 제어하며 변화를 확인해볼 수 있어 좋았다. 👍👍👍)
이 대단한 스토리북은 깃허브 레파지토리와 연결해두면, PR을 날릴 때 자동으로 테스트 검사를 해주고, 해당 레파지토리를 함께 사용하는 다른 사람들도 스토리를 확인할 수 있다고 하여 이 기능을 활용해야겠다고 결심했다!
깃허브와 스토리북을 연동하는 방식 자체는 크게 어렵지 않다.
npx chromatic --project-token <your-project-token>
를 입력하고 고분고분 하라는 거 다 따라하면 된다. 그리고 그 쉬운 거에 n시간을 소모한 이야기를 적어보려 한다.
위 명령어를 잘 수행하면 다음과 같은 체크 리스트가 등장한다.
√ Authenticated with Chromatic
√ Retrieved git information
√ Collected Storybook metadata
√ Initialized build
√ Building your Storybook
√ Publish your built Storybook
√ Verify your Storybook
√ Test your stories
이 체크리스트를 성공적으로 통과해야 github와 storybook을 연결할 수 있는데 시작부터 Authenticated with Chromatic
부분에서 에러가 발생했다. 이 문제에는 두 가지 원인이 있었는데, 이전에 github와 storybook을 시도하다가 포기한 전적이 있어서
"scripts": {
...
"chromatic": "npx chromatic --project-token=토큰",
}
이 부분에 이전 토큰이 남아있어 문제가 되고 있었다.
따라서, chromatic을 삭제 및 재설치하여 새 버전으로 바꿔주고, cli 명령어 설정에도 새로운 토큰으로 변경해주어 첫 번째 난관을 해결하였다.
이 때부터는 github에 뜨기 시작했는데, Test your stories
부분을 통과하지 못해 완전히 연결되지는 않았다.
(처참한 당시의 흔적...)
당시 자꾸 Components Error라고 뜨고, 자세한 오류 메시지를 확인해보니 다음과 같이
page.evaluate: Object
at <anonymous> (/opt/capture/src/chromatic-lib/capture/playwright-renderer/playwrightRenderer.js:804:19)
at NavigatorWatcher.waitForIdle (/opt/capture/src/chromatic-lib/capture/playwright-renderer/NavigatorWatcher.js:191:11)
at ChromeRenderer.renderSpecOrTimeout (/opt/capture/src/chromatic-lib/capture/playwright-renderer/playwrightRenderer.js:800:20)
at ChromeRenderer.renderSpec (/opt/capture/src/chromatic-lib/capture/playwright-renderer/playwrightRenderer.js:690:25)
at ChromeRenderer.renderSnapshot (/opt/capture/src/chromatic-lib/capture/playwright-renderer/playwrightRenderer.js:441:21)
at ChromeRenderer.captureSpecs (/opt/capture/src/chromatic-lib/capture/playwright-renderer/playwrightRenderer.js:313:9)
at ChromeRenderer.executeWithCallback (/opt/capture/src/chromatic-lib/capture/playwright-renderer/playwrightRenderer.js:101:14)
at Capture.execute (/opt/capture/src/chromatic-lib/capture/capture.js:282:5)
at FargateWorker.processJob (/opt/capture/src/worker.ts:258:7)
at FargateWorker.handleJob (/opt/capture/src/worker.ts:137:7)
at FargateWorker.run (/opt/capture/src/worker.ts:50:9)
at start (/opt/capture/src/index.ts:33:5)
사용하지도 않는 playwright가 자꾸 찍히길래 storybook과 playwright의 의존성 문제라고 착각하였다. storybook과 playwright를 번갈아가며 삭제, 재설치도 해보고, chatGPT한테 질문하여 나오는 답변을 다 따라해도 해결되지 않아 포기하려할 때 쯤... reddit에서 다음과 같은 글을 발견하였다.
이 글을 보고 해당 컴포넌트를 확인해보니 에러가 발생한 컴포넌트는 모두 navigation을 사용하고 있는 header, sidebar 컴포넌트였다.
또 그 다음으로 발견한 사실은 이 컴포넌트들이 디자이너님의 디자인을 반영하기 위해 코드를 아예 갈아엎으면서 더 이상 사용하지 않게 된 레거시 코드였다는 사실이었다!
문제를 알았지만, 당장 해결할 필요는 없겠다는 생각으로 해당 컴포넌트들을 삭제하고 가벼운 마음으로 다음 단계로 넘어가는데...
(어찌저찌 얻어낸 작고 소중한 초록불...)
이제 깃허브와 연결은 되었으니 만들어둔 메인 페이지 컴포넌트를 Story로 만들어보았다.
import MainPage from "../pages/MainPage.tsx";
export default {
title: 'Pages/MainPage',
component: MainPage
}
export const MainPageStory = {};
이 코드를 만들고, 스토리북을 실행하니 다시 Components Error로 돌아왔다.
그렇다. MainPage 컴포넌트에서 navigation을 사용하고 있었다. 대충 살려했던 경솔함을 반성하며 열심히 구글링을 했고, 다음 방식으로 해결하였다.
addon 설치
스토리북 자체만으로는 routing을 이해하지 못하기 때문에 addon을 설치해야 한다. 따라서 storybook-addon-react-router-v6
을 설치하였다.
routing addon main.ts에 적용
여기서 말하는 main.ts는 .storybook 폴더 내에 존재하는 main.ts 파일을 의미한다. 여기에 config를 통해 addon을 추가해준다.
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
// 설치한 react-router-v6 addon 추가
"@storybook/addon-react-router-v6",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag",
},
};
export default config;
...
decorators: [
withRouter,
(Story) => (
<div>
<Story />
</div>
),
]
reactRouter: {
initialEntries: ["/"],
initialIndex: 0,
storybookBasePath: "/",
}
우연히 이를 적용하는 방법을 잘 정리해둔 블로그 글을 찾아 비교적 쉽게 해결할 수 있었다. 이를 해결하고 마주친 랜딩페이지는...충격적인 모습을 하고 있었다.
(누구냐 넌...!)
8시간 가까이 작업하여 힘들게 만든 내 랜딩페이지가... 작업할 당시와 다른 모습으로 나타나고 있었다. 알고 보니 스토리북에 폰트 적용이 되어 있지 않아 기본 폰트가 적용이 되고 있었고, 덕분에 위를 포함한 상당한 레이아웃이 이상해보이는 문제가 발생했다.
처음에는 금방 해결하리라 생각했지만, 스토리북이 변화가 빠른 도구인지 1~2년이라도 지난 블로그 글 코드를 입력하고 있으면 더 이상 deprecated되어 없는 속성이라는 문구가 계속 따라왔고,
스토리북 공식 문서에서 알려주는 방식을 따라해보아도 여전히 폰트 적용이 되지 않았다.
그래서 나만의 길을 찾아보았다.
평소에 코드를 참고해볼만한 깃허브 레파지토리를 발견하면 북마크 서비스에 저장해놓는 습관을 들이고 있었다. 그리고, 이 중 storybook을 사용한 서비스들도 종종 있었으니 거기서 어떻게 설정하고 있나를 찾아보아야 겠다고 생각했다.
(그리고 아무도 없었다...)
storybook을 사용하고 있는 레파지토리들은 꽤 있었으나 별다른 폰트 설정을 안 하거나 storybook 관련 코드가 n년전 코드라 동작하지 않겠구나 판단이 들었다.
미련의 끈을 놓지 못하고 storybook 키워드로 깃허브를 계속 뒤져보는데 storybook 레파지토리(정확히 말하면 storybook design system 레파지토리였다)를 찾았다. 그리고, 거기서 해법도 찾았다.
import { Global, css } from '@storybook/theming';
export const fontUrl = 'https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css';
const GlobalStyles = css`
* {
fontFamily: Pretendard,'Noto Sans KR', sans-serif;
}
`;
export const GlobalStyle = () => <Global styles={GlobalStyles} />;
현재 사용중인 폰트(Pretendard)를 다운받을 수 있는 url 정보와 전역 스타일링으로 추가할 스타일 정의를 가지는 전역 스토리북 설정 파일을 만들었다.
import { fontUrl } from "../src/assets/styles/globalStorybook";
const fontLinkId = 'storybook-font-link-tag';
export const loadFontsForStorybook = () => {
if (!document.getElementById(fontLinkId)) {
const fontLink = document.createElement('link');
fontLink.id = fontLinkId;
fontLink.href = fontUrl;
fontLink.rel = 'stylesheet';
document.head.appendChild(fontLink);
}
};
위 코드는 코드 중 'storybook-font-link-tag'라는 id를 가진 element를 찾아 폰트 url을 적용하도록 하는 코드이다.
...
loadFontsForStorybook();
const withGlobalStyle = (storyFn) => (
<>
<GlobalStyle />
{storyFn()}
</>
);
...
decorators: [
withRouter,
withGlobalStyle,
(Story) => (
<div>
<Story />
</div>
),
]
여기까지가 storybook github에서 찾은 내용이었고, 여전히 폰트가 적용이 되지 않아 다음과 같은 시도를 해보았다.
혹시 폰트를 적용시키는 link 태그를 기존과 다르게 적용하고 있는 것이 원인일까?
생각하여 loadFontsForStorybook에 다음을 추가해보았다.
fontLink.type = 'text/css';
그리고 아무 변화도 없었다.
fontFamily 오타 수정
global storybook style을 정의하는 코드에서 font-family가 fontFamily라고 적혀있었다. 이 오타를 수정한 뒤 스토리북을 확인해보아도 여전히 코드가 변경되지 않고 있었다.
important 사용
개발자 도구를 열어보니 내가 설정한 font-family가 적용되고는 있었으나 이후 다른 font-family가 적용이 되면서 해당 설정이 무시되고 있었다. CSS에서 important의 사용을 최대한 지양하라고 배웠지만, 더 이상 지지고 볶을 수 있는 방법이 없다고 판단하여 important를 사용해서 해결하였다.
const GlobalStyles = css`
* {
font-family: Pretendard,'Noto Sans KR', sans-serif !important;
}
`;
결과물
오늘의 교훈 : 오늘 할 일을 내일로 미루지 말자...!